290 lines
9.1 KiB
Python
290 lines
9.1 KiB
Python
"""
|
||
EMA趋势策略 - 快速参数优化(精简版)
|
||
|
||
前次发现:EMA(8/21/120) 方向盈利 +327,但10%费用成本 428,净亏 -101。
|
||
优化方向:用更长EMA减少交易次数 + 更高ATR过滤提高质量。
|
||
"""
|
||
|
||
import sys
|
||
import time
|
||
import datetime
|
||
import sqlite3
|
||
from pathlib import Path
|
||
|
||
|
||
class EMA:
|
||
def __init__(self, period):
|
||
self.k = 2.0 / (period + 1)
|
||
self.value = None
|
||
def update(self, price):
|
||
if self.value is None:
|
||
self.value = price
|
||
else:
|
||
self.value = price * self.k + self.value * (1 - self.k)
|
||
return self.value
|
||
|
||
|
||
def load_data():
|
||
db_path = Path(__file__).parent.parent / 'models' / 'database.db'
|
||
start_ms = int(datetime.datetime(2025, 1, 1).timestamp()) * 1000
|
||
end_ms = int(datetime.datetime(2026, 1, 1).timestamp()) * 1000
|
||
conn = sqlite3.connect(str(db_path))
|
||
rows = conn.cursor().execute(
|
||
"SELECT id, open, high, low, close FROM bitmart_eth_1m WHERE id >= ? AND id < ? ORDER BY id",
|
||
(start_ms, end_ms)
|
||
).fetchall()
|
||
conn.close()
|
||
data = []
|
||
for r in rows:
|
||
data.append((
|
||
datetime.datetime.fromtimestamp(r[0] / 1000.0),
|
||
r[1], r[2], r[3], r[4], # open, high, low, close
|
||
))
|
||
return data
|
||
|
||
|
||
def backtest(data, fp, sp, bp, atr_min, sl_pct, mh):
|
||
bal = 1000.0
|
||
pos = 0
|
||
op = 0.0
|
||
ot = None
|
||
ps = 0.0
|
||
pending = None
|
||
|
||
ef = EMA(fp)
|
||
es = EMA(sp)
|
||
eb = EMA(bp)
|
||
|
||
highs = []
|
||
lows = []
|
||
closes = []
|
||
pf = None
|
||
pslow = None
|
||
|
||
tc = 0
|
||
wc = 0
|
||
dir_pnl = 0.0
|
||
tot_fee = 0.0
|
||
tot_reb = 0.0
|
||
|
||
hsl = sl_pct * 1.5
|
||
lev = 50
|
||
rp = 0.005
|
||
tf_rate = 0.0006
|
||
reb_rate = 0.90
|
||
min_h = 200
|
||
|
||
for dt, o_, h_, l_, c_ in data:
|
||
price = c_
|
||
highs.append(h_)
|
||
lows.append(l_)
|
||
closes.append(price)
|
||
|
||
fast = ef.update(price)
|
||
slow = es.update(price)
|
||
big = eb.update(price)
|
||
|
||
# ATR
|
||
atr_pct = 0
|
||
ap = 14
|
||
if len(highs) > ap + 1:
|
||
trs = []
|
||
for i in range(-ap, 0):
|
||
tr = max(highs[i] - lows[i], abs(highs[i] - closes[i-1]), abs(lows[i] - closes[i-1]))
|
||
trs.append(tr)
|
||
atr_pct = (sum(trs) / ap) / price if price > 0 else 0
|
||
|
||
cu = pf is not None and pf <= pslow and fast > slow
|
||
cd = pf is not None and pf >= pslow and fast < slow
|
||
pf = fast
|
||
pslow = slow
|
||
|
||
if pos != 0 and ot:
|
||
if pos == 1:
|
||
p = (price - op) / op
|
||
else:
|
||
p = (op - price) / op
|
||
hs = (dt - ot).total_seconds()
|
||
|
||
if -p >= hsl:
|
||
# close
|
||
pnl_ = ps * p
|
||
cv = ps * (1 + p)
|
||
cf = cv * tf_rate
|
||
of_ = ps * tf_rate
|
||
ttf = of_ + cf
|
||
rb = ttf * reb_rate
|
||
bal += pnl_ - cf + rb
|
||
dir_pnl += pnl_
|
||
tot_fee += ttf
|
||
tot_reb += rb
|
||
tc += 1
|
||
if pnl_ > 0: wc += 1
|
||
pos = 0; op = 0; ot = None; ps = 0; pending = None
|
||
continue
|
||
|
||
can_c = hs >= min_h
|
||
|
||
if can_c:
|
||
do_close = False
|
||
if -p >= sl_pct:
|
||
do_close = True
|
||
elif hs >= mh:
|
||
do_close = True
|
||
elif pos == 1 and cd:
|
||
do_close = True
|
||
elif pos == -1 and cu:
|
||
do_close = True
|
||
elif pending == 'cl' and pos == 1:
|
||
do_close = True
|
||
elif pending == 'cs' and pos == -1:
|
||
do_close = True
|
||
|
||
if do_close:
|
||
pnl_ = ps * p
|
||
cv = ps * (1 + p)
|
||
cf = cv * tf_rate
|
||
of_ = ps * tf_rate
|
||
ttf = of_ + cf
|
||
rb = ttf * reb_rate
|
||
bal += pnl_ - cf + rb
|
||
dir_pnl += pnl_
|
||
tot_fee += ttf
|
||
tot_reb += rb
|
||
tc += 1
|
||
if pnl_ > 0: wc += 1
|
||
pos = 0; op = 0; ot = None; ps = 0; pending = None
|
||
|
||
# re-enter
|
||
if atr_pct >= atr_min:
|
||
if (cd or (fast < slow)) and price < big:
|
||
ns = bal * rp * lev
|
||
if ns >= 1:
|
||
bal -= ns * tf_rate
|
||
pos = -1; op = price; ot = dt; ps = ns
|
||
elif (cu or (fast > slow)) and price > big:
|
||
ns = bal * rp * lev
|
||
if ns >= 1:
|
||
bal -= ns * tf_rate
|
||
pos = 1; op = price; ot = dt; ps = ns
|
||
continue
|
||
else:
|
||
if pos == 1 and cd: pending = 'cl'
|
||
elif pos == -1 and cu: pending = 'cs'
|
||
|
||
if pos == 0 and atr_pct >= atr_min:
|
||
if cu and price > big:
|
||
ns = bal * rp * lev
|
||
if ns >= 1:
|
||
bal -= ns * tf_rate
|
||
pos = 1; op = price; ot = dt; ps = ns
|
||
elif cd and price < big:
|
||
ns = bal * rp * lev
|
||
if ns >= 1:
|
||
bal -= ns * tf_rate
|
||
pos = -1; op = price; ot = dt; ps = ns
|
||
|
||
# force close
|
||
if pos != 0:
|
||
price = data[-1][4]
|
||
if pos == 1:
|
||
p = (price - op) / op
|
||
else:
|
||
p = (op - price) / op
|
||
pnl_ = ps * p
|
||
cv = ps * (1 + p)
|
||
cf = cv * tf_rate
|
||
of_ = ps * tf_rate
|
||
ttf = of_ + cf
|
||
rb = ttf * reb_rate
|
||
bal += pnl_ - cf + rb
|
||
dir_pnl += pnl_
|
||
tot_fee += ttf
|
||
tot_reb += rb
|
||
tc += 1
|
||
if pnl_ > 0: wc += 1
|
||
|
||
net = bal - 1000.0
|
||
wr = wc / tc * 100 if tc > 0 else 0
|
||
return net, tc, wr, dir_pnl, tot_reb, tot_fee - tot_reb
|
||
|
||
|
||
def main():
|
||
print("Loading data...", flush=True)
|
||
data = load_data()
|
||
print(f"Loaded {len(data)} bars", flush=True)
|
||
|
||
# Focused parameter grid (smaller)
|
||
fast_list = [8, 13, 20]
|
||
slow_list = [21, 34, 55]
|
||
big_list = [120, 200]
|
||
atr_list = [0.0003, 0.0006, 0.001, 0.0015]
|
||
sl_list = [0.003, 0.005, 0.008]
|
||
mh_list = [900, 1800, 3600]
|
||
|
||
combos = []
|
||
for fp in fast_list:
|
||
for sp in slow_list:
|
||
if fp >= sp: continue
|
||
for bp in big_list:
|
||
for am in atr_list:
|
||
for sl in sl_list:
|
||
for mh in mh_list:
|
||
combos.append((fp, sp, bp, am, sl, mh))
|
||
|
||
print(f"\nTotal 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 = backtest(data, fp, sp, bp, am, sl, mh)
|
||
results.append((net, tc, wr, dp, reb, nf, fp, sp, bp, am, sl, mh))
|
||
|
||
if (idx + 1) % 20 == 0:
|
||
elapsed = time.time() - t0
|
||
eta = elapsed / (idx + 1) * (len(combos) - idx - 1)
|
||
print(f" [{idx+1}/{len(combos)}] {elapsed:.0f}s elapsed, ~{eta:.0f}s remaining", flush=True)
|
||
|
||
total_time = time.time() - t0
|
||
print(f"\nDone! {len(results)} combos in {total_time:.1f}s\n", flush=True)
|
||
|
||
# Sort by net P&L
|
||
results.sort(key=lambda x: x[0], reverse=True)
|
||
|
||
# Print results
|
||
profitable = [r for r in results if r[0] > 0]
|
||
print(f"Profitable: {len(profitable)} / {len(results)} ({len(profitable)/len(results)*100:.1f}%)\n")
|
||
|
||
print(f"{'='*130}")
|
||
print(f" TOP 30 RESULTS")
|
||
print(f"{'='*130}")
|
||
header = f" {'#':>3} {'Fast':>4} {'Slow':>4} {'Big':>4} {'ATR%':>6} {'SL%':>5} {'MaxH':>5} | {'Net%':>7} {'Net$':>9} {'Trades':>7} {'WR':>6} {'DirPnL':>9} {'Rebate':>9} {'NetFee':>8}"
|
||
print(header)
|
||
print(f" {'-'*126}")
|
||
|
||
for i, (net, tc, wr, dp, reb, nf, fp, sp, bp, am, sl, mh) in enumerate(results[:30]):
|
||
net_pct = net / 10 # net / 1000 * 100
|
||
marker = " ***" if net > 0 else ""
|
||
print(f" {i+1:>3} {fp:>4} {sp:>4} {bp:>4} {am*100:>5.2f}% {sl*100:>4.1f}% {mh:>5} | {net_pct:>+6.2f}% {net:>+8.2f} {tc:>7} {wr:>5.1f}% {dp:>+8.2f} {reb:>8.2f} {nf:>8.2f}{marker}")
|
||
|
||
print(f"{'='*130}")
|
||
|
||
if profitable:
|
||
print(f"\nALL PROFITABLE:")
|
||
print(f" {'-'*126}")
|
||
for i, (net, tc, wr, dp, reb, nf, fp, sp, bp, am, sl, mh) in enumerate(profitable):
|
||
net_pct = net / 10
|
||
print(f" {i+1:>3} EMA({fp}/{sp}/{bp}) ATR>{am*100:.2f}% SL={sl*100:.1f}% MaxH={mh}s | {net_pct:>+6.2f}% {net:>+8.2f} trades={tc} WR={wr:.1f}% DirPnL={dp:+.2f} Rebate={reb:.2f}")
|
||
|
||
# Save CSV
|
||
csv_path = Path(__file__).parent.parent / 'param_results.csv'
|
||
with open(csv_path, '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"\nResults saved: {csv_path}")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|