316 lines
11 KiB
Python
316 lines
11 KiB
Python
"""
|
||
盈利信号组合策略 — 只保留回测验证盈利的3种信号
|
||
|
||
信号1: EMA交叉 (83笔, +1773) — 趋势变化捕捉
|
||
信号2: 吞没形态 (1909笔, +2471) — K线反转形态
|
||
信号3: BB反弹 (438笔, +171) — 均值回归
|
||
|
||
去掉: 三分之一(-17290)、PinBar(-1936) — 亏损信号
|
||
|
||
条件: 同一时间只持1个仓, 100U保证金, 100x杠杆, 90%返佣
|
||
"""
|
||
import sys, time, datetime, sqlite3
|
||
from pathlib import Path
|
||
from collections import defaultdict
|
||
|
||
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 calc_bb(closes, period=20, nstd=2.0):
|
||
if len(closes) < period: return None, None, None
|
||
rec = closes[-period:]
|
||
mid = sum(rec) / period
|
||
var = sum((x - mid)**2 for x in rec) / period
|
||
std = var ** 0.5
|
||
return mid + nstd * std, mid, mid - nstd * std
|
||
|
||
def calc_rsi(closes, period=14):
|
||
if len(closes) < period + 1: return None
|
||
g = 0; l = 0
|
||
for i in range(-period, 0):
|
||
d = closes[i] - closes[i-1]
|
||
if d > 0: g += d
|
||
else: l -= d
|
||
if l == 0: return 100.0
|
||
return 100 - 100 / (1 + (g/period)/(l/period))
|
||
|
||
def run_combo(data, notional, engulf_body_min, engulf_ratio, engulf_sl, engulf_tp,
|
||
bb_rsi_long, bb_rsi_short, bb_sl, bb_tp, bb_atr_min,
|
||
ema_atr_min, ema_sl):
|
||
N = len(data)
|
||
FEE = notional * 0.0006 * 2
|
||
REB = FEE * 0.9
|
||
NFEE = FEE - REB
|
||
MIN_HOLD = 200; MAX_HOLD = 1800
|
||
|
||
ema_f = EMA(8); ema_s = EMA(21); ema_b = EMA(120)
|
||
pf_ = None; ps_ = None
|
||
CB = []; HB = []; LB = []
|
||
pos = 0; op = 0.0; ot = None; st = ""; sl = 0; tp = 0
|
||
trades = []
|
||
|
||
def close_(price, dt_, reason):
|
||
nonlocal pos, op, ot, st
|
||
pp = (price-op)/op if pos==1 else (op-price)/op
|
||
trades.append((st, 'L' if pos==1 else 'S', op, price, notional*pp,
|
||
(dt_-ot).total_seconds(), reason, ot, dt_))
|
||
pos=0; op=0; ot=None; st=""
|
||
|
||
for i in range(N):
|
||
dt, o_, h_, l_, c_ = data[i]
|
||
p = c_
|
||
CB.append(p); HB.append(h_); LB.append(l_)
|
||
if len(CB) > 300: CB=CB[-300:]; HB=HB[-300:]; LB=LB[-300:]
|
||
|
||
fast = ema_f.update(p); slow = ema_s.update(p); big = ema_b.update(p)
|
||
atr = 0.0
|
||
if len(HB) > 15:
|
||
s=0
|
||
for j in range(-14, 0):
|
||
tr=HB[j]-LB[j]; d1=abs(HB[j]-CB[j-1]); d2=abs(LB[j]-CB[j-1])
|
||
if d1>tr: tr=d1
|
||
if d2>tr: tr=d2
|
||
s+=tr
|
||
atr = s/(14*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
|
||
|
||
bb_u, bb_m, bb_l = calc_bb(CB, 20, 2.0)
|
||
rsi = calc_rsi(CB, 14)
|
||
|
||
# 持仓管理
|
||
if pos!=0 and ot:
|
||
pp=(p-op)/op if pos==1 else (op-p)/op
|
||
hsec=(dt-ot).total_seconds()
|
||
hsl = max(sl*1.5, 0.006)
|
||
if -pp>=hsl: close_(p, dt, "硬止损"); continue
|
||
if hsec>=MIN_HOLD:
|
||
if -pp>=sl: close_(p, dt, "止损"); continue
|
||
if tp>0 and pp>=tp: close_(p, dt, "止盈"); continue
|
||
if hsec>=MAX_HOLD: close_(p, dt, "超时"); continue
|
||
if st=="EMA":
|
||
if pos==1 and cd: close_(p, dt, "EMA反转"); continue
|
||
if pos==-1 and cu: close_(p, dt, "EMA反转"); continue
|
||
if st=="BB" and bb_m:
|
||
if pos==1 and p>=bb_m: close_(p, dt, "BB回中轨"); continue
|
||
if pos==-1 and p<=bb_m: close_(p, dt, "BB回中轨"); continue
|
||
|
||
# 开仓
|
||
if pos==0 and i>20:
|
||
sig=None; s_sl=0; s_tp=0; s_type=""
|
||
|
||
# 信号1: EMA
|
||
if atr>=ema_atr_min:
|
||
if cu and p>big: sig='L'; s_type="EMA"; s_sl=ema_sl; s_tp=0
|
||
elif cd and p<big: sig='S'; s_type="EMA"; s_sl=ema_sl; s_tp=0
|
||
|
||
# 信号2: 吞没
|
||
if sig is None and i>0:
|
||
pb_o,pb_c = data[i-1][1], data[i-1][4]
|
||
cb_o,cb_c = o_, c_
|
||
pb_body = abs(pb_c-pb_o)
|
||
cb_body = abs(cb_c-cb_o)
|
||
cb_pct = cb_body/p if p>0 else 0
|
||
|
||
if cb_pct>=engulf_body_min and cb_body>pb_body*engulf_ratio:
|
||
if pb_c<pb_o and cb_c>cb_o and cb_c>pb_o and cb_o<=pb_c:
|
||
if p>big and atr>=0.001:
|
||
sig='L'; s_type="吞没"; s_sl=engulf_sl; s_tp=engulf_tp
|
||
elif pb_c>pb_o and cb_c<cb_o and cb_c<pb_o and cb_o>=pb_c:
|
||
if p<big and atr>=0.001:
|
||
sig='S'; s_type="吞没"; s_sl=engulf_sl; s_tp=engulf_tp
|
||
|
||
# 信号3: BB
|
||
if sig is None and bb_u and rsi is not None:
|
||
if p<=bb_l and rsi<bb_rsi_long and p>big and atr>=bb_atr_min:
|
||
sig='L'; s_type="BB"; s_sl=bb_sl; s_tp=bb_tp
|
||
elif p>=bb_u and rsi>bb_rsi_short and p<big and atr>=bb_atr_min:
|
||
sig='S'; s_type="BB"; s_sl=bb_sl; s_tp=bb_tp
|
||
|
||
if sig:
|
||
pos=1 if sig=='L' else -1; op=p; ot=dt; st=s_type; sl=s_sl; tp=s_tp
|
||
|
||
if pos!=0: close_(data[-1][4], data[-1][0], "结束")
|
||
return trades
|
||
|
||
def analyze_print(trades, notional, label=""):
|
||
if not trades:
|
||
print(f" [{label}] No trades", flush=True); return 0
|
||
n = len(trades)
|
||
FEE = notional * 0.0006 * 2
|
||
REB = FEE * 0.9
|
||
NFEE = FEE - REB
|
||
|
||
total_pnl = sum(t[4] for t in trades)
|
||
net = total_pnl - NFEE * n
|
||
total_reb = REB * n
|
||
wins = len([t for t in trades if t[4]>0])
|
||
wr = wins/n*100
|
||
|
||
by_type = defaultdict(lambda: {'n':0,'pnl':0,'w':0})
|
||
for t in trades:
|
||
by_type[t[0]]['n']+=1; by_type[t[0]]['pnl']+=t[4]
|
||
if t[4]>0: by_type[t[0]]['w']+=1
|
||
|
||
monthly = defaultdict(lambda: {'n':0,'net':0,'w':0})
|
||
for t in trades:
|
||
k = t[8].strftime('%Y-%m')
|
||
monthly[k]['n']+=1
|
||
monthly[k]['net']+=t[4]-NFEE
|
||
if t[4]>0: monthly[k]['w']+=1
|
||
|
||
cum=0; peak=0; dd=0
|
||
for t in trades:
|
||
cum+=t[4]-NFEE
|
||
if cum>peak: peak=cum
|
||
if peak-cum>dd: dd=peak-cum
|
||
|
||
# 月度盈利月数
|
||
profit_months = len([m for m in monthly.values() if m['net']>0])
|
||
min_month = min(monthly.values(), key=lambda x: x['net'])
|
||
max_month = max(monthly.values(), key=lambda x: x['net'])
|
||
|
||
print(f"\n{'='*75}", flush=True)
|
||
print(f" {label}", flush=True)
|
||
print(f"{'='*75}", flush=True)
|
||
print(f" 年净利: {net:>+10.2f} | 月均: {net/12:>+8.2f} | 交易: {n}笔 | 胜率: {wr:.1f}%", flush=True)
|
||
print(f" 返佣: {total_reb:>.0f} | 最大回撤: {dd:>.0f} | 盈利月: {profit_months}/12", flush=True)
|
||
|
||
print(f"\n 信号拆分:", flush=True)
|
||
for st in sorted(by_type.keys()):
|
||
d=by_type[st]
|
||
nt=d['pnl']-NFEE*d['n']
|
||
wt=d['w']/d['n']*100 if d['n']>0 else 0
|
||
print(f" {st:<8} {d['n']:>5}笔 净利{nt:>+8.0f} 胜率{wt:.0f}%", flush=True)
|
||
|
||
print(f"\n 月度:", flush=True)
|
||
print(f" {'月份':<8} {'笔':>4} {'净利':>9} {'胜率':>6}", flush=True)
|
||
print(f" {'-'*30}", flush=True)
|
||
for m in sorted(monthly.keys()):
|
||
d=monthly[m]
|
||
wr_m=d['w']/d['n']*100 if d['n']>0 else 0
|
||
mark=" <<" if d['net']<0 else ""
|
||
print(f" {m:<8} {d['n']:>4} {d['net']:>+9.0f} {wr_m:>5.1f}%{mark}", flush=True)
|
||
print(f" {'-'*30}", flush=True)
|
||
print(f" {'合计':<8} {n:>4} {net:>+9.0f}", flush=True)
|
||
print(f"{'='*75}", flush=True)
|
||
return net
|
||
|
||
def main():
|
||
print("Loading...", flush=True)
|
||
data = load()
|
||
print(f"{len(data)} bars\n", flush=True)
|
||
|
||
# === 测试多种参数组合 ===
|
||
configs = [
|
||
# (label, notional, engulf_body_min, engulf_ratio, engulf_sl, engulf_tp,
|
||
# bb_rsi_long, bb_rsi_short, bb_sl, bb_tp, bb_atr_min, ema_atr_min, ema_sl)
|
||
|
||
# 基线:上一轮参数
|
||
("v1: 基线", 10000,
|
||
0.001, 1.5, 0.004, 0.005,
|
||
30, 70, 0.003, 0.002, 0.0008,
|
||
0.003, 0.004),
|
||
|
||
# v2: 更严格的吞没(减少低质量交易)
|
||
("v2: 严格吞没", 10000,
|
||
0.0015, 2.0, 0.004, 0.006,
|
||
30, 70, 0.003, 0.002, 0.0008,
|
||
0.003, 0.004),
|
||
|
||
# v3: 更严格吞没 + 更宽BB RSI
|
||
("v3: 严格吞没+宽BB", 10000,
|
||
0.0015, 2.0, 0.004, 0.006,
|
||
25, 75, 0.003, 0.003, 0.001,
|
||
0.003, 0.004),
|
||
|
||
# v4: 最严格吞没 + 最严BB
|
||
("v4: 超严格", 10000,
|
||
0.002, 2.5, 0.005, 0.008,
|
||
20, 80, 0.004, 0.003, 0.001,
|
||
0.003, 0.004),
|
||
|
||
# v5: 中等吞没 + 更大止盈
|
||
("v5: 大止盈", 10000,
|
||
0.0012, 1.8, 0.005, 0.008,
|
||
28, 72, 0.004, 0.003, 0.001,
|
||
0.003, 0.004),
|
||
|
||
# v6: 去掉BB(只用EMA+吞没)
|
||
("v6: 仅EMA+吞没", 10000,
|
||
0.0015, 2.0, 0.004, 0.006,
|
||
999, -999, 0.003, 0.002, 999, # BB不会触发
|
||
0.003, 0.004),
|
||
|
||
# v7: 放宽EMA的ATR
|
||
("v7: EMA ATR>0.2%", 10000,
|
||
0.0015, 2.0, 0.004, 0.006,
|
||
25, 75, 0.003, 0.003, 0.001,
|
||
0.002, 0.004),
|
||
|
||
# v8: v3的最优 x 300U仓位
|
||
("v8: v3 x 300U仓位", 30000,
|
||
0.0015, 2.0, 0.004, 0.006,
|
||
25, 75, 0.003, 0.003, 0.001,
|
||
0.003, 0.004),
|
||
|
||
# v9: v3 x 500U
|
||
("v9: v3 x 500U仓位", 50000,
|
||
0.0015, 2.0, 0.004, 0.006,
|
||
25, 75, 0.003, 0.003, 0.001,
|
||
0.003, 0.004),
|
||
|
||
# v10: v4 x 500U
|
||
("v10: v4 x 500U", 50000,
|
||
0.002, 2.5, 0.005, 0.008,
|
||
20, 80, 0.004, 0.003, 0.001,
|
||
0.003, 0.004),
|
||
]
|
||
|
||
best_net = -99999; best_label = ""
|
||
summary = []
|
||
|
||
for cfg in configs:
|
||
label = cfg[0]
|
||
trades = run_combo(data, *cfg[1:])
|
||
net = analyze_print(trades, cfg[1], label)
|
||
summary.append((label, cfg[1], len(trades), net))
|
||
if net > best_net:
|
||
best_net = net; best_label = label
|
||
|
||
# 总览
|
||
print(f"\n\n{'='*80}", flush=True)
|
||
print(f" SUMMARY — 目标: 月均 1000 USDT", flush=True)
|
||
print(f"{'='*80}", flush=True)
|
||
print(f" {'方案':<25} {'名义值':>10} {'交易数':>6} {'年净利':>10} {'月均':>8} {'达标':>4}", flush=True)
|
||
print(f" {'-'*72}", flush=True)
|
||
for label, notional, n, net in summary:
|
||
mavg = net/12
|
||
ok = "Yes" if mavg>=1000 else "No"
|
||
print(f" {label:<25} {notional:>10,.0f} {n:>6} {net:>+10.0f} {mavg:>+8.0f} {ok:>4}", flush=True)
|
||
print(f" {'-'*72}", flush=True)
|
||
print(f" Best: {best_label} → {best_net:+.0f}/年 = {best_net/12:+.0f}/月", flush=True)
|
||
print(f"{'='*80}", flush=True)
|
||
|
||
|
||
if __name__=='__main__':
|
||
main()
|