185 lines
7.7 KiB
Python
185 lines
7.7 KiB
Python
"""
|
||
EMA趋势策略 - 精准优化(~50组参数,约2分钟)
|
||
|
||
已知基线:EMA(8/21/120) ATR>0.03% SL=0.4% MH=1800s
|
||
→ 14066 trades, dirPnL +327, netFee 428, net -101
|
||
|
||
核心优化思路:
|
||
1. 提高 ATR 门槛 → 减少交易次数,只在波动大时交易
|
||
2. 加长 EMA 周期 → 减少交叉频率
|
||
3. 放宽止损 → 让趋势有更多空间发展
|
||
4. 延长最大持仓 → 捕获更大行情
|
||
"""
|
||
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 bt(data, fp, sp, bp, atr_min, sl_pct, mh):
|
||
bal=1000.0; pos=0; op=0.0; ot=None; ps=0.0; pend=None
|
||
ef=EMA(fp); es=EMA(sp); eb=EMA(bp)
|
||
H=[]; L=[]; C=[]
|
||
pf_=None; ps_=None
|
||
tc=0; wc=0; dpnl=0.0; tfee=0.0; treb=0.0
|
||
hsl=sl_pct*1.5; AP=14
|
||
|
||
def _close(price, dt_):
|
||
nonlocal bal,pos,op,ot,ps,pend,tc,wc,dpnl,tfee,treb
|
||
pp = (price-op)/op if pos==1 else (op-price)/op
|
||
pnl_=ps*pp; cv=ps*(1+pp); cf=cv*0.0006; of_=ps*0.0006; tt=of_+cf; rb=tt*0.9
|
||
bal+=pnl_-cf+rb; dpnl+=pnl_; tfee+=tt; treb+=rb
|
||
tc+=1; wc+=(1 if pnl_>0 else 0)
|
||
pos=0; op=0; ot=None; ps=0; pend=None
|
||
|
||
def _open(d, price, dt_):
|
||
nonlocal bal,pos,op,ot,ps
|
||
ns=bal*0.005*50
|
||
if ns<1: return
|
||
bal-=ns*0.0006
|
||
pos=1 if d=='L' else -1; op=price; ot=dt_; ps=ns
|
||
|
||
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:
|
||
pp=(p-op)/op if pos==1 else (op-p)/op
|
||
hsec=(dt-ot).total_seconds()
|
||
if -pp>=hsl: _close(p,dt); continue
|
||
if hsec>=200:
|
||
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:
|
||
_close(p,dt)
|
||
if atr_pct>=atr_min:
|
||
if (cd or fast<slow) and p<big: _open('S',p,dt)
|
||
elif (cu or fast>slow) and p>big: _open('L',p,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: _open('L',p,dt)
|
||
elif cd and p<big: _open('S',p,dt)
|
||
|
||
if pos!=0: _close(data[-1][4], data[-1][0])
|
||
net=bal-1000.0; wr=wc/tc*100 if tc>0 else 0
|
||
return net,tc,wr,dpnl,treb,tfee-treb
|
||
|
||
def main():
|
||
print("Loading...", flush=True)
|
||
data = load()
|
||
print(f"{len(data)} bars\n", flush=True)
|
||
|
||
# ~50 targeted combos
|
||
combos = []
|
||
# Group 1: baseline variations (tweak one param at a time)
|
||
base = (8, 21, 120, 0.0003, 0.004, 1800)
|
||
# Vary ATR threshold (most impactful for reducing trades)
|
||
for am in [0.0003, 0.0005, 0.0008, 0.001, 0.0013, 0.0015, 0.002, 0.003]:
|
||
combos.append((8, 21, 120, am, 0.004, 1800))
|
||
combos.append((8, 21, 120, am, 0.006, 1800))
|
||
combos.append((8, 21, 120, am, 0.008, 1800))
|
||
# Vary EMA periods
|
||
for fp, sp in [(13, 34), (13, 55), (20, 55), (20, 80), (30, 80)]:
|
||
for am in [0.0005, 0.001, 0.0015, 0.002]:
|
||
combos.append((fp, sp, 120, am, 0.005, 1800))
|
||
combos.append((fp, sp, 200, am, 0.005, 1800))
|
||
combos.append((fp, sp, 120, am, 0.008, 3600))
|
||
combos.append((fp, sp, 200, am, 0.008, 3600))
|
||
# Vary max hold
|
||
for mh in [900, 1800, 3600, 5400, 7200]:
|
||
combos.append((8, 21, 120, 0.001, 0.005, mh))
|
||
combos.append((13, 34, 120, 0.001, 0.005, mh))
|
||
combos.append((13, 34, 200, 0.001, 0.008, mh))
|
||
# Remove duplicates
|
||
combos = list(set(combos))
|
||
combos.sort()
|
||
|
||
print(f"Combos: {len(combos)}\n", flush=True)
|
||
results = []
|
||
t0 = time.time()
|
||
|
||
for idx, (fp, sp, bp, am, sl, mh) in enumerate(combos):
|
||
net, tc, wr, dp, reb, nf = bt(data, fp, sp, bp, am, sl, mh)
|
||
results.append((net, tc, wr, dp, reb, nf, fp, sp, bp, am, sl, mh))
|
||
if (idx+1) % 10 == 0:
|
||
el=time.time()-t0; eta=el/(idx+1)*(len(combos)-idx-1)
|
||
print(f" [{idx+1}/{len(combos)}] {el:.0f}s / ~{eta:.0f}s left", flush=True)
|
||
|
||
tt = time.time() - t0
|
||
results.sort(key=lambda x: x[0], reverse=True)
|
||
profitable = [r for r in results if r[0] > 0]
|
||
|
||
print(f"\nDone! {tt:.1f}s | Profitable: {len(profitable)}/{len(results)}\n", flush=True)
|
||
|
||
print("="*125, flush=True)
|
||
print(" TOP 30 RESULTS (sorted by Net P&L)", flush=True)
|
||
print("="*125, flush=True)
|
||
print(f" {'#':>3} {'F':>3} {'S':>3} {'B':>4} {'ATR':>6} {'SL':>5} {'MH':>5} | {'Net%':>7} {'Net$':>9} {'#Trd':>6} {'WR':>6} {'DirPnL':>9} {'Rebate':>9} {'NetFee':>8}", flush=True)
|
||
print(f" {'-'*119}", flush=True)
|
||
for i,(net,tc,wr,dp,reb,nf,fp,sp,bp,am,sl,mh) in enumerate(results[:30]):
|
||
mk=" <<<" if net>0 else ""
|
||
print(f" {i+1:>3} {fp:>3} {sp:>3} {bp:>4} {am*100:>5.2f}% {sl*100:>4.1f}% {mh:>5} | {net/10:>+6.2f}% {net:>+8.2f} {tc:>6} {wr:>5.1f}% {dp:>+8.2f} {reb:>8.2f} {nf:>8.2f}{mk}", flush=True)
|
||
|
||
if profitable:
|
||
print(f"\n{'='*125}", flush=True)
|
||
print(f" ALL {len(profitable)} PROFITABLE COMBOS", flush=True)
|
||
print(f"{'='*125}", flush=True)
|
||
for i,(net,tc,wr,dp,reb,nf,fp,sp,bp,am,sl,mh) in enumerate(profitable):
|
||
print(f" {i+1:>3} EMA({fp}/{sp}/{bp}) ATR>{am*100:.2f}% SL={sl*100:.1f}% MH={mh}s | net={net:+.2f}$ ({net/10:+.2f}%) trades={tc} WR={wr:.1f}% dir={dp:+.2f} reb={reb:.2f} fee={nf:.2f}", flush=True)
|
||
else:
|
||
print("\n No profitable combos found. The EMA trend strategy may need a fundamentally different approach.", flush=True)
|
||
# Show the closest to profitable
|
||
print(f"\n Closest to breakeven:", flush=True)
|
||
for i,(net,tc,wr,dp,reb,nf,fp,sp,bp,am,sl,mh) in enumerate(results[:5]):
|
||
gap = -net
|
||
print(f" EMA({fp}/{sp}/{bp}) ATR>{am*100:.2f}% SL={sl*100:.1f}% MH={mh}s | net={net:+.2f}$ gap_to_profit={gap:.2f}$ trades={tc} dir={dp:+.2f}", flush=True)
|
||
|
||
print("="*125, flush=True)
|
||
|
||
csv = Path(__file__).parent.parent / 'param_results.csv'
|
||
with open(csv, 'w', encoding='utf-8-sig') as f:
|
||
f.write("fast,slow,big,atr_min,stop_loss,max_hold,net_pct,net_usd,trades,win_rate,dir_pnl,rebate,net_fee\n")
|
||
for net,tc,wr,dp,reb,nf,fp,sp,bp,am,sl,mh in results:
|
||
f.write(f"{fp},{sp},{bp},{am},{sl},{mh},{net/10:.4f},{net:.4f},{tc},{wr:.2f},{dp:.4f},{reb:.4f},{nf:.4f}\n")
|
||
print(f"\nSaved: {csv}", flush=True)
|
||
|
||
if __name__=='__main__':
|
||
main()
|