Created
October 16, 2025 00:44
-
-
Save chenyaofo/68da8a15dba8835d8a1a426fbdc16200 to your computer and use it in GitHub Desktop.
Return backtesting for any trading pair on Binance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # grid_backtest_step_both_unique_range_progress.py | |
| # 在上一版基础上新增: | |
| # 每天结束后打印执行进度: | |
| # [arith_step10] finished 2025-10-14 cash=xxxxx pairs=xxx realized=xxx | |
| # 并在日志文件 runs/.../progress.log 中同步写入。 | |
| import csv, os, io, zipfile, urllib.request | |
| from datetime import datetime, timezone, timedelta | |
| from torch.utils.tensorboard import SummaryWriter | |
| # ====== 基本配置 ====== | |
| SYMBOL = "ETHFDUSD" | |
| INTERVAL = "1s" | |
| DATA_DIR = "data" | |
| INVEST_Q = 10_000.0 | |
| FEE_RATE = 0.000 | |
| GRID_LOWER = 3700.0 | |
| GRID_UPPER = 4800.0 | |
| ARITH_STEPS = [5,10,20,30,40,50,60,70,80] | |
| START_DATE = "2025-09-01" | |
| END_DATE = "2025-10-14" | |
| LOGROOT = "runs/grid_step_both_unique_range_progress" | |
| # ====================== | |
| OPEN_TIME=0; OPEN=1; HIGH=2; LOW=3; CLOSE=4; CLOSE_TIME=6 | |
| HOUR_MS = 3_600_000 | |
| def ms13(x:str)->int: return int(str(x).strip()[:13]) | |
| def iso_utc(ms:int)->str: return datetime.fromtimestamp(ms/1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") | |
| def daterange_utc(start_date:str, end_date:str): | |
| s=datetime.strptime(start_date,"%Y-%m-%d"); e=datetime.strptime(end_date,"%Y-%m-%d") | |
| while s<=e: | |
| yield s.strftime("%Y-%m-%d") | |
| s+=timedelta(days=1) | |
| def local_csv_path(date_str): return os.path.join(DATA_DIR, f"{SYMBOL}-{INTERVAL}-{date_str}.csv") | |
| def remote_zip_url(date_str): return f"https://data.binance.vision/data/spot/daily/klines/{SYMBOL}/{INTERVAL}/{SYMBOL}-{INTERVAL}-{date_str}.zip" | |
| def ensure_day_csv(date_str): | |
| os.makedirs(DATA_DIR,exist_ok=True) | |
| path=local_csv_path(date_str) | |
| if os.path.exists(path): return path | |
| url=remote_zip_url(date_str) | |
| print(f"[download] {url}") | |
| with urllib.request.urlopen(url) as resp: data=resp.read() | |
| with zipfile.ZipFile(io.BytesIO(data)) as zf: | |
| members=[n for n in zf.namelist() if n.endswith(".csv")] | |
| if not members: raise RuntimeError("no csv in zip") | |
| with zf.open(members[0]) as zcsv, open(path,"wb") as fout: fout.write(zcsv.read()) | |
| print(f"[saved] {path}") | |
| return path | |
| def build_levels_arith(lower,upper,step): | |
| n=int((upper-lower)/step) | |
| if n<1: raise ValueError("N<1") | |
| return [lower+i*step for i in range(n+1)],n | |
| def build_levels_geom_with_N(lower,upper,n): | |
| r=(upper/lower)**(1.0/n) | |
| return [lower*(r**i) for i in range(n+1)] | |
| def price_to_interval_idx(p,levels): | |
| if p<levels[0]: return -1 | |
| if p>=levels[-1]: return len(levels)-1 | |
| lo,hi=0,len(levels)-2 | |
| while lo<=hi: | |
| m=(lo+hi)//2 | |
| if levels[m]<=p<levels[m+1]: return m | |
| if p>=levels[m+1]: lo=m+1 | |
| else: hi=m-1 | |
| return -1 | |
| def try_buy_level(i,levels,fee,per_notional,cash_q,pos_b, | |
| holding,qty,cost,buy_time,buy_price,event_ms): | |
| if holding[i]: return False,cash_q,pos_b | |
| p=levels[i]; denom=p*(1+fee) | |
| if denom<=0: return False,cash_q,pos_b | |
| q=per_notional/denom; pay=per_notional | |
| if pay<=cash_q and q>0: | |
| cash_q-=pay; pos_b+=q | |
| holding[i]=True; qty[i]=q; cost[i]=pay | |
| buy_time[i]=event_ms; buy_price[i]=p | |
| return True,cash_q,pos_b | |
| return False,cash_q,pos_b | |
| def try_sell_level(i,levels,fee,per_notional,cash_q,pos_b, | |
| holding,qty,cost,buy_time,buy_price, | |
| event_ms,pair_writer): | |
| if not holding[i] or qty[i]<=0: return False,0.0,cash_q,pos_b | |
| p_sell=levels[i+1]; q=qty[i] | |
| sell_net=q*p_sell*(1-fee); profit=sell_net-cost[i] | |
| cash_q+=sell_net; pos_b-=q | |
| pair_writer.writerow({ | |
| "level":i,"buy_time":iso_utc(buy_time[i]), | |
| "buy_price":f"{buy_price[i]:.8f}","sell_time":iso_utc(event_ms), | |
| "sell_price":f"{p_sell:.8f}","qty_base":f"{q:.12f}", | |
| "buy_amount_quote":f"{cost[i]:.8f}","sell_amount_quote":f"{sell_net:.8f}", | |
| "profit_quote":f"{profit:.8f}" | |
| }) | |
| holding[i]=False; qty[i]=0; cost[i]=0; buy_time[i]=0; buy_price[i]=0 | |
| return True,profit,cash_q,pos_b | |
| def cross_segment(prev_idx,new_idx,levels, | |
| fee,per_notional, | |
| cash_q,pos_b, | |
| holding,qty,cost,buy_time,buy_price, | |
| event_ms,pair_writer): | |
| realized=0.0; pairs=0; i=prev_idx | |
| if new_idx>prev_idx: | |
| while i<new_idx: | |
| if i==-1: i+=1; continue | |
| ok,prof,cash_q,pos_b=try_sell_level(i,levels,fee,per_notional, | |
| cash_q,pos_b,holding,qty,cost,buy_time,buy_price,event_ms,pair_writer) | |
| if ok: realized+=prof; pairs+=1 | |
| i+=1 | |
| elif new_idx<prev_idx: | |
| while i>new_idx: | |
| if i==len(levels)-1: i-=1; continue | |
| _ok,cash_q,pos_b=try_buy_level(i,levels,fee,per_notional, | |
| cash_q,pos_b,holding,qty,cost,buy_time,buy_price,event_ms) | |
| i-=1 | |
| return new_idx,cash_q,pos_b,realized,pairs | |
| def write_hour(writer,hour_idx,price_c, | |
| inside_cnt,below_cnt,above_cnt, | |
| hour_pairs,total_pairs, | |
| hour_grid_profit,realized_grid_profit, | |
| cash_q,pos_b,invest_q): | |
| denom=max(1,inside_cnt+below_cnt+above_cnt) | |
| equity=cash_q+pos_b*price_c | |
| floating_pnl=equity-invest_q | |
| writer.add_scalar("market/price",price_c,hour_idx) | |
| writer.add_scalar("pairs/hour_pairs",hour_pairs,hour_idx) | |
| writer.add_scalar("pairs/cum_pairs",total_pairs,hour_idx) | |
| writer.add_scalar("grid/grid_profit_hour",hour_grid_profit,hour_idx) | |
| writer.add_scalar("grid/grid_profit_cum",realized_grid_profit,hour_idx) | |
| writer.add_scalar("pnl/floating_pnl",floating_pnl,hour_idx) | |
| writer.add_scalar("pnl/total_pnl",floating_pnl,hour_idx) | |
| writer.flush() | |
| def run_one_config(levels,n_grid,scheme_name,logdir,start_date,end_date): | |
| os.makedirs(logdir,exist_ok=True) | |
| writer=SummaryWriter(logdir,max_queue=1,flush_secs=5) | |
| writer.add_hparams({"grid_lower":GRID_LOWER,"grid_upper":GRID_UPPER, | |
| "grid_num":n_grid,"scheme":scheme_name,"fee":FEE_RATE,"invest":INVEST_Q, | |
| "start":start_date,"end":end_date},{}) | |
| progress_path=os.path.join(logdir,"progress.log") | |
| flog=open(progress_path,"a") | |
| pairs_csv=os.path.join(logdir,"pairs_log.csv") | |
| f=open(pairs_csv,"w",newline="") | |
| pw=csv.DictWriter(f,fieldnames=["level","buy_time","buy_price","sell_time","sell_price", | |
| "qty_base","buy_amount_quote","sell_amount_quote","profit_quote"]) | |
| pw.writeheader() | |
| per_notional=INVEST_Q/n_grid; cash_q=INVEST_Q; pos_b=0.0 | |
| m=len(levels)-1 | |
| holding=[False]*m; qty=[0.0]*m; cost=[0.0]*m | |
| buy_time=[0]*m; buy_price=[0.0]*m | |
| realized_grid_profit=0.0; total_pairs=0 | |
| hour_pairs=0; hour_grid_profit=0.0; hour_idx=0 | |
| inside_cnt=below_cnt=above_cnt=0 | |
| cur_hour_bucket=None; last_price=None | |
| seeded=False; prev_ref=None; prev_idx=None | |
| for day in daterange_utc(start_date,end_date): | |
| path=ensure_day_csv(day) | |
| with open(path,"r") as fr: | |
| r=csv.reader(fr) | |
| for row in r: | |
| ot_ms=ms13(row[OPEN_TIME]); ct_ms=ms13(row[CLOSE_TIME]) | |
| o=float(row[OPEN]); h=float(row[HIGH]); l=float(row[LOW]); c=float(row[CLOSE]) | |
| if not seeded: | |
| p0=c; idx0=price_to_interval_idx(p0,levels) | |
| if idx0>=0: | |
| for i in range(0,idx0+1): | |
| ok,cash_q,pos_b=try_buy_level(i,levels,FEE_RATE,per_notional, | |
| cash_q,pos_b,holding,qty,cost,buy_time,buy_price,ot_ms) | |
| if not ok: break | |
| seeded=True | |
| hb=ot_ms//HOUR_MS | |
| if cur_hour_bucket is None: cur_hour_bucket=hb; hour_idx=1 | |
| elif hb!=cur_hour_bucket: | |
| write_hour(writer,hour_idx,last_price or c, | |
| inside_cnt,below_cnt,above_cnt, | |
| hour_pairs,total_pairs,hour_grid_profit,realized_grid_profit, | |
| cash_q,pos_b,INVEST_Q) | |
| hour_idx+=1; cur_hour_bucket=hb | |
| hour_pairs=hour_grid_profit=0.0 | |
| inside_cnt=below_cnt=above_cnt=0 | |
| last_price=c | |
| if c<=levels[0]: below_cnt+=1 | |
| elif c>=levels[-1]: above_cnt+=1 | |
| else: inside_cnt+=1 | |
| if prev_ref is None: | |
| prev_ref=o; prev_idx=price_to_interval_idx(o,levels) | |
| path_seq=([h,l,c] if c>=o else [l,h,c]) | |
| cur=o; cur_idx=price_to_interval_idx(cur,levels) | |
| for p in path_seq: | |
| new_idx=price_to_interval_idx(p,levels) | |
| cur_idx,cash_q,pos_b,realized,pairs=cross_segment(cur_idx,new_idx,levels, | |
| FEE_RATE,per_notional,cash_q,pos_b, | |
| holding,qty,cost,buy_time,buy_price,ot_ms,pw) | |
| realized_grid_profit+=realized | |
| hour_grid_profit+=realized | |
| total_pairs+=pairs | |
| hour_pairs+=pairs | |
| cur=p | |
| prev_ref=c; prev_idx=cur_idx | |
| # 每天结束打印进度 | |
| equity=cash_q+pos_b*(last_price or c) | |
| line=f"[{scheme_name}] finished {day} cash={cash_q:.2f} equity={equity:.2f} pairs={total_pairs} realized={realized_grid_profit:.2f}" | |
| print(line); flog.write(line+"\n"); flog.flush() | |
| # 收尾 | |
| if cur_hour_bucket is not None and last_price is not None: | |
| write_hour(writer,hour_idx,last_price,inside_cnt,below_cnt,above_cnt, | |
| hour_pairs,total_pairs,hour_grid_profit,realized_grid_profit, | |
| cash_q,pos_b,INVEST_Q) | |
| f.close(); flog.close(); writer.close() | |
| def main(): | |
| abs_root=os.path.abspath(LOGROOT) | |
| for step in ARITH_STEPS: | |
| levels_arith,n=build_levels_arith(GRID_LOWER,GRID_UPPER,step) | |
| run_one_config(levels_arith,n,f"arith_step{step}",f"{LOGROOT}/arith_step{step}",START_DATE,END_DATE) | |
| levels_geom=build_levels_geom_with_N(GRID_LOWER,GRID_UPPER,n) | |
| run_one_config(levels_geom,n,f"geom_step{step}",f"{LOGROOT}/geom_N{n}_from_step{step}",START_DATE,END_DATE) | |
| print("TensorBoard logdir:",os.path.abspath(LOGROOT)) | |
| if __name__=="__main__": | |
| main() | |
| ''' | |
| 以下是针对你当前网格交易回测工程的 **TensorBoard 数据查看与目录命名说明文档**。 | |
| --- | |
| ## 📘 使用说明文档 | |
| ### 一、TensorBoard 数据查看方式 | |
| #### 1. 运行代码并启动 TensorBoard | |
| 在项目根目录下执行: | |
| ```bash | |
| python main.py | |
| tensorboard --logdir runs --port 6006 | |
| ``` | |
| 然后打开浏览器访问: | |
| ``` | |
| http://localhost:6006 | |
| ``` | |
| #### 2. 数据目录结构 | |
| 运行脚本后,会自动生成: | |
| ``` | |
| runs/ | |
| ├── grid_step_both_unique_range_progress/ | |
| │ ├── arith_step5/ | |
| │ ├── arith_step10/ | |
| │ ├── arith_step20/ | |
| │ ├── ... | |
| │ ├── geom_step5/ | |
| │ ├── geom_step10/ | |
| │ ├── ... | |
| ``` | |
| 每个子文件夹对应一个**独立的策略配置**(一种网格划分方案),其内部包含: | |
| ``` | |
| events.out.tfevents... # TensorBoard 原始日志 | |
| pairs_log.csv # 每次网格配对完成的详细成交记录 | |
| progress.log # 每天计算完成后的进度信息(方便断点监控) | |
| ``` | |
| --- | |
| ### 二、目录命名规则 | |
| | 文件夹名 | 含义 | 示例 | | |
| | --------------------- | -------------------------------------- | ----------------------------------- | | |
| | `arith_step5` | 等差网格,步长为 5 美元 | 下限 3500,上限 4500,每 5 美元一个格,共 200 个网格 | | |
| | `geom_step5` | 等比网格,网格数量与等差方案相同(如上例为 19 个区间),指数比率自动计算 | 用于比较等差与等比划分的收益差异 | | |
| 每个配置的资金分配规则一致: | |
| ``` | |
| 每格资金 = 总投资金额 / 网格数量 | |
| ``` | |
| --- | |
| ### 三、TensorBoard 面板说明 | |
| | 指标路径 | 含义 | 单位/说明 | | |
| | ---------------------- | ------------------------------- | ----- | | |
| | `market/price` | 每小时的 ETH 现价(取该小时最后一秒的 Close) | 美元 | | |
| | `pairs/hour_pairs` | 该小时完成的配对次数(一次完整买入+卖出) | 次 | | |
| | `pairs/cum_pairs` | 累计配对次数(从首日首秒起) | 次 | | |
| | `grid/grid_profit_hour` | 该小时内已实现的网格收益 | USD | | |
| | `grid/grid_profit_cum` | 从开始到当前累计的已实现网格收益 | USD | | |
| | `pnl/floating_pnl` | 含持仓浮盈亏(当前资产市值-初始投资) | USD | | |
| | `pnl/total_pnl` | 总体盈亏(与 floating_pnl 相同,这里即净值变化) | USD | | |
| --- | |
| ### 四、辅助文件说明 | |
| | 文件 | 说明 | | |
| | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | |
| | `pairs_log.csv` | 每笔配对的成交记录,包括买入价/时间、卖出价/时间、数量、收益等。<br>格式:<br>`level,buy_time,buy_price,sell_time,sell_price,qty_base,buy_amount_quote,sell_amount_quote,profit_quote` | | |
| | `progress.log` | 每天结束后写入一行运行状态:<br>`[arith_step10] finished 2025-10-15 cash=9876.12 equity=10123.45 pairs=182 realized=123.45` | | |
| | `events.out.tfevents...` | TensorBoard 用于绘图的原始日志文件,可直接用 TensorBoard 打开。 | | |
| --- | |
| ### 五、查看多个配置对比 | |
| 在 TensorBoard 左侧勾选多个文件夹(如 `arith_step10` 与 `geom_step10`),即可在同一面板中对比: | |
| * 不同网格数量或步长下的收益曲线; | |
| * 等差与等比配置的配对效率; | |
| * 不同网格规模下的浮动净值变化趋势。 | |
| 常见对比指标: | |
| * `pairs/cum_pairs` → 网格活跃度; | |
| * `grid/grid_profit_cum` → 已实现的网格收益; | |
| * `pnl/floating_pnl` → 含未平仓盈亏的总表现。 | |
| --- | |
| ### 六、调试与恢复 | |
| * 若回测中断,可通过 `progress.log` 查看已完成的最后日期; | |
| * 已下载的 CSV 文件保存在 `data/` 文件夹下; | |
| * 程序会自动跳过已存在的文件,不会重复下载。 | |
| --- | |
| ### 七、建议的分析流程 | |
| 1. 确认 `START_DATE` 与 `END_DATE`,执行脚本; | |
| 2. 打开 TensorBoard,选择 `grid/grid_profit_cum` 与 `pnl/total_pnl`; | |
| 3. 观察等差、等比不同步长下的累计收益曲线; | |
| 4. 打开 `pairs_log.csv` 查看成交分布与区间活跃度; | |
| 5. 结合 `progress.log` 监控每日资金流与运行完成进度。 | |
| ''' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment