188 lines
7.7 KiB
Python
188 lines
7.7 KiB
Python
|
|
"""
|
|||
|
|
EMA趋势策略 - 精简参数优化 v2
|
|||
|
|
|
|||
|
|
已知:EMA(8/21/120) ATR>0.03% SL=0.4% MaxH=1800 → 方向PnL +327, 净亏 -101
|
|||
|
|
优化目标:找到净盈利 > 0 的参数组合
|
|||
|
|
|
|||
|
|
策略核心:减少交易次数(更长EMA/更高ATR门槛),提高每笔质量
|
|||
|
|
"""
|
|||
|
|
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, price):
|
|||
|
|
if self.v is None:
|
|||
|
|
self.v = price
|
|||
|
|
else:
|
|||
|
|
self.v = price * 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()
|
|||
|
|
# pre-convert to list of tuples for speed
|
|||
|
|
out = []
|
|||
|
|
for r in rows:
|
|||
|
|
out.append((datetime.datetime.fromtimestamp(r[0]/1000.0), r[1], r[2], r[3], r[4]))
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
hs_buf = []; ls_buf = []; cs_buf = []
|
|||
|
|
pf_ = None; ps_ = None
|
|||
|
|
tc=0; wc=0; dpnl=0.0; tfee=0.0; treb=0.0
|
|||
|
|
hsl = sl_pct * 1.5
|
|||
|
|
LEV=50; RP=0.005; TF=0.0006; RR=0.90; MH=200; AP=14
|
|||
|
|
|
|||
|
|
for dt, o_, h_, l_, c_ in data:
|
|||
|
|
p = c_
|
|||
|
|
hs_buf.append(h_); ls_buf.append(l_); cs_buf.append(p)
|
|||
|
|
fast = ef.update(p); slow = es.update(p); big = eb.update(p)
|
|||
|
|
|
|||
|
|
atr_pct = 0.0
|
|||
|
|
if len(hs_buf) > AP + 1:
|
|||
|
|
s = 0.0
|
|||
|
|
for i in range(-AP, 0):
|
|||
|
|
tr = hs_buf[i] - ls_buf[i]
|
|||
|
|
d1 = abs(hs_buf[i] - cs_buf[i-1])
|
|||
|
|
d2 = abs(ls_buf[i] - cs_buf[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_ = ps * pp; cv = ps*(1+pp); cf = cv*TF; of_=ps*TF; tt=of_+cf; rb=tt*RR
|
|||
|
|
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
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if hsec >= MH:
|
|||
|
|
do_c = False
|
|||
|
|
if -pp >= sl_pct: do_c = True
|
|||
|
|
elif hsec >= mh: do_c = True
|
|||
|
|
elif pos == 1 and cd: do_c = True
|
|||
|
|
elif pos == -1 and cu: do_c = True
|
|||
|
|
elif pend == 'cl' and pos == 1: do_c = True
|
|||
|
|
elif pend == 'cs' and pos == -1: do_c = True
|
|||
|
|
|
|||
|
|
if do_c:
|
|||
|
|
pnl_ = ps * pp; cv = ps*(1+pp); cf = cv*TF; of_=ps*TF; tt=of_+cf; rb=tt*RR
|
|||
|
|
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
|
|||
|
|
if atr_pct >= atr_min:
|
|||
|
|
if (cd or fast < slow) and p < big:
|
|||
|
|
ns = bal * RP * LEV
|
|||
|
|
if ns >= 1: bal -= ns*TF; pos=-1; op=p; ot=dt; ps=ns
|
|||
|
|
elif (cu or fast > slow) and p > big:
|
|||
|
|
ns = bal * RP * LEV
|
|||
|
|
if ns >= 1: bal -= ns*TF; pos=1; op=p; ot=dt; ps=ns
|
|||
|
|
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:
|
|||
|
|
ns = bal * RP * LEV
|
|||
|
|
if ns >= 1: bal -= ns*TF; pos=1; op=p; ot=dt; ps=ns
|
|||
|
|
elif cd and p < big:
|
|||
|
|
ns = bal * RP * LEV
|
|||
|
|
if ns >= 1: bal -= ns*TF; pos=-1; op=p; ot=dt; ps=ns
|
|||
|
|
|
|||
|
|
if pos != 0:
|
|||
|
|
p = data[-1][4]
|
|||
|
|
pp = (p - op) / op if pos == 1 else (op - p) / op
|
|||
|
|
pnl_ = ps * pp; cv = ps*(1+pp); cf = cv*TF; of_=ps*TF; tt=of_+cf; rb=tt*RR
|
|||
|
|
bal += pnl_ - cf + rb; dpnl += pnl_; tfee += tt; treb += rb
|
|||
|
|
tc += 1; wc += (1 if pnl_ > 0 else 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 loaded\n", flush=True)
|
|||
|
|
|
|||
|
|
# 精简参数网格 - 只测最有潜力的范围
|
|||
|
|
combos = []
|
|||
|
|
for fp in [8, 13, 20, 30]:
|
|||
|
|
for sp in [21, 34, 55, 80]:
|
|||
|
|
if fp >= sp: continue
|
|||
|
|
for bp in [120, 200]:
|
|||
|
|
for am in [0.0003, 0.0006, 0.001, 0.0015, 0.002]:
|
|||
|
|
for sl in [0.004, 0.006, 0.008, 0.01]:
|
|||
|
|
for mh in [1200, 1800, 3600]:
|
|||
|
|
combos.append((fp, sp, bp, am, sl, mh))
|
|||
|
|
|
|||
|
|
print(f"Combos: {len(combos)}", 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) % 30 == 0:
|
|||
|
|
el = time.time() - t0
|
|||
|
|
eta = el/(idx+1)*(len(combos)-idx-1)
|
|||
|
|
print(f" [{idx+1}/{len(combos)}] {el:.0f}s done, ~{eta:.0f}s left", flush=True)
|
|||
|
|
|
|||
|
|
tt = time.time() - t0
|
|||
|
|
print(f"\nAll done! {len(results)} combos in {tt:.1f}s\n", flush=True)
|
|||
|
|
|
|||
|
|
results.sort(key=lambda x: x[0], reverse=True)
|
|||
|
|
profitable = [r for r in results if r[0] > 0]
|
|||
|
|
print(f"Profitable: {len(profitable)} / {len(results)} ({len(profitable)/len(results)*100:.1f}%)\n", flush=True)
|
|||
|
|
|
|||
|
|
print(f"{'='*130}", flush=True)
|
|||
|
|
print(f" TOP 30", flush=True)
|
|||
|
|
print(f"{'='*130}", 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" {'-'*120}", 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{'='*130}", flush=True)
|
|||
|
|
print(f" ALL PROFITABLE COMBOS", flush=True)
|
|||
|
|
print(f"{'='*130}", 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}% dirPnL={dp:+.2f} rebate={reb:.2f} netFee={nf:.2f}", flush=True)
|
|||
|
|
|
|||
|
|
# Save
|
|||
|
|
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()
|