""" EMA趋势策略参数优化扫描 基于前一轮回测发现 EMA-Trend 方向盈利 +327 USDT,仅差 101 USDT 即可盈利。 核心优化方向:减少交易次数(降低费用),同时保持方向盈利。 扫描参数: - fast_ema: [8, 13, 15, 20] - slow_ema: [21, 34, 40, 55] - big_ema: [120, 200, 300] - atr_min_pct: [0.0003, 0.0005, 0.0008, 0.0012] - stop_loss: [0.003, 0.004, 0.005, 0.006] - max_hold: [900, 1200, 1800, 2700, 3600] """ import time import datetime import sqlite3 import itertools from pathlib import Path from dataclasses import dataclass from typing import List # ========================= EMA ========================= 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(start_date='2025-01-01', end_date='2025-12-31'): db_path = Path(__file__).parent.parent / 'models' / 'database.db' start_ms = int(datetime.datetime.strptime(start_date, '%Y-%m-%d').timestamp()) * 1000 end_ms = int((datetime.datetime.strptime(end_date, '%Y-%m-%d') + datetime.timedelta(days=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.datetime.fromtimestamp(r[0] / 1000.0), 'open': r[1], 'high': r[2], 'low': r[3], 'close': r[4], }) return data # ========================= 快速回测引擎 ========================= def run_ema_backtest(data, fast_p, slow_p, big_p, atr_period, atr_min, stop_loss_pct, hard_sl_pct, max_hold_sec, initial_balance=1000.0, leverage=50, risk_pct=0.005, taker_fee=0.0006, rebate_rate=0.90, min_hold_sec=200): """ 快速回测 EMA 趋势策略,返回关键指标字典 """ balance = initial_balance position = 0 # -1/0/1 open_price = 0.0 open_time = None pos_size = 0.0 pending = None ema_f = EMA(fast_p) ema_s = EMA(slow_p) ema_b = EMA(big_p) highs = [] lows = [] closes = [] prev_fast = None prev_slow = None trade_count = 0 win_count = 0 total_dir_pnl = 0.0 total_fee = 0.0 total_rebate = 0.0 def calc_atr(): if len(highs) < atr_period + 1: return None trs = [] for i in range(-atr_period, 0): tr = max(highs[i] - lows[i], abs(highs[i] - closes[i-1]), abs(lows[i] - closes[i-1])) trs.append(tr) return sum(trs) / len(trs) def do_open(direction, price, dt_): nonlocal balance, position, open_price, open_time, pos_size, total_fee ps = balance * risk_pct * leverage if ps < 1: return fee_ = ps * taker_fee balance -= fee_ position = 1 if direction == 'long' else -1 open_price = price open_time = dt_ pos_size = ps def do_close(price, dt_): nonlocal balance, position, open_price, open_time, pos_size nonlocal trade_count, win_count, total_dir_pnl, total_fee, total_rebate, pending if position == 0: return if position == 1: pp = (price - open_price) / open_price else: pp = (open_price - price) / open_price pnl_ = pos_size * pp cv = pos_size * (1 + pp) cf = cv * taker_fee of = pos_size * taker_fee tf = of + cf rb = tf * rebate_rate balance += pnl_ - cf + rb total_dir_pnl += pnl_ total_fee += tf total_rebate += rb trade_count += 1 if pnl_ > 0: win_count += 1 position = 0 open_price = 0.0 open_time = None pos_size = 0.0 pending = None for bar in data: price = bar['close'] dt = bar['datetime'] highs.append(bar['high']) lows.append(bar['low']) closes.append(price) fast = ema_f.update(price) slow = ema_s.update(price) big = ema_b.update(price) atr = calc_atr() atr_pct = atr / price if atr and price > 0 else 0 cross_up = (prev_fast is not None and prev_fast <= prev_slow and fast > slow) cross_down = (prev_fast is not None and prev_fast >= prev_slow and fast < slow) prev_fast = fast prev_slow = slow # === 有持仓 === if position != 0 and open_time: if position == 1: p = (price - open_price) / open_price else: p = (open_price - price) / open_price hs = (dt - open_time).total_seconds() # 硬止损 if -p >= hard_sl_pct: do_close(price, dt) continue can_close_ = hs >= min_hold_sec if can_close_: # 止损 if -p >= stop_loss_pct: do_close(price, dt) continue # 超时 if hs >= max_hold_sec: do_close(price, dt) continue # 反手 if position == 1 and cross_down: do_close(price, dt) if price < big and atr_pct >= atr_min: do_open('short', price, dt) continue if position == -1 and cross_up: do_close(price, dt) if price > big and atr_pct >= atr_min: do_open('long', price, dt) continue # 延迟信号 if pending == 'close_long' and position == 1: do_close(price, dt) if fast < slow and price < big and atr_pct >= atr_min: do_open('short', price, dt) continue if pending == 'close_short' and position == -1: do_close(price, dt) if fast > slow and price > big and atr_pct >= atr_min: do_open('long', price, dt) continue else: if position == 1 and cross_down: pending = 'close_long' elif position == -1 and cross_up: pending = 'close_short' # === 无持仓 === if position == 0 and atr_pct >= atr_min: if cross_up and price > big: do_open('long', price, dt) elif cross_down and price < big: do_open('short', price, dt) # 强制平仓 if position != 0: do_close(data[-1]['close'], data[-1]['datetime']) net = balance - initial_balance net_fee_cost = total_fee - total_rebate vol = total_fee / taker_fee if taker_fee > 0 else 0 wr = win_count / trade_count * 100 if trade_count > 0 else 0 avg_dir = total_dir_pnl / trade_count if trade_count > 0 else 0 return { 'balance': balance, 'net': net, 'net_pct': net / initial_balance * 100, 'trades': trade_count, 'win_rate': wr, 'dir_pnl': total_dir_pnl, 'total_fee': total_fee, 'rebate': total_rebate, 'net_fee': net_fee_cost, 'volume': vol, 'avg_dir_pnl': avg_dir, } # ========================= 参数扫描 ========================= def main(): print("Loading data...") data = load_data('2025-01-01', '2025-12-31') print(f"Loaded {len(data)} bars\n") # 参数组合 param_grid = { 'fast_p': [8, 13, 15, 20], 'slow_p': [21, 34, 40, 55], 'big_p': [120, 200, 300], 'atr_min': [0.0003, 0.0005, 0.0008, 0.0012], 'stop_loss_pct':[0.003, 0.004, 0.005, 0.008], 'max_hold_sec': [900, 1200, 1800, 3600], } # 过滤无效组合 (fast >= slow) combos = [] for fp, sp, bp, am, sl, mh in itertools.product( param_grid['fast_p'], param_grid['slow_p'], param_grid['big_p'], param_grid['atr_min'], param_grid['stop_loss_pct'], param_grid['max_hold_sec'] ): if fp >= sp: continue combos.append((fp, sp, bp, am, sl, mh)) print(f"Total parameter combinations: {len(combos)}") print(f"Estimated time: ~{len(combos) * 2 / 60:.0f} minutes\n") # 只跑最有潜力的子集(减少扫描时间) # 基于前次回测,聚焦在更长周期EMA(减少交易)+ 更高ATR过滤(质量过滤) focused_combos = [] for fp, sp, bp, am, sl, mh in combos: # 过滤:聚焦减少交易次数的参数 if fp < 8: continue if sp < 21: continue focused_combos.append((fp, sp, bp, am, sl, mh)) print(f"Focused combinations: {len(focused_combos)}") # 如果组合太多,进一步采样 if len(focused_combos) > 500: # 分两轮:先粗扫,再精调 print("Phase 1: Coarse scan with subset...") coarse_combos = [] for fp, sp, bp, am, sl, mh in focused_combos: if am in [0.0003, 0.0008] and sl in [0.004, 0.006] and mh in [1200, 1800]: coarse_combos.append((fp, sp, bp, am, sl, mh)) elif am in [0.0005, 0.0012] and sl in [0.003, 0.005, 0.008] and mh in [900, 3600]: coarse_combos.append((fp, sp, bp, am, sl, mh)) focused_combos = coarse_combos[:600] # cap print(f" Reduced to {len(focused_combos)} combos") results = [] t0 = time.time() for idx, (fp, sp, bp, am, sl, mh) in enumerate(focused_combos): r = run_ema_backtest( data, fast_p=fp, slow_p=sp, big_p=bp, atr_period=14, atr_min=am, stop_loss_pct=sl, hard_sl_pct=sl * 1.5, max_hold_sec=mh, ) r['params'] = f"EMA({fp}/{sp}/{bp}) ATR>{am*100:.2f}% SL={sl*100:.1f}% MaxH={mh}s" r['fp'] = fp r['sp'] = sp r['bp'] = bp r['am'] = am r['sl'] = sl r['mh'] = mh results.append(r) if (idx + 1) % 50 == 0: elapsed = time.time() - t0 eta = elapsed / (idx + 1) * (len(focused_combos) - idx - 1) print(f" [{idx+1}/{len(focused_combos)}] elapsed={elapsed:.0f}s eta={eta:.0f}s") total_time = time.time() - t0 print(f"\nScan complete! {len(results)} combos in {total_time:.1f}s") # 按净收益排序 results.sort(key=lambda x: x['net'], reverse=True) # === 打印 Top 20 === print(f"\n{'='*120}") print(f" TOP 20 PARAMETER COMBINATIONS (by Net P&L)") print(f"{'='*120}") print(f" {'#':>3} {'Params':<52} {'Net%':>7} {'Net$':>9} {'Trades':>7} {'WR':>6} {'DirPnL':>9} {'Rebate':>9} {'NetFee':>8}") print(f" {'-'*116}") for i, r in enumerate(results[:20]): print(f" {i+1:>3} {r['params']:<52} {r['net_pct']:>+6.2f}% {r['net']:>+8.2f} {r['trades']:>7} {r['win_rate']:>5.1f}% {r['dir_pnl']:>+8.2f} {r['rebate']:>8.2f} {r['net_fee']:>8.2f}") # === 打印 Bottom 5 (最差) === print(f"\n BOTTOM 5:") print(f" {'-'*116}") for i, r in enumerate(results[-5:]): print(f" {len(results)-4+i:>3} {r['params']:<52} {r['net_pct']:>+6.2f}% {r['net']:>+8.2f} {r['trades']:>7} {r['win_rate']:>5.1f}% {r['dir_pnl']:>+8.2f} {r['rebate']:>8.2f} {r['net_fee']:>8.2f}") print(f"{'='*120}") # === 盈利的参数组合统计 === profitable = [r for r in results if r['net'] > 0] print(f"\nProfitable combinations: {len(profitable)} / {len(results)} ({len(profitable)/len(results)*100:.1f}%)") if profitable: print(f"\nAll profitable combinations:") print(f" {'#':>3} {'Params':<52} {'Net%':>7} {'Net$':>9} {'Trades':>7} {'WR':>6} {'DirPnL':>9} {'Rebate':>9}") print(f" {'-'*106}") for i, r in enumerate(profitable): print(f" {i+1:>3} {r['params']:<52} {r['net_pct']:>+6.2f}% {r['net']:>+8.2f} {r['trades']:>7} {r['win_rate']:>5.1f}% {r['dir_pnl']:>+8.2f} {r['rebate']:>8.2f}") # 保存全部结果到CSV csv_path = Path(__file__).parent.parent / 'param_scan_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,volume\n") for r in results: f.write(f"{r['fp']},{r['sp']},{r['bp']},{r['am']},{r['sl']},{r['mh']}," f"{r['net_pct']:.4f},{r['net']:.4f},{r['trades']},{r['win_rate']:.2f}," f"{r['dir_pnl']:.4f},{r['rebate']:.4f},{r['net_fee']:.4f},{r['volume']:.0f}\n") print(f"\nFull results saved: {csv_path}") if __name__ == '__main__': main()