Skip to content

Instantly share code, notes, and snippets.

@chenyaofo
Created October 16, 2025 00:44
Show Gist options
  • Select an option

  • Save chenyaofo/68da8a15dba8835d8a1a426fbdc16200 to your computer and use it in GitHub Desktop.

Select an option

Save chenyaofo/68da8a15dba8835d8a1a426fbdc16200 to your computer and use it in GitHub Desktop.
Return backtesting for any trading pair on Binance.
# 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