364 lines
15 KiB
Python
364 lines
15 KiB
Python
"""
|
||
多策略组合回测 — 目标: 1000 USDT/月
|
||
|
||
思路: 同时运行多个不同参数的 EMA 策略,它们在不同时间段产生信号,
|
||
彼此信号不重叠时各自独立开仓,等于"多个策略并行跑"。
|
||
|
||
条件:
|
||
- 每笔: 100 USDT 保证金, 100x 杠杆, 名义 10,000 USDT
|
||
- 90% 返佣
|
||
- 最低持仓 > 3 分钟
|
||
- ETH 合约, 2025 全年
|
||
|
||
测试方案:
|
||
A) 单策略加大仓位 (500U/1000U)
|
||
B) 多策略组合 (3-5个不同参数策略并行)
|
||
C) 降低 ATR 门槛 + 更宽止损
|
||
D) 综合最优方案
|
||
"""
|
||
import sys, time, datetime, sqlite3
|
||
from pathlib import Path
|
||
|
||
class EMA:
|
||
__slots__ = ('k', 'v')
|
||
def __init__(self, p):
|
||
self.k = 2.0 / (p + 1); self.v = None
|
||
def update(self, x):
|
||
self.v = x if self.v is None else x * self.k + self.v * (1 - self.k)
|
||
return self.v
|
||
|
||
def load():
|
||
db = Path(__file__).parent.parent / 'models' / 'database.db'
|
||
s = int(datetime.datetime(2025,1,1).timestamp()) * 1000
|
||
e = int(datetime.datetime(2026,1,1).timestamp()) * 1000
|
||
conn = sqlite3.connect(str(db))
|
||
rows = conn.cursor().execute(
|
||
"SELECT id,open,high,low,close FROM bitmart_eth_1m WHERE id>=? AND id<? ORDER BY id", (s,e)
|
||
).fetchall()
|
||
conn.close()
|
||
return [(datetime.datetime.fromtimestamp(r[0]/1000.0), r[1], r[2], r[3], r[4]) for r in rows]
|
||
|
||
|
||
def run_strategy(data, fp, sp, bp, atr_min, sl_pct, mh, notional=10000.0):
|
||
"""单策略回测,返回交易列表"""
|
||
ef=EMA(fp); es=EMA(sp); eb=EMA(bp)
|
||
H=[]; L=[]; C=[]
|
||
pf_=None; ps_=None
|
||
pos=0; op=0.0; ot=None; pend=None
|
||
hsl=sl_pct*1.5; AP=14
|
||
FEE_RATE=0.0006; REB_RATE=0.90; MIN_H=200
|
||
trades=[]
|
||
|
||
for dt,o_,h_,l_,c_ in data:
|
||
p=c_; H.append(h_); L.append(l_); C.append(p)
|
||
fast=ef.update(p); slow=es.update(p); big=eb.update(p)
|
||
atr_pct=0.0
|
||
if len(H)>AP+1:
|
||
s=0.0
|
||
for i in range(-AP,0):
|
||
tr=H[i]-L[i]; d1=abs(H[i]-C[i-1]); d2=abs(L[i]-C[i-1])
|
||
if d1>tr: tr=d1
|
||
if d2>tr: tr=d2
|
||
s+=tr
|
||
atr_pct=s/(AP*p) if p>0 else 0
|
||
cu=pf_ is not None and pf_<=ps_ and fast>slow
|
||
cd=pf_ is not None and pf_>=ps_ and fast<slow
|
||
pf_=fast; ps_=slow
|
||
|
||
if pos!=0 and ot is not None:
|
||
pp=(p-op)/op if pos==1 else (op-p)/op
|
||
hsec=(dt-ot).total_seconds()
|
||
if -pp>=hsl:
|
||
pnl=notional*pp; fee=notional*FEE_RATE*2; reb=fee*REB_RATE
|
||
trades.append((pos, op, p, pnl, fee, reb, hsec, ot, dt))
|
||
pos=0; op=0; ot=None; pend=None; continue
|
||
if hsec>=MIN_H:
|
||
dc=False
|
||
if -pp>=sl_pct: dc=True
|
||
elif hsec>=mh: dc=True
|
||
elif pos==1 and cd: dc=True
|
||
elif pos==-1 and cu: dc=True
|
||
elif pend=='cl' and pos==1: dc=True
|
||
elif pend=='cs' and pos==-1: dc=True
|
||
if dc:
|
||
pnl=notional*pp; fee=notional*FEE_RATE*2; reb=fee*REB_RATE
|
||
trades.append((pos, op, p, pnl, fee, reb, hsec, ot, dt))
|
||
pos=0; op=0; ot=None; pend=None
|
||
if atr_pct>=atr_min:
|
||
if (cd or fast<slow) and p<big: pos=-1; op=p; ot=dt
|
||
elif (cu or fast>slow) and p>big: pos=1; op=p; ot=dt
|
||
continue
|
||
else:
|
||
if pos==1 and cd: pend='cl'
|
||
elif pos==-1 and cu: pend='cs'
|
||
|
||
if pos==0 and atr_pct>=atr_min:
|
||
if cu and p>big: pos=1; op=p; ot=dt
|
||
elif cd and p<big: pos=-1; op=p; ot=dt
|
||
|
||
if pos!=0:
|
||
p=data[-1][4]; dt=data[-1][0]
|
||
pp=(p-op)/op if pos==1 else (op-p)/op
|
||
pnl=notional*pp; fee=notional*FEE_RATE*2; reb=fee*REB_RATE
|
||
trades.append((pos, op, p, pnl, fee, reb, (dt-ot).total_seconds(), ot, dt))
|
||
return trades
|
||
|
||
|
||
def analyze(trades, label, notional=10000.0):
|
||
"""分析交易结果,返回摘要字典"""
|
||
if not trades:
|
||
return {'label': label, 'n': 0, 'net': 0, 'pnl': 0, 'reb': 0, 'fee': 0, 'dd': 0, 'monthly': {}}
|
||
n = len(trades)
|
||
total_pnl = sum(t[3] for t in trades)
|
||
total_fee = sum(t[4] for t in trades)
|
||
total_reb = sum(t[5] for t in trades)
|
||
net = total_pnl - (total_fee - total_reb)
|
||
|
||
# 最大回撤
|
||
cum=0; peak=0; dd=0
|
||
for t in trades:
|
||
cum += t[3] - (t[4] - t[5])
|
||
if cum > peak: peak = cum
|
||
if peak - cum > dd: dd = peak - cum
|
||
|
||
# 月度
|
||
monthly = {}
|
||
for t in trades:
|
||
k = t[8].strftime('%Y-%m')
|
||
if k not in monthly: monthly[k] = {'n': 0, 'net': 0}
|
||
monthly[k]['n'] += 1
|
||
monthly[k]['net'] += t[3] - (t[4] - t[5])
|
||
|
||
wr = len([t for t in trades if t[3]>0]) / n * 100
|
||
return {'label': label, 'n': n, 'net': net, 'pnl': total_pnl, 'reb': total_reb,
|
||
'fee': total_fee, 'dd': dd, 'wr': wr, 'monthly': monthly}
|
||
|
||
|
||
def merge_trades(all_trade_lists):
|
||
"""合并多个策略的交易(检查时间冲突:同一时间只能有一个持仓)"""
|
||
# 简单合并:按开仓时间排序,跳过与已有持仓重叠的交易
|
||
all_trades = []
|
||
for trades in all_trade_lists:
|
||
for t in trades:
|
||
all_trades.append(t)
|
||
all_trades.sort(key=lambda x: x[7]) # 按开仓时间排序
|
||
|
||
merged = []
|
||
last_close = None
|
||
for t in all_trades:
|
||
open_time = t[7]
|
||
close_time = t[8]
|
||
if last_close is None or open_time >= last_close:
|
||
merged.append(t)
|
||
last_close = close_time
|
||
return merged
|
||
|
||
|
||
def print_comparison(results):
|
||
"""打印对比表"""
|
||
print(f"\n{'='*110}", flush=True)
|
||
print(f" COMPARISON: Target = 1000 USDT/month = 12,000 USDT/year", flush=True)
|
||
print(f"{'='*110}", flush=True)
|
||
print(f" {'方案':<40} {'交易数':>6} {'年净利':>10} {'月均':>8} {'胜率':>6} {'返佣':>10} {'最大回撤':>10} {'达标':>4}", flush=True)
|
||
print(f" {'-'*104}", flush=True)
|
||
|
||
for r in results:
|
||
monthly_avg = r['net'] / 12
|
||
ok = "Yes" if monthly_avg >= 1000 else "No"
|
||
print(f" {r['label']:<40} {r['n']:>6} {r['net']:>+10.0f} {monthly_avg:>+8.0f} {r.get('wr',0):>5.1f}% {r['reb']:>10.0f} {r['dd']:>10.0f} {ok:>4}", flush=True)
|
||
|
||
print(f"{'='*110}", flush=True)
|
||
|
||
# 打印最佳方案的月度明细
|
||
best = max(results, key=lambda x: x['net'])
|
||
print(f"\n 最佳方案: {best['label']}", flush=True)
|
||
print(f" 年净利: {best['net']:+.0f} USDT | 月均: {best['net']/12:+.0f} USDT\n", flush=True)
|
||
|
||
if best['monthly']:
|
||
print(f" {'月份':<8} {'笔数':>5} {'净利润':>10}", flush=True)
|
||
print(f" {'-'*28}", flush=True)
|
||
for m in sorted(best['monthly'].keys()):
|
||
d = best['monthly'][m]
|
||
print(f" {m:<8} {d['n']:>5} {d['net']:>+10.0f}", flush=True)
|
||
print(f" {'-'*28}", flush=True)
|
||
print(f" {'合计':<8} {best['n']:>5} {best['net']:>+10.0f}", flush=True)
|
||
|
||
|
||
def main():
|
||
print("Loading data...", flush=True)
|
||
data = load()
|
||
print(f"{len(data)} bars loaded\n", flush=True)
|
||
|
||
NOTIONAL = 10000.0 # 100U * 100x
|
||
results = []
|
||
|
||
# ============================
|
||
# 方案 1: 原始策略(基线)
|
||
# ============================
|
||
t1 = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, NOTIONAL)
|
||
results.append(analyze(t1, "A1: EMA(8/21) ATR>0.3% [基线]", NOTIONAL))
|
||
print(f" A1 done: {len(t1)} trades", flush=True)
|
||
|
||
# ============================
|
||
# 方案 2: 加大仓位 - 500U (5倍)
|
||
# ============================
|
||
t2 = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, 50000.0)
|
||
results.append(analyze(t2, "A2: 基线 x5 (500U保证金)", 50000.0))
|
||
print(f" A2 done: {len(t2)} trades", flush=True)
|
||
|
||
# ============================
|
||
# 方案 3: 加大仓位 - 1000U (10倍)
|
||
# ============================
|
||
t3 = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, 100000.0)
|
||
results.append(analyze(t3, "A3: 基线 x10 (1000U保证金)", 100000.0))
|
||
print(f" A3 done: {len(t3)} trades", flush=True)
|
||
|
||
# ============================
|
||
# 方案 4: 降低ATR到0.15%(更多交易)
|
||
# ============================
|
||
t4 = run_strategy(data, 8, 21, 120, 0.0015, 0.004, 1800, NOTIONAL)
|
||
results.append(analyze(t4, "B1: ATR>0.15% (更频繁)", NOTIONAL))
|
||
print(f" B1 done: {len(t4)} trades", flush=True)
|
||
|
||
# ============================
|
||
# 方案 5: ATR>0.1% + 宽止损0.8%
|
||
# ============================
|
||
t5 = run_strategy(data, 8, 21, 120, 0.001, 0.008, 1800, NOTIONAL)
|
||
results.append(analyze(t5, "B2: ATR>0.1% SL=0.8%", NOTIONAL))
|
||
print(f" B2 done: {len(t5)} trades", flush=True)
|
||
|
||
# ============================
|
||
# 方案 6: ATR>0.2% + SL=0.8% + 更长持仓3600s
|
||
# ============================
|
||
t6 = run_strategy(data, 8, 21, 120, 0.002, 0.008, 3600, NOTIONAL)
|
||
results.append(analyze(t6, "B3: ATR>0.2% SL=0.8% MH=3600", NOTIONAL))
|
||
print(f" B3 done: {len(t6)} trades", flush=True)
|
||
|
||
# ============================
|
||
# 方案 7: 多策略组合(3个不同EMA参数并行,无时间重叠)
|
||
# ============================
|
||
s1 = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, NOTIONAL)
|
||
s2 = run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, NOTIONAL)
|
||
s3 = run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, NOTIONAL)
|
||
merged3 = merge_trades([s1, s2, s3])
|
||
results.append(analyze(merged3, "C1: 3策略组合 (不重叠)", NOTIONAL))
|
||
print(f" C1 done: {len(s1)}+{len(s2)}+{len(s3)} -> {len(merged3)} merged", flush=True)
|
||
|
||
# ============================
|
||
# 方案 8: 5策略组合
|
||
# ============================
|
||
s4 = run_strategy(data, 20, 55, 120, 0.002, 0.006, 1800, NOTIONAL)
|
||
s5 = run_strategy(data, 8, 34, 200, 0.002, 0.005, 1800, NOTIONAL)
|
||
merged5 = merge_trades([s1, s2, s3, s4, s5])
|
||
results.append(analyze(merged5, "C2: 5策略组合 (不重叠)", NOTIONAL))
|
||
print(f" C2 done: -> {len(merged5)} merged", flush=True)
|
||
|
||
# ============================
|
||
# 方案 9: 3策略组合 + 加大仓位到 300U
|
||
# ============================
|
||
s1b = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, 30000.0)
|
||
s2b = run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, 30000.0)
|
||
s3b = run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, 30000.0)
|
||
merged3b = merge_trades([s1b, s2b, s3b])
|
||
results.append(analyze(merged3b, "D1: 3策略+300U仓位", 30000.0))
|
||
print(f" D1 done: -> {len(merged3b)} merged", flush=True)
|
||
|
||
# ============================
|
||
# 方案 10: 3策略组合 + 500U仓位
|
||
# ============================
|
||
s1c = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, 50000.0)
|
||
s2c = run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, 50000.0)
|
||
s3c = run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, 50000.0)
|
||
merged3c = merge_trades([s1c, s2c, s3c])
|
||
results.append(analyze(merged3c, "D2: 3策略+500U仓位", 50000.0))
|
||
print(f" D2 done: -> {len(merged3c)} merged", flush=True)
|
||
|
||
# ============================
|
||
# 方案 11: 多策略并行(允许同时持仓,各策略独立运行)
|
||
# ============================
|
||
# 如果账户允许同时持有多个仓位(不同策略各自独立)
|
||
s1_r = analyze(s1, "sub1")
|
||
s2_r = analyze(run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, NOTIONAL), "sub2")
|
||
s3_r = analyze(run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, NOTIONAL), "sub3")
|
||
s4_r = analyze(run_strategy(data, 20, 55, 120, 0.002, 0.006, 1800, NOTIONAL), "sub4")
|
||
s5_r = analyze(run_strategy(data, 8, 34, 200, 0.002, 0.005, 1800, NOTIONAL), "sub5")
|
||
|
||
# 合并月度(允许重叠 = 各自独立计算再加总)
|
||
parallel_net = s1_r['net'] + s2_r['net'] + s3_r['net'] + s4_r['net'] + s5_r['net']
|
||
parallel_reb = s1_r['reb'] + s2_r['reb'] + s3_r['reb'] + s4_r['reb'] + s5_r['reb']
|
||
parallel_fee = s1_r['fee'] + s2_r['fee'] + s3_r['fee'] + s4_r['fee'] + s5_r['fee']
|
||
parallel_pnl = s1_r['pnl'] + s2_r['pnl'] + s3_r['pnl'] + s4_r['pnl'] + s5_r['pnl']
|
||
parallel_n = s1_r['n'] + s2_r['n'] + s3_r['n'] + s4_r['n'] + s5_r['n']
|
||
parallel_dd = max(s1_r['dd'], s2_r['dd'], s3_r['dd'], s4_r['dd'], s5_r['dd']) * 2 # 保守估计
|
||
|
||
# 合并月度
|
||
all_months = set()
|
||
for sr in [s1_r, s2_r, s3_r, s4_r, s5_r]:
|
||
all_months.update(sr['monthly'].keys())
|
||
parallel_monthly = {}
|
||
for m in all_months:
|
||
n_m = 0; net_m = 0
|
||
for sr in [s1_r, s2_r, s3_r, s4_r, s5_r]:
|
||
if m in sr['monthly']:
|
||
n_m += sr['monthly'][m]['n']
|
||
net_m += sr['monthly'][m]['net']
|
||
parallel_monthly[m] = {'n': n_m, 'net': net_m}
|
||
|
||
results.append({
|
||
'label': "E1: 5策略并行(允许同时持仓) 100U each",
|
||
'n': parallel_n, 'net': parallel_net, 'pnl': parallel_pnl,
|
||
'reb': parallel_reb, 'fee': parallel_fee, 'dd': parallel_dd,
|
||
'wr': 0, 'monthly': parallel_monthly
|
||
})
|
||
print(f" E1 done: {parallel_n} total trades (parallel)", flush=True)
|
||
|
||
# ============================
|
||
# 方案 12: 5策略并行 + 200U仓位
|
||
# ============================
|
||
N2 = 20000.0
|
||
ps1 = analyze(run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, N2), "p1")
|
||
ps2 = analyze(run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, N2), "p2")
|
||
ps3 = analyze(run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, N2), "p3")
|
||
ps4 = analyze(run_strategy(data, 20, 55, 120, 0.002, 0.006, 1800, N2), "p4")
|
||
ps5 = analyze(run_strategy(data, 8, 34, 200, 0.002, 0.005, 1800, N2), "p5")
|
||
|
||
p_net = ps1['net']+ps2['net']+ps3['net']+ps4['net']+ps5['net']
|
||
p_reb = ps1['reb']+ps2['reb']+ps3['reb']+ps4['reb']+ps5['reb']
|
||
p_fee = ps1['fee']+ps2['fee']+ps3['fee']+ps4['fee']+ps5['fee']
|
||
p_pnl = ps1['pnl']+ps2['pnl']+ps3['pnl']+ps4['pnl']+ps5['pnl']
|
||
p_n = ps1['n']+ps2['n']+ps3['n']+ps4['n']+ps5['n']
|
||
p_dd = max(ps1['dd'],ps2['dd'],ps3['dd'],ps4['dd'],ps5['dd'])*2
|
||
|
||
p_monthly = {}
|
||
for m in all_months:
|
||
n_m=0; net_m=0
|
||
for sr in [ps1,ps2,ps3,ps4,ps5]:
|
||
if m in sr['monthly']:
|
||
n_m+=sr['monthly'][m]['n']; net_m+=sr['monthly'][m]['net']
|
||
p_monthly[m] = {'n': n_m, 'net': net_m}
|
||
|
||
results.append({
|
||
'label': "E2: 5策略并行 200U each",
|
||
'n': p_n, 'net': p_net, 'pnl': p_pnl,
|
||
'reb': p_reb, 'fee': p_fee, 'dd': p_dd,
|
||
'wr': 0, 'monthly': p_monthly
|
||
})
|
||
print(f" E2 done: {p_n} total trades (parallel 200U)", flush=True)
|
||
|
||
# ============================
|
||
# 打印对比
|
||
# ============================
|
||
print_comparison(results)
|
||
|
||
# 保存
|
||
csv = Path(__file__).parent.parent / 'multi_strategy_results.csv'
|
||
with open(csv, 'w', encoding='utf-8-sig') as f:
|
||
f.write("方案,交易数,年净利,月均,返佣,最大回撤\n")
|
||
for r in results:
|
||
f.write(f"{r['label']},{r['n']},{r['net']:.0f},{r['net']/12:.0f},{r['reb']:.0f},{r['dd']:.0f}\n")
|
||
print(f"\nSaved: {csv}", flush=True)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|