This commit is contained in:
Your Name
2026-02-15 02:16:45 +08:00
parent c3817c7937
commit b5af5b07f3
30 changed files with 96125 additions and 0 deletions

View File

@@ -0,0 +1,307 @@
"""
BitMart ETH 返佣策略回测 — 固定仓位版
条件:
- 每笔仓位固定 100 USDT 保证金
- 100 倍杠杆 → 每笔名义价值 10,000 USDT
- ETH 合约
- 90% 手续费返佣
- 最低持仓 > 3 分钟
策略EMA(8/21/120) + ATR>0.3% 过滤(回测最优参数)
"""
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 main():
print("=" * 70, flush=True)
print(" ETH 返佣策略回测 | 固定 100U 仓位 | 100x 杠杆", flush=True)
print("=" * 70, flush=True)
# ===== 固定参数 =====
MARGIN = 100.0 # 每笔保证金 100 USDT
LEVERAGE = 100 # 100 倍杠杆
NOTIONAL = MARGIN * LEVERAGE # 名义价值 10,000 USDT
TAKER_FEE = 0.0006 # taker 手续费 0.06%
REBATE_RATE = 0.90 # 90% 返佣
MIN_HOLD = 200 # 最低持仓秒数 (>3分钟)
MAX_HOLD = 1800 # 最大持仓秒数 (30分钟)
SL_PCT = 0.004 # 止损 0.4%
HARD_SL = 0.006 # 硬止损 0.6%
# EMA 参数(回测最优)
FP, SP, BP = 8, 21, 120
ATR_MIN = 0.003 # ATR > 0.3%
ATR_P = 14
print(f"\n 保证金: {MARGIN} USDT/笔", flush=True)
print(f" 杠杆: {LEVERAGE}x → 名义价值: {NOTIONAL:,.0f} USDT/笔", flush=True)
print(f" 手续费: {TAKER_FEE*100:.2f}% | 返佣: {REBATE_RATE*100:.0f}%", flush=True)
print(f" 策略: EMA({FP}/{SP}/{BP}) ATR>{ATR_MIN*100:.1f}%", flush=True)
print(f" 止损: {SL_PCT*100:.1f}% | 硬止损: {HARD_SL*100:.1f}%", flush=True)
print(f" 持仓: {MIN_HOLD}s ~ {MAX_HOLD}s\n", flush=True)
data = load()
print(f" 数据: {len(data)} 根 1分钟K线 (2025全年)\n", flush=True)
# ===== 回测引擎 =====
ef = EMA(FP); es = EMA(SP); eb = EMA(BP)
H = []; L = []; C = []
pf_ = None; ps_ = None
pos = 0 # -1/0/1
op = 0.0 # 开仓价
ot = None # 开仓时间
pend = None # 延迟信号
# 统计
trades = [] # [(方向, 开仓价, 平仓价, 盈亏, 手续费, 返佣, 持仓秒, 原因, 开仓时间, 平仓时间)]
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
atr_pct = 0.0
if len(H) > ATR_P + 1:
s = 0.0
for i in range(-ATR_P, 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 / (ATR_P * p) if p > 0 else 0
# EMA 交叉检测
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 >= HARD_SL:
pnl = NOTIONAL * pp
fee = NOTIONAL * TAKER_FEE * 2 # 开+平
reb = fee * REBATE_RATE
d = 'long' if pos == 1 else 'short'
trades.append((d, op, p, pnl, fee, reb, hsec, f"硬止损({pp*100:+.2f}%)", ot, dt))
pos = 0; op = 0; ot = None; pend = None
continue
can_c = hsec >= MIN_HOLD
if can_c:
do_close = False; reason = ""
if -pp >= SL_PCT:
do_close = True; reason = f"止损({pp*100:+.2f}%)"
elif hsec >= MAX_HOLD:
do_close = True; reason = f"超时({hsec:.0f}s)"
elif pos == 1 and cd:
do_close = True; reason = "死叉反转"
elif pos == -1 and cu:
do_close = True; reason = "金叉反转"
elif pend == 'cl' and pos == 1:
do_close = True; reason = "延迟死叉"
elif pend == 'cs' and pos == -1:
do_close = True; reason = "延迟金叉"
if do_close:
pnl = NOTIONAL * pp
fee = NOTIONAL * TAKER_FEE * 2
reb = fee * REBATE_RATE
d = 'long' if pos == 1 else 'short'
trades.append((d, op, p, pnl, fee, reb, hsec, reason, ot, dt))
pos = 0; op = 0; ot = None; pend = None
# 反手
if atr_pct >= ATR_MIN:
if (cd or fast < slow) and p < big:
pos = -1; op = p; ot = dt
elif (cu or fast > slow) and p > big:
pos = 1; op = p; ot = 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:
pos = 1; op = p; ot = dt
elif cd and p < big:
pos = -1; op = p; ot = dt
# 强制平仓
if pos != 0:
p = data[-1][4]; dt = data[-1][0]
pp = (p - op) / op if pos == 1 else (op - p) / op
pnl = NOTIONAL * pp
fee = NOTIONAL * TAKER_FEE * 2
reb = fee * REBATE_RATE
d = 'long' if pos == 1 else 'short'
trades.append((d, op, p, pnl, fee, reb, (dt - ot).total_seconds(), "回测结束", ot, dt))
# ===== 输出结果 =====
if not trades:
print(" 无交易记录!", flush=True)
return
n = len(trades)
wins = [t for t in trades if t[3] > 0]
losses = [t for t in trades if t[3] <= 0]
total_pnl = sum(t[3] for t in trades) # 方向总盈亏
total_fee = sum(t[4] for t in trades) # 总手续费
total_rebate = sum(t[5] for t in trades) # 总返佣
net_fee = total_fee - total_rebate # 净手续费成本(10%)
net_profit = total_pnl - net_fee # 最终净利润
total_volume = NOTIONAL * n * 2 # 总交易额(开+平)
avg_hold = sum(t[6] for t in trades) / n
wr = len(wins) / n * 100
avg_win = sum(t[3] for t in wins) / len(wins) if wins else 0
avg_loss = sum(t[3] for t in losses) / len(losses) if losses else 0
best = max(t[3] for t in trades)
worst = min(t[3] for t in trades)
pf_num = sum(t[3] for t in wins) if wins else 0
pf_den = abs(sum(t[3] for t in losses)) if losses else 0
pf = pf_num / pf_den if pf_den > 0 else float('inf')
long_t = [t for t in trades if t[0] == 'long']
short_t = [t for t in trades if t[0] == 'short']
long_wr = len([t for t in long_t if t[3] > 0]) / len(long_t) * 100 if long_t else 0
short_wr = len([t for t in short_t if t[3] > 0]) / len(short_t) * 100 if short_t else 0
# 连续亏损
max_streak = 0; cur = 0
for t in trades:
if t[3] <= 0: cur += 1; max_streak = max(max_streak, cur)
else: cur = 0
# 最大回撤(基于累计净利润)
cum = 0; peak = 0; max_dd = 0
for t in trades:
net_t = t[3] - (t[4] - t[5]) # pnl - net_fee
cum += net_t
if cum > peak: peak = cum
dd = peak - cum
if dd > max_dd: max_dd = dd
# 平仓原因
reasons = {}
for t in trades:
r = t[7].split('(')[0]
reasons[r] = reasons.get(r, 0) + 1
print("=" * 70, flush=True)
print(" 回测结果", flush=True)
print("=" * 70, flush=True)
print(f"\n --- 核心收益 ---", flush=True)
print(f" 方向交易盈亏: {total_pnl:>+12.2f} USDT", flush=True)
print(f" 总手续费: {total_fee:>12.2f} USDT", flush=True)
print(f" 返佣收入(90%): {total_rebate:>+12.2f} USDT", flush=True)
print(f" 净手续费(10%): {net_fee:>12.2f} USDT", flush=True)
print(f" ================================", flush=True)
print(f" 最终净利润: {net_profit:>+12.2f} USDT", flush=True)
print(f" 最大回撤: {max_dd:>12.2f} USDT", flush=True)
print(f"\n --- 交易统计 ---", flush=True)
print(f" 总交易次数: {n:>8}", flush=True)
print(f" 盈利笔数: {len(wins):>8} 笔 ({wr:.1f}%)", flush=True)
print(f" 亏损笔数: {len(losses):>8} 笔 ({100-wr:.1f}%)", flush=True)
print(f" 做多: {len(long_t):>8} 笔 (胜率 {long_wr:.1f}%)", flush=True)
print(f" 做空: {len(short_t):>8} 笔 (胜率 {short_wr:.1f}%)", flush=True)
print(f" 盈亏比: {pf:>8.2f}", flush=True)
print(f" 最大连亏: {max_streak:>8}", flush=True)
print(f"\n --- 单笔详情 ---", flush=True)
print(f" 每笔保证金: {MARGIN:>8.0f} USDT", flush=True)
print(f" 每笔名义价值: {NOTIONAL:>8,.0f} USDT", flush=True)
print(f" 平均盈利: {avg_win:>+12.2f} USDT", flush=True)
print(f" 平均亏损: {avg_loss:>+12.2f} USDT", flush=True)
print(f" 最大单笔盈利: {best:>+12.2f} USDT", flush=True)
print(f" 最大单笔亏损: {worst:>+12.2f} USDT", flush=True)
print(f" 平均持仓: {avg_hold:>8.0f} 秒 ({avg_hold/60:.1f}分钟)", flush=True)
print(f"\n --- 费用明细 ---", flush=True)
print(f" 总交易额: {total_volume:>12,.0f} USDT", flush=True)
per_trade_fee = NOTIONAL * TAKER_FEE * 2
per_trade_reb = per_trade_fee * REBATE_RATE
per_trade_net_fee = per_trade_fee - per_trade_reb
print(f" 每笔手续费: {per_trade_fee:>12.2f} USDT", flush=True)
print(f" 每笔返佣: {per_trade_reb:>+12.2f} USDT", flush=True)
print(f" 每笔净费用: {per_trade_net_fee:>12.2f} USDT", flush=True)
print(f"\n --- 平仓原因 ---", flush=True)
for r, c in sorted(reasons.items(), key=lambda x: -x[1]):
print(f" {r:<16} {c:>5} 笔 ({c/n*100:.1f}%)", flush=True)
# 月度统计
print(f"\n --- 月度明细 ---", flush=True)
print(f" {'月份':<8} {'笔数':>5} {'方向盈亏':>10} {'返佣':>10} {'净利润':>10} {'胜率':>6}", flush=True)
print(f" {'-'*55}", flush=True)
monthly = {}
for t in trades:
k = t[9].strftime('%Y-%m') # 用平仓时间
if k not in monthly:
monthly[k] = {'n': 0, 'pnl': 0, 'reb': 0, 'fee': 0, 'w': 0}
monthly[k]['n'] += 1
monthly[k]['pnl'] += t[3]
monthly[k]['reb'] += t[5]
monthly[k]['fee'] += t[4]
if t[3] > 0: monthly[k]['w'] += 1
for m in sorted(monthly.keys()):
d = monthly[m]
net_m = d['pnl'] - (d['fee'] - d['reb']) # 方向盈亏 - 净手续费
wr_m = d['w'] / d['n'] * 100 if d['n'] > 0 else 0
print(f" {m:<8} {d['n']:>5} {d['pnl']:>+10.2f} {d['reb']:>10.2f} {net_m:>+10.2f} {wr_m:>5.1f}%", flush=True)
# 年度汇总
print(f" {'-'*55}", flush=True)
print(f" {'合计':<8} {n:>5} {total_pnl:>+10.2f} {total_rebate:>10.2f} {net_profit:>+10.2f} {wr:>5.1f}%", flush=True)
print(f"\n{'='*70}", flush=True)
# 保存交易记录
csv_path = Path(__file__).parent.parent / '100u_trades.csv'
with open(csv_path, 'w', encoding='utf-8-sig') as f:
f.write("开仓时间,平仓时间,方向,开仓价,平仓价,名义价值,方向盈亏,手续费,返佣,净盈亏,持仓秒,原因\n")
for t in trades:
net_t = t[3] - (t[4] - t[5])
f.write(f"{t[8]},{t[9]},{t[0]},{t[1]:.2f},{t[2]:.2f},{NOTIONAL:.0f},"
f"{t[3]:.2f},{t[4]:.2f},{t[5]:.2f},{net_t:.2f},{t[6]:.0f},{t[7]}\n")
print(f"\n 交易记录已保存: {csv_path}", flush=True)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,376 @@
"""
AI策略优化 v2 — 目标: 100U保证金达到1000U/月
优化方向:
1. 多时间框架特征: 加入5分钟/15分钟聚合K线指标
2. Ensemble: LightGBM + RandomForest 投票
3. 更长训练窗口: 4个月 vs 3个月
4. 高置信度过滤: 只在双模型一致时交易
5. 动态止盈: 用ATR倍数而非固定比例
6. 更多K线形态特征: 连续涨跌、缺口、波动率变化率
7. 扫描最优参数组合
"""
import datetime, sqlite3, time as _time
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
import warnings
from pathlib import Path
from collections import defaultdict
warnings.filterwarnings('ignore')
def load_data():
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))
df = pd.read_sql_query(
f"SELECT id as ts,open,high,low,close FROM bitmart_eth_1m WHERE id>={s} AND id<{e} ORDER BY id", conn)
conn.close()
df['datetime'] = pd.to_datetime(df['ts'], unit='ms')
df.set_index('datetime', inplace=True)
return df
def add_features(df):
c=df['close']; h=df['high']; l=df['low']; o=df['open']
# === 1分钟基础指标 ===
for p in [5,8,13,21,50,120]:
df[f'ema_{p}'] = c.ewm(span=p, adjust=False).mean()
df['ema_fast_slow'] = (df['ema_8']-df['ema_21'])/c
df['ema_slow_big'] = (df['ema_21']-df['ema_120'])/c
df['price_vs_ema120'] = (c-df['ema_120'])/c
df['price_vs_ema50'] = (c-df['ema_50'])/c
df['ema8_slope'] = df['ema_8'].pct_change(5)
df['ema21_slope'] = df['ema_21'].pct_change(5)
df['ema120_slope'] = df['ema_120'].pct_change(20)
# 三线排列
df['triple_bull'] = ((df['ema_8']>df['ema_21'])&(df['ema_21']>df['ema_120'])).astype(float)
df['triple_bear'] = ((df['ema_8']<df['ema_21'])&(df['ema_21']<df['ema_120'])).astype(float)
# RSI
delta = c.diff(); gain = delta.clip(lower=0); loss = (-delta).clip(lower=0)
for p in [7,14,21]:
ag=gain.rolling(p).mean(); al=loss.rolling(p).mean()
df[f'rsi_{p}'] = 100 - 100/(1+ag/al.replace(0,np.nan))
df['rsi_14_slope'] = df['rsi_14'].diff(5) # RSI变化率
# BB
mid=c.rolling(20).mean(); std=c.rolling(20).std()
df['bb_pct'] = (c-(mid-2*std))/((mid+2*std)-(mid-2*std)).replace(0,np.nan)
df['bb_width'] = 4*std/mid
df['bb_width_change'] = df['bb_width'].pct_change(10) # 波动率变化
# MACD
ema12=c.ewm(span=12,adjust=False).mean(); ema26=c.ewm(span=26,adjust=False).mean()
df['macd'] = (ema12-ema26)/c
df['macd_signal'] = df['macd'].ewm(span=9,adjust=False).mean()
df['macd_hist'] = df['macd']-df['macd_signal']
df['macd_hist_slope'] = df['macd_hist'].diff(3) # MACD柱变化
# ATR
tr = pd.concat([h-l,(h-c.shift(1)).abs(),(l-c.shift(1)).abs()],axis=1).max(axis=1)
df['atr_pct'] = tr.rolling(14).mean()/c
df['atr_7'] = tr.rolling(7).mean()/c
df['atr_ratio'] = df['atr_7']/df['atr_pct'].replace(0,np.nan) # 短期/长期ATR
# Stochastic
for p in [14,28]:
low_p=l.rolling(p).min(); high_p=h.rolling(p).max()
df[f'stoch_k_{p}'] = (c-low_p)/(high_p-low_p).replace(0,np.nan)*100
df['stoch_d_14'] = df['stoch_k_14'].rolling(3).mean()
# 动量
for p in [1,3,5,10,20,60,120]:
df[f'ret_{p}'] = c.pct_change(p)
# 波动率
df['vol_5'] = c.pct_change().rolling(5).std()
df['vol_20'] = c.pct_change().rolling(20).std()
df['vol_60'] = c.pct_change().rolling(60).std()
df['vol_ratio'] = df['vol_5']/df['vol_20'].replace(0,np.nan)
df['vol_trend'] = df['vol_20'].pct_change(20) # 波动率趋势
# K线形态
body = (c-o).abs()
df['body_pct'] = body/c
df['upper_shadow'] = (h-pd.concat([o,c],axis=1).max(axis=1))/c
df['lower_shadow'] = (pd.concat([o,c],axis=1).min(axis=1)-l)/c
df['body_vs_range'] = body/(h-l).replace(0,np.nan)
df['is_bullish'] = (c>o).astype(float)
df['range_pct'] = (h-l)/c # K线振幅
# 连续方向
bullish = (c>o).astype(int)
df['streak'] = bullish.groupby((bullish!=bullish.shift()).cumsum()).cumcount()+1
df['streak'] = df['streak'] * bullish - df['streak'] * (1-bullish) # 正=连阳, 负=连阴
# 吞没/锤子
prev_body = body.shift(1)
df['engulf_ratio'] = body/prev_body.replace(0,np.nan)
df['hammer'] = (df['lower_shadow']>df['body_pct']*2).astype(float)
df['shooting_star'] = (df['upper_shadow']>df['body_pct']*2).astype(float)
# 价格位置
for p in [20,60]:
df[f'high_{p}'] = h.rolling(p).max()
df[f'low_{p}'] = l.rolling(p).min()
df[f'pos_{p}'] = (c-df[f'low_{p}'])/(df[f'high_{p}']-df[f'low_{p}']).replace(0,np.nan)
# === 多时间框架: 5分钟 ===
c5 = c.resample('5min').last()
h5 = h.resample('5min').max()
l5 = l.resample('5min').min()
o5 = o.resample('5min').first()
ema5_8 = c5.ewm(span=8,adjust=False).mean()
ema5_21 = c5.ewm(span=21,adjust=False).mean()
rsi5_14_delta = c5.diff()
rsi5_g = rsi5_14_delta.clip(lower=0).rolling(14).mean()
rsi5_l = (-rsi5_14_delta).clip(lower=0).rolling(14).mean()
rsi5 = 100 - 100/(1+rsi5_g/rsi5_l.replace(0,np.nan))
# 5分钟指标 reindex 到1分钟
df['ema5m_fast_slow'] = ((ema5_8-ema5_21)/c5).reindex(df.index, method='ffill')
df['rsi5m_14'] = rsi5.reindex(df.index, method='ffill')
tr5 = pd.concat([h5-l5,(h5-c5.shift(1)).abs(),(l5-c5.shift(1)).abs()],axis=1).max(axis=1)
df['atr5m'] = (tr5.rolling(14).mean()/c5).reindex(df.index, method='ffill')
df['ret5m_1'] = c5.pct_change(1).reindex(df.index, method='ffill')
df['ret5m_5'] = c5.pct_change(5).reindex(df.index, method='ffill')
df['ret5m_20'] = c5.pct_change(20).reindex(df.index, method='ffill')
# === 多时间框架: 15分钟 ===
c15 = c.resample('15min').last()
ema15_21 = c15.ewm(span=21,adjust=False).mean()
df['ema15m_trend'] = ((c15-ema15_21)/c15).reindex(df.index, method='ffill')
df['ret15m_5'] = c15.pct_change(5).reindex(df.index, method='ffill')
# 时间
df['hour'] = df.index.hour
df['minute'] = df.index.minute
df['hour_sin'] = np.sin(2*np.pi*df['hour']/24)
df['hour_cos'] = np.cos(2*np.pi*df['hour']/24)
df['weekday'] = df.index.weekday
return df
def get_feature_cols(df):
exclude = {'ts','open','high','low','close','label','month',
'ema_5','ema_8','ema_13','ema_21','ema_50','ema_120',
'high_20','low_20','high_60','low_60'}
return [c for c in df.columns if c not in exclude
and df[c].dtype in ('float64','float32','int64','int32')]
def train_ensemble(X_tr, y_tr, X_te, fcols):
"""训练 LightGBM + GradientBoosting ensemble"""
y_cls = y_tr + 1 # -1→0, 0→1, 1→2
# Model 1: LightGBM
params = {
'objective':'multiclass','num_class':3,'metric':'multi_logloss',
'learning_rate':0.03,'num_leaves':63,'max_depth':8,
'min_child_samples':100,'subsample':0.7,'colsample_bytree':0.7,
'reg_alpha':0.5,'reg_lambda':0.5,'verbose':-1,'n_jobs':-1,'seed':42
}
dt_ = lgb.Dataset(X_tr, label=y_cls)
m1 = lgb.train(params, dt_, num_boost_round=300)
p1 = m1.predict(X_te) # (n, 3)
# Model 2: GradientBoosting (sklearn)
m2 = GradientBoostingClassifier(
n_estimators=150, max_depth=5, learning_rate=0.05,
subsample=0.8, min_samples_leaf=50, random_state=42
)
m2.fit(X_tr, y_cls)
p2 = m2.predict_proba(X_te) # (n, 3)
# Ensemble: 加权平均 (LightGBM权重更高)
proba = p1 * 0.6 + p2 * 0.4
return proba, m1
def backtest(df, pl, ps, notional, prob_th, sl_pct, tp_pct, max_hold, use_atr_tp=False):
FEE = notional*0.0006*2; REB=FEE*0.9; NFEE=FEE-REB
pos=0; op=0.0; ot=None; trades=[]; atr_at_open=0
for i in range(len(df)):
dt=df.index[i]; p=df['close'].iloc[i]; p_l=pl.iloc[i]; p_s=ps.iloc[i]
atr_val = df['atr_pct'].iloc[i] if 'atr_pct' in df.columns else 0.002
if pos!=0 and ot is not None:
pp=(p-op)/op if pos==1 else (op-p)/op
hsec=(dt-ot).total_seconds()
# 动态止盈ATR倍数
if use_atr_tp and atr_at_open > 0:
dyn_tp = atr_at_open * 2.5 # 2.5倍ATR止盈
dyn_tp = max(dyn_tp, tp_pct) # 不低于固定TP
else:
dyn_tp = tp_pct
hard_sl = max(sl_pct*1.5, 0.006)
if -pp>=hard_sl:
trades.append((pos,op,p,notional*pp,hsec,'硬止损',ot,dt)); pos=0; continue
if hsec>=200:
if -pp>=sl_pct:
trades.append((pos,op,p,notional*pp,hsec,'止损',ot,dt)); pos=0; continue
if pp>=dyn_tp:
trades.append((pos,op,p,notional*pp,hsec,'止盈',ot,dt)); pos=0; continue
if hsec>=max_hold:
trades.append((pos,op,p,notional*pp,hsec,'超时',ot,dt)); pos=0; continue
# 模型反转
if pos==1 and p_s>prob_th+0.08:
trades.append((pos,op,p,notional*pp,hsec,'AI反转',ot,dt)); pos=0
elif pos==-1 and p_l>prob_th+0.08:
trades.append((pos,op,p,notional*pp,hsec,'AI反转',ot,dt)); pos=0
if pos==0:
if p_l>prob_th and p_l>p_s+0.03: # 要求概率差距>3%
pos=1; op=p; ot=dt; atr_at_open=atr_val
elif p_s>prob_th and p_s>p_l+0.03:
pos=-1; op=p; ot=dt; atr_at_open=atr_val
if pos!=0:
p=df['close'].iloc[-1]; dt=df.index[-1]
pp=(p-op)/op if pos==1 else (op-p)/op
trades.append((pos,op,p,notional*pp,(dt-ot).total_seconds(),'end',ot,dt))
return trades
def analyze(trades, notional, label):
if not trades: print(f" [{label}] No trades"); return 0, {}
n=len(trades)
FEE=notional*0.0006*2; REB=FEE*0.9; NFEE=FEE-REB
tpnl=sum(t[3] for t in trades); net=tpnl-NFEE*n; treb=REB*n
wins=len([t for t in trades if t[3]>0]); wr=wins/n*100 if n else 0
monthly=defaultdict(lambda:{'n':0,'net':0,'w':0})
for t in trades:
k=t[7].strftime('%Y-%m')
monthly[k]['n']+=1; monthly[k]['net']+=t[3]-NFEE
if t[3]>0: monthly[k]['w']+=1
cum=0;peak=0;dd=0
for t in trades:
cum+=t[3]-NFEE
if cum>peak:peak=cum
if peak-cum>dd:dd=peak-cum
pm=len([m for m in monthly.values() if m['net']>0])
min_m=min(monthly.values(),key=lambda x:x['net'])['net'] if monthly else 0
max_m=max(monthly.values(),key=lambda x:x['net'])['net'] if monthly else 0
return net, {'n':n,'wr':wr,'pm':pm,'dd':dd,'treb':treb,'min_m':min_m,'max_m':max_m,'monthly':monthly}
def main():
t0 = _time.time()
print("="*70, flush=True)
print(" AI策略优化 v2 — Ensemble + 多时间框架 + 60+特征", flush=True)
print(" 100U保证金 × 100倍 = 10,000U名义", flush=True)
print("="*70, flush=True)
df = load_data()
print(f" {len(df):,} bars", flush=True)
df = add_features(df)
fcols = get_feature_cols(df)
print(f" {len(fcols)} features", flush=True)
NOTIONAL = 10000.0
# 测试多种配置
configs = [
# (fb, thresh, prob_th, sl, tp, max_hold, use_atr_tp, train_m, label)
(10, 0.003, 0.45, 0.005, 0.008, 1800, False, 3, "v1: 基线(上轮最佳)"),
(10, 0.003, 0.48, 0.005, 0.008, 1800, False, 3, "v2: 高置信0.48"),
(10, 0.003, 0.50, 0.005, 0.010, 2400, False, 3, "v3: 超高置信0.50 大TP"),
(10, 0.003, 0.45, 0.005, 0.010, 2400, True, 3, "v4: ATR动态止盈"),
(10, 0.003, 0.48, 0.006, 0.010, 2400, True, 3, "v5: 高置信+ATR+宽SL"),
(15, 0.004, 0.45, 0.006, 0.010, 2400, True, 3, "v6: 15bar前瞻 大波动"),
(10, 0.003, 0.45, 0.005, 0.008, 1800, False, 4, "v7: 4月训练窗口"),
(10, 0.003, 0.48, 0.005, 0.010, 2400, True, 4, "v8: 4月+高置信+ATR"),
]
results = []
for fb, thresh, prob_th, sl, tp, mh, use_atr, train_m, label in configs:
print(f"\n--- {label} ---", flush=True)
print(f" 前瞻={fb} 阈值={thresh*100:.1f}% prob>{prob_th} SL={sl*100:.1f}% TP={tp*100:.1f}% MH={mh}s ATR_TP={use_atr} train={train_m}m", flush=True)
# 标签
df_t = df.copy()
future_ret = df_t['close'].shift(-fb)/df_t['close'] - 1
df_t['label'] = 0
df_t.loc[future_ret > thresh, 'label'] = 1
df_t.loc[future_ret < -thresh, 'label'] = -1
df_t['month'] = df_t.index.to_period('M')
months = sorted(df_t['month'].unique())
pl = pd.Series(index=df_t.index, dtype=float); pl[:] = 0.0
ps = pd.Series(index=df_t.index, dtype=float); ps[:] = 0.0
for mi in range(train_m, len(months)):
tm = months[mi]; ts_ = months[mi-train_m]
tr_mask = (df_t['month']>=ts_) & (df_t['month']<tm)
te_mask = df_t['month']==tm
tr_df = df_t[tr_mask].dropna(subset=fcols+['label'])
te_df = df_t[te_mask].dropna(subset=fcols)
if len(tr_df)<1000 or len(te_df)<100: continue
proba, _ = train_ensemble(tr_df[fcols].values, tr_df['label'].values, te_df[fcols].values, fcols)
pl.loc[te_df.index] = proba[:,2]
ps.loc[te_df.index] = proba[:,0]
# 回测
trades = backtest(df_t, pl, ps, NOTIONAL, prob_th, sl, tp, mh, use_atr)
net, info = analyze(trades, NOTIONAL, label)
if info:
print(f" 净利={net:+.0f} 交易={info['n']} 胜率={info['wr']:.1f}% 盈利月={info['pm']}/12 回撤={info['dd']:.0f}", flush=True)
# 月度简览
for m in sorted(info['monthly'].keys()):
d = info['monthly'][m]
s = "+" if d['net']>0 else "-"
print(f" {m}: {d['net']:>+6.0f} ({d['n']}笔) {s}", flush=True)
results.append((label, net, info))
# === 总览 ===
elapsed = _time.time()-t0
results.sort(key=lambda x: x[1], reverse=True)
print(f"\n\n{'='*80}", flush=True)
print(f" 总览 | 100U保证金 × 100倍 | 耗时 {elapsed:.0f}s", flush=True)
print(f"{'='*80}", flush=True)
print(f" {'方案':<30} {'年净利':>8} {'月均':>6} {'交易':>5} {'胜率':>5} {'盈月':>4} {'回撤':>6}", flush=True)
print(f" {'-'*72}", flush=True)
for label, net, info in results:
if not info: continue
mavg = net/12
print(f" {label:<30} {net:>+8.0f} {mavg:>+6.0f} {info['n']:>5} {info['wr']:>4.1f}% {info['pm']:>2}/12 {info['dd']:>6.0f}", flush=True)
best = results[0]
print(f"\n 最佳: {best[0]}", flush=True)
print(f" 年净利: {best[1]:+.0f} USDT = 月均 {best[1]/12:+.0f} USDT", flush=True)
if best[2]:
print(f"\n 最佳方案月度:", flush=True)
for m in sorted(best[2]['monthly'].keys()):
d = best[2]['monthly'][m]
wr_m = d['w']/d['n']*100 if d['n']>0 else 0
print(f" {m}: {d['n']:>4}{d['net']:>+8.0f}U [{('盈利' if d['net']>0 else '亏损')}]", flush=True)
print(f"\n 对比基线(LightGBM v1): +4801/年 = +400/月", flush=True)
if best[1] > 4801:
print(f" 优化提升: {(best[1]/4801-1)*100:+.0f}%", flush=True)
print(f"{'='*80}", flush=True)
# 保存最佳交易
if best[2]:
# 重跑最佳配置保存CSV
csv = Path(__file__).parent.parent / 'ai_v2_best.csv'
print(f" Results saved summary to console.", flush=True)
if __name__=='__main__':
main()

View File

@@ -0,0 +1,316 @@
"""
AI策略快速优化 — 只用LightGBM多时间框架特征扫描参数
优化点 vs v1:
1. 63个特征(加5m/15m多时间框架)
2. 更强LightGBM参数(更多树+更深)
3. 扫描: 概率阈值/止损/止盈/前瞻期/持仓时间
4. 要求多空概率差距>3%才开仓(减少弱信号)
5. 动态ATR止盈选项
固定: 100U保证金, 100x杠杆, 10,000U名义, 90%返佣
"""
import datetime, sqlite3, time as _time
import numpy as np
import pandas as pd
import lightgbm as lgb
import warnings
from pathlib import Path
from collections import defaultdict
warnings.filterwarnings('ignore')
def load_data():
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))
df = pd.read_sql_query(
f"SELECT id as ts,open,high,low,close FROM bitmart_eth_1m WHERE id>={s} AND id<{e} ORDER BY id", conn)
conn.close()
df['datetime'] = pd.to_datetime(df['ts'], unit='ms')
df.set_index('datetime', inplace=True)
return df
def add_features(df):
c=df['close']; h=df['high']; l=df['low']; o=df['open']
for p in [5,8,13,21,50,120]:
df[f'ema_{p}'] = c.ewm(span=p, adjust=False).mean()
df['ema_fast_slow'] = (df['ema_8']-df['ema_21'])/c
df['ema_slow_big'] = (df['ema_21']-df['ema_120'])/c
df['price_vs_ema120'] = (c-df['ema_120'])/c
df['price_vs_ema50'] = (c-df['ema_50'])/c
df['ema8_slope'] = df['ema_8'].pct_change(5)
df['ema21_slope'] = df['ema_21'].pct_change(5)
df['ema120_slope'] = df['ema_120'].pct_change(20)
df['triple_bull'] = ((df['ema_8']>df['ema_21'])&(df['ema_21']>df['ema_120'])).astype(float)
df['triple_bear'] = ((df['ema_8']<df['ema_21'])&(df['ema_21']<df['ema_120'])).astype(float)
delta = c.diff(); gain = delta.clip(lower=0); loss = (-delta).clip(lower=0)
for p in [7,14,21]:
ag=gain.rolling(p).mean(); al=loss.rolling(p).mean()
df[f'rsi_{p}'] = 100 - 100/(1+ag/al.replace(0,np.nan))
df['rsi_14_slope'] = df['rsi_14'].diff(5)
mid=c.rolling(20).mean(); std=c.rolling(20).std()
df['bb_pct'] = (c-(mid-2*std))/((mid+2*std)-(mid-2*std)).replace(0,np.nan)
df['bb_width'] = 4*std/mid
df['bb_width_chg'] = df['bb_width'].pct_change(10)
ema12=c.ewm(span=12,adjust=False).mean(); ema26=c.ewm(span=26,adjust=False).mean()
df['macd'] = (ema12-ema26)/c
df['macd_signal'] = df['macd'].ewm(span=9,adjust=False).mean()
df['macd_hist'] = df['macd']-df['macd_signal']
df['macd_hist_slope'] = df['macd_hist'].diff(3)
tr = pd.concat([h-l,(h-c.shift(1)).abs(),(l-c.shift(1)).abs()],axis=1).max(axis=1)
df['atr_pct'] = tr.rolling(14).mean()/c
df['atr_7'] = tr.rolling(7).mean()/c
df['atr_ratio'] = df['atr_7']/df['atr_pct'].replace(0,np.nan)
for p in [14,28]:
low_p=l.rolling(p).min(); high_p=h.rolling(p).max()
df[f'stoch_k_{p}'] = (c-low_p)/(high_p-low_p).replace(0,np.nan)*100
df['stoch_d_14'] = df['stoch_k_14'].rolling(3).mean()
for p in [1,3,5,10,20,60,120]:
df[f'ret_{p}'] = c.pct_change(p)
df['vol_5'] = c.pct_change().rolling(5).std()
df['vol_20'] = c.pct_change().rolling(20).std()
df['vol_60'] = c.pct_change().rolling(60).std()
df['vol_ratio'] = df['vol_5']/df['vol_20'].replace(0,np.nan)
df['vol_trend'] = df['vol_20'].pct_change(20)
body = (c-o).abs()
df['body_pct'] = body/c
df['upper_shadow'] = (h-pd.concat([o,c],axis=1).max(axis=1))/c
df['lower_shadow'] = (pd.concat([o,c],axis=1).min(axis=1)-l)/c
df['body_vs_range'] = body/(h-l).replace(0,np.nan)
df['range_pct'] = (h-l)/c
bullish = (c>o).astype(int)
df['streak'] = bullish.groupby((bullish!=bullish.shift()).cumsum()).cumcount()+1
df['streak'] = df['streak'] * bullish - df['streak'] * (1-bullish)
df['engulf_ratio'] = body/body.shift(1).replace(0,np.nan)
for p in [20,60]:
df[f'high_{p}'] = h.rolling(p).max()
df[f'low_{p}'] = l.rolling(p).min()
df[f'pos_{p}'] = (c-df[f'low_{p}'])/(df[f'high_{p}']-df[f'low_{p}']).replace(0,np.nan)
# 5分钟
c5=c.resample('5min').last(); h5=h.resample('5min').max()
l5=l.resample('5min').min(); o5=o.resample('5min').first()
e5_8=c5.ewm(span=8,adjust=False).mean(); e5_21=c5.ewm(span=21,adjust=False).mean()
df['ema5m_fs'] = ((e5_8-e5_21)/c5).reindex(df.index, method='ffill')
d5=c5.diff(); g5=d5.clip(lower=0).rolling(14).mean(); l5r=(-d5).clip(lower=0).rolling(14).mean()
df['rsi5m'] = (100-100/(1+g5/l5r.replace(0,np.nan))).reindex(df.index, method='ffill')
tr5=pd.concat([h5-l5,(h5-c5.shift(1)).abs(),(l5-c5.shift(1)).abs()],axis=1).max(axis=1)
df['atr5m'] = (tr5.rolling(14).mean()/c5).reindex(df.index, method='ffill')
for p in [1,5,20]:
df[f'ret5m_{p}'] = c5.pct_change(p).reindex(df.index, method='ffill')
# 15分钟
c15=c.resample('15min').last()
e15=c15.ewm(span=21,adjust=False).mean()
df['ema15m_trend'] = ((c15-e15)/c15).reindex(df.index, method='ffill')
df['ret15m_5'] = c15.pct_change(5).reindex(df.index, method='ffill')
df['hour'] = df.index.hour; df['minute'] = df.index.minute
df['hour_sin'] = np.sin(2*np.pi*df['hour']/24)
df['hour_cos'] = np.cos(2*np.pi*df['hour']/24)
df['weekday'] = df.index.weekday
return df
def get_fcols(df):
exclude = {'ts','open','high','low','close','label','month',
'ema_5','ema_8','ema_13','ema_21','ema_50','ema_120',
'high_20','low_20','high_60','low_60'}
return [c for c in df.columns if c not in exclude
and df[c].dtype in ('float64','float32','int64','int32')]
def train_predict(df, fcols, fb, thresh, train_m=3):
future_ret = df['close'].shift(-fb)/df['close'] - 1
df['label'] = 0
df.loc[future_ret > thresh, 'label'] = 1
df.loc[future_ret < -thresh, 'label'] = -1
df['month'] = df.index.to_period('M')
months = sorted(df['month'].unique())
pl = pd.Series(index=df.index, dtype=float); pl[:] = 0.0
ps = pd.Series(index=df.index, dtype=float); ps[:] = 0.0
params = {
'objective':'multiclass','num_class':3,'metric':'multi_logloss',
'learning_rate':0.03,'num_leaves':63,'max_depth':8,
'min_child_samples':80,'subsample':0.7,'colsample_bytree':0.7,
'reg_alpha':0.3,'reg_lambda':0.3,'verbose':-1,'n_jobs':-1,'seed':42
}
for i in range(train_m, len(months)):
tm = months[i]; ts_ = months[i-train_m]
tr_mask = (df['month']>=ts_)&(df['month']<tm)
te_mask = df['month']==tm
tr_df = df[tr_mask].dropna(subset=fcols+['label'])
te_df = df[te_mask].dropna(subset=fcols)
if len(tr_df)<1000 or len(te_df)<100: continue
dt_ = lgb.Dataset(tr_df[fcols].values, label=tr_df['label'].values+1)
model = lgb.train(params, dt_, num_boost_round=300)
proba = model.predict(te_df[fcols].values)
pl.loc[te_df.index] = proba[:,2]
ps.loc[te_df.index] = proba[:,0]
return pl, ps
def backtest(df, pl, ps, prob_th, sl, tp, mh, gap=0.03):
NOTIONAL=10000.0; FEE=NOTIONAL*0.0006*2; REB=FEE*0.9; NFEE=FEE-REB
pos=0; op=0.0; ot=None; trades=[]
for i in range(len(df)):
dt=df.index[i]; p=df['close'].iloc[i]; p_l=pl.iloc[i]; p_s=ps.iloc[i]
if pos!=0 and ot is not None:
pp=(p-op)/op if pos==1 else (op-p)/op
hsec=(dt-ot).total_seconds()
hard_sl=max(sl*1.5,0.006)
if -pp>=hard_sl: trades.append((pos,op,p,NOTIONAL*pp,hsec,'hsl',ot,dt));pos=0;continue
if hsec>=200:
if -pp>=sl: trades.append((pos,op,p,NOTIONAL*pp,hsec,'sl',ot,dt));pos=0;continue
if pp>=tp: trades.append((pos,op,p,NOTIONAL*pp,hsec,'tp',ot,dt));pos=0;continue
if hsec>=mh: trades.append((pos,op,p,NOTIONAL*pp,hsec,'to',ot,dt));pos=0;continue
if pos==1 and p_s>prob_th+0.08:
trades.append((pos,op,p,NOTIONAL*pp,hsec,'ai',ot,dt));pos=0
elif pos==-1 and p_l>prob_th+0.08:
trades.append((pos,op,p,NOTIONAL*pp,hsec,'ai',ot,dt));pos=0
if pos==0:
if p_l>prob_th and p_l>p_s+gap: pos=1;op=p;ot=dt
elif p_s>prob_th and p_s>p_l+gap: pos=-1;op=p;ot=dt
if pos!=0:
p=df['close'].iloc[-1];dt=df.index[-1]
pp=(p-op)/op if pos==1 else (op-p)/op
trades.append((pos,op,p,NOTIONAL*pp,(dt-ot).total_seconds(),'end',ot,dt))
return trades
def score(trades):
if not trades: return 0, 0, 0, 0, {}
NFEE=1.2; n=len(trades)
tpnl=sum(t[3] for t in trades); net=tpnl-NFEE*n
wins=len([t for t in trades if t[3]>0]); wr=wins/n*100
cum=0;peak=0;dd=0
monthly=defaultdict(lambda:{'n':0,'net':0,'w':0})
for t in trades:
cum+=t[3]-NFEE
if cum>peak:peak=cum
if peak-cum>dd:dd=peak-cum
k=t[7].strftime('%Y-%m')
monthly[k]['n']+=1;monthly[k]['net']+=t[3]-NFEE
if t[3]>0:monthly[k]['w']+=1
pm=len([m for m in monthly.values() if m['net']>0])
return net, wr, pm, dd, monthly
def main():
t0=_time.time()
print("="*70, flush=True)
print(" AI快速优化 | 100U x 100倍 | LightGBM + 63特征", flush=True)
print("="*70, flush=True)
df = load_data()
df = add_features(df)
fcols = get_fcols(df)
print(f" {len(df):,} bars, {len(fcols)} features\n", flush=True)
# 预训练不同前瞻/阈值的模型(最耗时的部分)
model_configs = [
(10, 0.003, 3, "10bar/0.3%/3m"),
(10, 0.003, 4, "10bar/0.3%/4m"),
(10, 0.004, 3, "10bar/0.4%/3m"),
(15, 0.004, 3, "15bar/0.4%/3m"),
(20, 0.005, 3, "20bar/0.5%/3m"),
]
predictions = {}
for fb, thresh, tm, lbl in model_configs:
print(f" Training: {lbl}...", flush=True)
dfc = df.copy()
pl, ps = train_predict(dfc, fcols, fb, thresh, tm)
predictions[lbl] = (pl, ps)
# 快速检查
t_ = backtest(dfc, pl, ps, 0.45, 0.005, 0.008, 1800)
n_, _, _, _, _ = score(t_)
print(f" quick check: {len(t_)} trades, net={n_:+.0f}", flush=True)
# 扫描回测参数
print(f"\n Scanning backtest params...\n", flush=True)
bt_configs = [
# prob_th, sl, tp, max_hold, gap
(0.42, 0.005, 0.008, 1800, 0.02),
(0.45, 0.005, 0.008, 1800, 0.03),
(0.48, 0.005, 0.008, 1800, 0.03),
(0.45, 0.005, 0.010, 2400, 0.03),
(0.48, 0.005, 0.010, 2400, 0.03),
(0.50, 0.005, 0.010, 2400, 0.03),
(0.45, 0.006, 0.012, 2400, 0.03),
(0.48, 0.006, 0.012, 3600, 0.03),
(0.50, 0.006, 0.015, 3600, 0.03),
(0.45, 0.004, 0.008, 1800, 0.03),
(0.42, 0.004, 0.006, 1200, 0.02),
]
results = []
for mlbl, (pl, ps) in predictions.items():
for prob_th, sl, tp, mh, gap in bt_configs:
trades = backtest(df, pl, ps, prob_th, sl, tp, mh, gap)
net, wr, pm, dd, monthly = score(trades)
n = len(trades)
if n > 0:
results.append({
'model': mlbl, 'prob': prob_th, 'sl': sl, 'tp': tp,
'mh': mh, 'gap': gap, 'n': n, 'net': net,
'wr': wr, 'pm': pm, 'dd': dd, 'monthly': monthly
})
# 按净利排序
results.sort(key=lambda x: x['net'], reverse=True)
# 打印Top 15
print(f"{'='*100}", flush=True)
print(f" TOP 15 配置 (100U保证金 x 100倍)", flush=True)
print(f"{'='*100}", flush=True)
print(f" {'#':>2} {'模型':<18} {'概率':>4} {'SL':>4} {'TP':>4} {'MH':>5} {'gap':>4} {'交易':>5} {'年净利':>8} {'月均':>6} {'胜率':>5} {'盈月':>4} {'回撤':>6}", flush=True)
print(f" {'-'*95}", flush=True)
for i, r in enumerate(results[:15]):
mavg = r['net']/12
print(f" {i+1:>2} {r['model']:<18} {r['prob']:.2f} {r['sl']*100:.1f}% {r['tp']*100:.1f}% {r['mh']:>5} {r['gap']:.2f} {r['n']:>5} {r['net']:>+8.0f} {mavg:>+6.0f} {r['wr']:>4.1f}% {r['pm']:>2}/12 {r['dd']:>6.0f}", flush=True)
# 最佳方案详情
best = results[0]
print(f"\n{'='*70}", flush=True)
print(f" 最佳方案: {best['model']}", flush=True)
print(f" 概率>{best['prob']} SL={best['sl']*100:.1f}% TP={best['tp']*100:.1f}% MH={best['mh']}s gap={best['gap']}", flush=True)
print(f" 年净利: {best['net']:+.0f} USDT = 月均 {best['net']/12:+.0f} USDT", flush=True)
print(f" 交易: {best['n']}笔 | 胜率: {best['wr']:.1f}% | 盈利月: {best['pm']}/12 | 回撤: {best['dd']:.0f}", flush=True)
print(f"{'='*70}", flush=True)
print(f"\n 月度明细:", flush=True)
for m in sorted(best['monthly'].keys()):
d = best['monthly'][m]
wr_m = d['w']/d['n']*100 if d['n']>0 else 0
s = "盈利" if d['net']>0 else "亏损"
print(f" {m}: {d['n']:>4}{d['net']:>+8.0f}U 胜率{wr_m:.0f}% [{s}]", flush=True)
# 对比
print(f"\n 对比:", flush=True)
print(f" 纯EMA策略: +1196/年 = +100/月 (227笔)", flush=True)
print(f" AI v1基线: +4801/年 = +400/月 (923笔)", flush=True)
print(f" AI v2优化: {best['net']:+.0f}/年 = {best['net']/12:+.0f}/月 ({best['n']}笔)", flush=True)
if best['net'] > 4801:
print(f" v2 vs v1提升: {(best['net']/4801-1)*100:+.0f}%", flush=True)
elapsed = _time.time()-t0
print(f"\n 耗时: {elapsed:.0f}s", flush=True)
print(f"{'='*70}", flush=True)
if __name__=='__main__':
main()

View File

@@ -0,0 +1,258 @@
"""
AI最佳配置回测 — 基于之前扫描结果
最佳: AI-v4: 10bar前瞻, 方向阈值0.3%, 概率阈值0.45, SL=0.5%, TP=0.8%
该配置在100U时年净利+5544, 月均+462, 935笔交易, 8/12月盈利
"""
import datetime, sqlite3, time as _time
import numpy as np
import pandas as pd
import lightgbm as lgb
import warnings
from pathlib import Path
from collections import defaultdict
warnings.filterwarnings('ignore')
def main():
t0 = _time.time()
print("Loading...", flush=True)
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))
df = pd.read_sql_query(
f"SELECT id as ts,open,high,low,close FROM bitmart_eth_1m WHERE id>={s} AND id<{e} ORDER BY id", conn)
conn.close()
df['datetime'] = pd.to_datetime(df['ts'], unit='ms')
df.set_index('datetime', inplace=True)
print(f" {len(df):,} bars", flush=True)
# ===== 特征 =====
print("Features...", flush=True)
c=df['close']; h=df['high']; l=df['low']; o=df['open']
for p in [5,8,13,21,50,120]:
df[f'ema_{p}'] = c.ewm(span=p, adjust=False).mean()
df['ema_fast_slow'] = (df['ema_8']-df['ema_21'])/c
df['ema_slow_big'] = (df['ema_21']-df['ema_120'])/c
df['price_vs_ema120'] = (c-df['ema_120'])/c
df['price_vs_ema50'] = (c-df['ema_50'])/c
df['ema8_slope'] = df['ema_8'].pct_change(5)
df['ema21_slope'] = df['ema_21'].pct_change(5)
delta = c.diff()
gain = delta.clip(lower=0); loss = (-delta).clip(lower=0)
for p in [7,14,21]:
ag=gain.rolling(p).mean(); al=loss.rolling(p).mean()
df[f'rsi_{p}'] = 100 - 100/(1+ag/al.replace(0,np.nan))
mid=c.rolling(20).mean(); std=c.rolling(20).std()
df['bb_pct'] = (c-(mid-2*std))/((mid+2*std)-(mid-2*std)).replace(0,np.nan)
df['bb_width'] = 4*std/mid
ema12=c.ewm(span=12,adjust=False).mean(); ema26=c.ewm(span=26,adjust=False).mean()
df['macd'] = (ema12-ema26)/c
df['macd_signal'] = df['macd'].ewm(span=9,adjust=False).mean()
df['macd_hist'] = df['macd']-df['macd_signal']
tr = pd.concat([h-l,(h-c.shift(1)).abs(),(l-c.shift(1)).abs()],axis=1).max(axis=1)
df['atr_pct'] = tr.rolling(14).mean()/c
df['atr_7'] = tr.rolling(7).mean()/c
low14=l.rolling(14).min(); high14=h.rolling(14).max()
df['stoch_k'] = (c-low14)/(high14-low14).replace(0,np.nan)*100
df['stoch_d'] = df['stoch_k'].rolling(3).mean()
for p in [1,3,5,10,20,60]:
df[f'ret_{p}'] = c.pct_change(p)
df['vol_5'] = c.pct_change().rolling(5).std()
df['vol_20'] = c.pct_change().rolling(20).std()
df['vol_ratio'] = df['vol_5']/df['vol_20'].replace(0,np.nan)
body = (c-o).abs()
df['body_pct'] = body/c
df['upper_shadow'] = (h-pd.concat([o,c],axis=1).max(axis=1))/c
df['lower_shadow'] = (pd.concat([o,c],axis=1).min(axis=1)-l)/c
df['body_vs_range'] = body/(h-l).replace(0,np.nan)
df['is_bullish'] = (c>o).astype(float)
df['high_20'] = h.rolling(20).max()
df['low_20'] = l.rolling(20).min()
df['price_position'] = (c-df['low_20'])/(df['high_20']-df['low_20']).replace(0,np.nan)
df['hour'] = df.index.hour
df['minute'] = df.index.minute
df['hour_sin'] = np.sin(2*np.pi*df['hour']/24)
df['hour_cos'] = np.cos(2*np.pi*df['hour']/24)
prev_body = body.shift(1)
df['engulf_ratio'] = body/prev_body.replace(0,np.nan)
exclude = {'ts','open','high','low','close','label',
'high_20','low_20','ema_5','ema_8','ema_13','ema_21','ema_50','ema_120'}
fcols = [c_ for c_ in df.columns if c_ not in exclude
and df[c_].dtype in ('float64','float32','int64','int32')]
print(f" {len(fcols)} features", flush=True)
# ===== 标签: 10bar前瞻, 0.3%阈值 =====
fb = 10; thresh = 0.003
future_ret = df['close'].shift(-fb)/df['close'] - 1
df['label'] = 0
df.loc[future_ret > thresh, 'label'] = 1
df.loc[future_ret < -thresh, 'label'] = -1
# ===== 滚动训练 =====
print("Walk-forward training...", flush=True)
df['month'] = df.index.to_period('M')
months = sorted(df['month'].unique())
pl = pd.Series(index=df.index, dtype=float); pl[:] = 0.0
ps = pd.Series(index=df.index, dtype=float); ps[:] = 0.0
params = {
'objective':'multiclass','num_class':3,'metric':'multi_logloss',
'learning_rate':0.05,'num_leaves':31,'max_depth':6,
'min_child_samples':50,'subsample':0.8,'colsample_bytree':0.8,
'reg_alpha':0.1,'reg_lambda':0.1,'verbose':-1,'n_jobs':-1,'seed':42
}
for i in range(3, len(months)):
tm = months[i]; ts_ = months[i-3]
tr_mask = (df['month']>=ts_) & (df['month']<tm)
te_mask = df['month']==tm
tr_df = df[tr_mask].dropna(subset=fcols+['label'])
te_df = df[te_mask].dropna(subset=fcols)
if len(tr_df)<1000 or len(te_df)<100: continue
X_tr = tr_df[fcols].values; y_tr = tr_df['label'].values + 1
dt_ = lgb.Dataset(X_tr, label=y_tr)
model = lgb.train(params, dt_, num_boost_round=200)
proba = model.predict(te_df[fcols].values)
pl.loc[te_df.index] = proba[:,2]
ps.loc[te_df.index] = proba[:,0]
lc = (proba[:,2]>0.45).sum(); sc = (proba[:,0]>0.45).sum()
print(f" {tm}: long={lc} short={sc}", flush=True)
# ===== 回测 =====
print("\nBacktest...", flush=True)
NOTIONAL = 10000.0
FEE = NOTIONAL*0.0006*2; REB = FEE*0.9; NFEE = FEE-REB
prob_th = 0.45; sl_pct = 0.005; tp_pct = 0.008
pos=0; op=0.0; ot=None; trades=[]
for i in range(len(df)):
dt=df.index[i]; p=df['close'].iloc[i]; p_l=pl.iloc[i]; p_s=ps.iloc[i]
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>=sl_pct*1.5:
trades.append((pos,op,p,NOTIONAL*pp,hsec,'hard_sl',ot,dt)); pos=0; continue
if hsec>=200:
if -pp>=sl_pct:
trades.append((pos,op,p,NOTIONAL*pp,hsec,'sl',ot,dt)); pos=0; continue
if pp>=tp_pct:
trades.append((pos,op,p,NOTIONAL*pp,hsec,'tp',ot,dt)); pos=0; continue
if hsec>=1800:
trades.append((pos,op,p,NOTIONAL*pp,hsec,'timeout',ot,dt)); pos=0; continue
if pos==1 and p_s>prob_th+0.05:
trades.append((pos,op,p,NOTIONAL*pp,hsec,'ai_rev',ot,dt)); pos=0
elif pos==-1 and p_l>prob_th+0.05:
trades.append((pos,op,p,NOTIONAL*pp,hsec,'ai_rev',ot,dt)); pos=0
if pos==0:
if p_l>prob_th and p_l>p_s: pos=1; op=p; ot=dt
elif p_s>prob_th and p_s>p_l: pos=-1; op=p; ot=dt
if pos!=0:
p=df['close'].iloc[-1]; dt=df.index[-1]
pp=(p-op)/op if pos==1 else (op-p)/op
trades.append((pos,op,p,NOTIONAL*pp,(dt-ot).total_seconds(),'end',ot,dt))
# ===== 结果 =====
n = len(trades)
tpnl = sum(t[3] for t in trades)
net = tpnl - NFEE*n
treb = REB*n
wins = len([t for t in trades if t[3]>0])
wr = wins/n*100 if n else 0
monthly = defaultdict(lambda: {'n':0,'net':0,'w':0})
for t in trades:
k = t[7].strftime('%Y-%m')
monthly[k]['n'] += 1
monthly[k]['net'] += t[3] - NFEE
if t[3]>0: monthly[k]['w'] += 1
cum=0; peak=0; dd=0
for t in trades:
cum += t[3]-NFEE
if cum>peak: peak=cum
if peak-cum>dd: dd=peak-cum
reasons = defaultdict(int)
for t in trades:
reasons[t[5]] += 1
elapsed = _time.time()-t0
print(f"\n{'='*70}", flush=True)
print(f" AI策略最佳配置 (LightGBM + 42特征)", flush=True)
print(f" 10bar前瞻 | 阈值0.3% | 概率>0.45 | SL=0.5% TP=0.8%", flush=True)
print(f" 100U保证金 x 100倍杠杆 = 10,000U名义 | 耗时{elapsed:.0f}s", flush=True)
print(f"{'='*70}", flush=True)
print(f" 方向盈亏: {tpnl:>+10.0f} USDT", flush=True)
print(f" 返佣(90%): {treb:>+10.0f} USDT", flush=True)
print(f" 净手续费(10%):{NFEE*n:>10.0f} USDT", flush=True)
print(f" ================================", flush=True)
print(f" 年净利: {net:>+10.0f} USDT", flush=True)
print(f" 月均: {net/12:>+10.0f} USDT", flush=True)
print(f" 最大回撤: {dd:>10.0f} USDT", flush=True)
print(f" 交易笔数: {n:>10}", flush=True)
print(f" 胜率: {wr:>9.1f}%", flush=True)
if wins>0 and wins<n:
aw = sum(t[3] for t in trades if t[3]>0)/wins
al = sum(t[3] for t in trades if t[3]<=0)/(n-wins)
print(f" 平均盈利: {aw:>+10.1f} USDT", flush=True)
print(f" 平均亏损: {al:>+10.1f} USDT", flush=True)
print(f" 盈亏比: {abs(aw/al):>10.2f}", flush=True)
print(f"\n 平仓原因:", flush=True)
for r,cnt in sorted(reasons.items(), key=lambda x:-x[1]):
print(f" {r:<10} {cnt:>5}笔 ({cnt/n*100:.1f}%)", flush=True)
print(f"\n 月度明细:", flush=True)
pm = 0
for m in sorted(monthly.keys()):
d = monthly[m]
wr_m = d['w']/d['n']*100 if d['n']>0 else 0
status = "盈利" if d['net']>0 else "亏损"
print(f" {m}: {d['n']:>4}{d['net']:>+8.0f}U 胜率{wr_m:.0f}% [{status}]", flush=True)
if d['net']>0: pm += 1
print(f" 合计: {n:>4}{net:>+8.0f}U 盈利月: {pm}/12", flush=True)
print(f"\n --- 不同保证金下的月均收入 ---", flush=True)
for margin in [100, 200, 300, 500, 800, 1000]:
sc = margin*100/NOTIONAL
mn = net*sc/12
ok = " <<< 达标!" if mn>=1000 else ""
print(f" {margin:>5}U保证金: 月均 {mn:>+6.0f} USDT{ok}", flush=True)
# 对比EMA基线
print(f"\n --- 对比: AI vs 纯EMA策略 ---", flush=True)
ema_net = 1196 # 之前EMA基线100U的年净利
print(f" 纯EMA: {ema_net:>+6.0f}/年 = {ema_net/12:>+4.0f}/月 (227笔)", flush=True)
print(f" AI策略: {net:>+6.0f}/年 = {net/12:>+4.0f}/月 ({n}笔)", flush=True)
if net > ema_net:
print(f" AI提升: {(net/ema_net-1)*100:>+.0f}% ({net-ema_net:>+.0f} USDT)", flush=True)
print(f"\n{'='*70}", flush=True)
# 保存
csv = Path(__file__).parent.parent / 'ai_trades.csv'
with open(csv, 'w', encoding='utf-8-sig') as f:
f.write("dir,open_px,close_px,pnl,hold_sec,reason,open_time,close_time\n")
for t in trades:
d = 'long' if t[0]==1 else 'short'
f.write(f"{d},{t[1]:.2f},{t[2]:.2f},{t[3]:.2f},{t[4]:.0f},{t[5]},{t[6]},{t[7]}\n")
print(f" Saved: {csv}", flush=True)
if __name__=='__main__':
main()

View File

@@ -0,0 +1,442 @@
"""
AI/ML 交易策略回测 — LightGBM + 30+技术指标
核心思路:
1. 用30+种技术指标作为特征EMA/RSI/BB/MACD/ATR/K线形态/动量/波动率等)
2. 标签未来N根K线的收益方向涨>阈值=做多,跌>阈值=做空,否则=不交易)
3. 滚动训练每月用过去3个月数据训练预测下一个月
4. 只在模型高置信度时开仓(概率>阈值)
5. 同一时间只持1个仓
条件: 100U保证金, 100x杠杆, 90%返佣, >3分钟持仓
"""
import datetime
import sqlite3
import time as _time
from pathlib import Path
from collections import defaultdict
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.model_selection import TimeSeriesSplit
import warnings
warnings.filterwarnings('ignore')
# ==================== 数据加载 ====================
def load_data():
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))
df = pd.read_sql_query(
f"SELECT id as ts, open, high, low, close FROM bitmart_eth_1m "
f"WHERE id >= {s} AND id < {e} ORDER BY id", conn)
conn.close()
df['datetime'] = pd.to_datetime(df['ts'], unit='ms')
df.set_index('datetime', inplace=True)
return df
# ==================== 特征工程 ====================
def add_features(df):
"""生成30+技术指标特征"""
c = df['close']; h = df['high']; l = df['low']; o = df['open']
# --- EMA ---
for p in [5, 8, 13, 21, 50, 120]:
df[f'ema_{p}'] = c.ewm(span=p, adjust=False).mean()
# EMA 相对位置
df['ema_fast_slow'] = (df['ema_8'] - df['ema_21']) / c # 快慢线差距
df['ema_slow_big'] = (df['ema_21'] - df['ema_120']) / c
df['price_vs_ema120'] = (c - df['ema_120']) / c
df['price_vs_ema50'] = (c - df['ema_50']) / c
df['ema8_slope'] = df['ema_8'].pct_change(5) # EMA斜率
df['ema21_slope'] = df['ema_21'].pct_change(5)
# --- RSI ---
for p in [7, 14, 21]:
delta = c.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.rolling(p).mean()
avg_loss = loss.rolling(p).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df[f'rsi_{p}'] = 100 - 100 / (1 + rs)
# --- Bollinger Bands ---
for p in [20]:
mid = c.rolling(p).mean()
std = c.rolling(p).std()
df['bb_upper'] = mid + 2 * std
df['bb_lower'] = mid - 2 * std
df['bb_mid'] = mid
df['bb_pct'] = (c - df['bb_lower']) / (df['bb_upper'] - df['bb_lower']).replace(0, np.nan)
df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / mid # 波动率
# --- MACD ---
ema12 = c.ewm(span=12, adjust=False).mean()
ema26 = c.ewm(span=26, adjust=False).mean()
df['macd'] = (ema12 - ema26) / c
df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean()
df['macd_hist'] = df['macd'] - df['macd_signal']
# --- ATR ---
tr = pd.concat([
h - l,
(h - c.shift(1)).abs(),
(l - c.shift(1)).abs()
], axis=1).max(axis=1)
df['atr_14'] = tr.rolling(14).mean()
df['atr_pct'] = df['atr_14'] / c
df['atr_7'] = tr.rolling(7).mean() / c
# --- Stochastic ---
low14 = l.rolling(14).min()
high14 = h.rolling(14).max()
df['stoch_k'] = (c - low14) / (high14 - low14).replace(0, np.nan) * 100
df['stoch_d'] = df['stoch_k'].rolling(3).mean()
# --- 动量 ---
for p in [1, 3, 5, 10, 20, 60]:
df[f'ret_{p}'] = c.pct_change(p) # 过去N根收益率
# --- 波动率 ---
df['vol_5'] = c.pct_change().rolling(5).std()
df['vol_20'] = c.pct_change().rolling(20).std()
df['vol_ratio'] = df['vol_5'] / df['vol_20'].replace(0, np.nan)
# --- K线形态 ---
body = (c - o).abs()
df['body_pct'] = body / c # 实体占比
df['upper_shadow'] = (h - pd.concat([o, c], axis=1).max(axis=1)) / c
df['lower_shadow'] = (pd.concat([o, c], axis=1).min(axis=1) - l) / c
df['body_vs_range'] = body / (h - l).replace(0, np.nan) # 实体/全幅
df['is_bullish'] = (c > o).astype(float)
# 连续同向K线
bullish = (c > o).astype(int)
df['consec_bull'] = bullish.groupby((bullish != bullish.shift()).cumsum()).cumcount() + 1
df['consec_bull'] = df['consec_bull'] * bullish
bearish = (c < o).astype(int)
df['consec_bear'] = bearish.groupby((bearish != bearish.shift()).cumsum()).cumcount() + 1
df['consec_bear'] = df['consec_bear'] * bearish
# 吞没形态
prev_body = body.shift(1)
df['engulf_ratio'] = body / prev_body.replace(0, np.nan)
df['bullish_engulf'] = ((c.shift(1) < o.shift(1)) & (c > o) &
(c > o.shift(1)) & (o <= c.shift(1))).astype(float)
df['bearish_engulf'] = ((c.shift(1) > o.shift(1)) & (c < o) &
(c < o.shift(1)) & (o >= c.shift(1))).astype(float)
# 相对高低位置
df['high_20'] = h.rolling(20).max()
df['low_20'] = l.rolling(20).min()
df['price_position'] = (c - df['low_20']) / (df['high_20'] - df['low_20']).replace(0, np.nan)
# 小时/分钟时间特征
df['hour'] = df.index.hour
df['minute'] = df.index.minute
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
return df
# ==================== 标签生成 ====================
def add_labels(df, forward_bars=10, threshold=0.002):
"""
未来N根K线的收益:
> threshold → 1 (做多机会)
< -threshold → -1 (做空机会)
否则 → 0 (不交易)
"""
future_ret = df['close'].shift(-forward_bars) / df['close'] - 1
df['label'] = 0
df.loc[future_ret > threshold, 'label'] = 1
df.loc[future_ret < -threshold, 'label'] = -1
return df
# ==================== 模型训练 + 预测 ====================
def get_feature_cols(df):
exclude = {'ts', 'open', 'high', 'low', 'close', 'label',
'bb_upper', 'bb_lower', 'bb_mid', 'high_20', 'low_20',
'atr_14', 'ema_5', 'ema_8', 'ema_13', 'ema_21', 'ema_50', 'ema_120'}
return [c for c in df.columns if c not in exclude and df[c].dtype in ('float64','float32','int64','int32')]
def train_predict_walkforward(df, feature_cols, train_months=3):
"""
滚动训练:
用过去 train_months 个月训练 → 预测下一个月
从第4个月开始有预测
"""
df['month'] = df.index.to_period('M')
months = sorted(df['month'].unique())
all_preds = pd.Series(index=df.index, dtype=float)
all_preds[:] = 0.0 # 默认不交易
all_proba_long = pd.Series(index=df.index, dtype=float)
all_proba_short = pd.Series(index=df.index, dtype=float)
all_proba_long[:] = 0.0
all_proba_short[:] = 0.0
print(f"\n Walk-forward training ({len(months)} months, train={train_months}m):", flush=True)
for i in range(train_months, len(months)):
test_month = months[i]
train_start = months[i - train_months]
# 训练数据
train_mask = (df['month'] >= train_start) & (df['month'] < test_month)
test_mask = df['month'] == test_month
train_df = df[train_mask].dropna(subset=feature_cols + ['label'])
test_df = df[test_mask].dropna(subset=feature_cols)
if len(train_df) < 1000 or len(test_df) < 100:
print(f" {test_month}: skip (data insufficient)", flush=True)
continue
X_train = train_df[feature_cols].values
y_train = train_df['label'].values
X_test = test_df[feature_cols].values
# 将 -1,0,1 映射到 0,1,2 用于多分类
y_train_cls = y_train + 1 # -1→0, 0→1, 1→2
# LightGBM 训练
params = {
'objective': 'multiclass',
'num_class': 3,
'metric': 'multi_logloss',
'learning_rate': 0.05,
'num_leaves': 31,
'max_depth': 6,
'min_child_samples': 50,
'subsample': 0.8,
'colsample_bytree': 0.8,
'reg_alpha': 0.1,
'reg_lambda': 0.1,
'verbose': -1,
'n_jobs': -1,
'seed': 42,
}
dtrain = lgb.Dataset(X_train, label=y_train_cls)
model = lgb.train(params, dtrain, num_boost_round=200)
# 预测概率
proba = model.predict(X_test) # shape: (n, 3) → [P(short), P(neutral), P(long)]
test_idx = test_df.index
all_proba_short.loc[test_idx] = proba[:, 0] # P(short)
all_proba_long.loc[test_idx] = proba[:, 2] # P(long)
# 特征重要性(只打印最后一个月的)
if i == len(months) - 1:
importance = model.feature_importance(importance_type='gain')
feat_imp = sorted(zip(feature_cols, importance), key=lambda x: -x[1])
print(f"\n Top 10 features:", flush=True)
for fname, imp in feat_imp[:10]:
print(f" {fname:<20} {imp:.0f}", flush=True)
long_cnt = (proba[:, 2] > 0.45).sum()
short_cnt = (proba[:, 0] > 0.45).sum()
print(f" {test_month}: train={len(train_df):,} test={len(test_df):,} "
f"signals: long={long_cnt} short={short_cnt}", flush=True)
return all_proba_long, all_proba_short
# ==================== 回测引擎 ====================
def backtest(df, proba_long, proba_short, notional=10000.0,
prob_threshold=0.45, min_hold=200, max_hold=1800,
sl_pct=0.004, tp_pct=0.006):
FEE = notional * 0.0006 * 2
REB = FEE * 0.9
NFEE = FEE - REB
pos = 0; op = 0.0; ot = None
trades = []
for i in range(len(df)):
dt = df.index[i]
p = df['close'].iloc[i]
pl = proba_long.iloc[i]
ps = proba_short.iloc[i]
# 持仓管理
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 >= sl_pct * 1.5:
trades.append((pos, op, p, notional*pp, hsec, "硬止损", ot, dt))
pos=0; continue
if hsec >= min_hold:
if -pp >= sl_pct:
trades.append((pos, op, p, notional*pp, hsec, "止损", ot, dt))
pos=0; continue
if pp >= tp_pct:
trades.append((pos, op, p, notional*pp, hsec, "止盈", ot, dt))
pos=0; continue
if hsec >= max_hold:
trades.append((pos, op, p, notional*pp, hsec, "超时", ot, dt))
pos=0; continue
# AI反向信号平仓
if pos == 1 and ps > prob_threshold + 0.05:
trades.append((pos, op, p, notional*pp, hsec, "AI反转", ot, dt))
pos=0
elif pos == -1 and pl > prob_threshold + 0.05:
trades.append((pos, op, p, notional*pp, hsec, "AI反转", ot, dt))
pos=0
# 开仓
if pos == 0:
if pl > prob_threshold and pl > ps:
pos = 1; op = p; ot = dt
elif ps > prob_threshold and ps > pl:
pos = -1; op = p; ot = dt
if pos != 0:
p = df['close'].iloc[-1]; dt = df.index[-1]
pp = (p-op)/op if pos==1 else (op-p)/op
trades.append((pos, op, p, notional*pp, (dt-ot).total_seconds(), "结束", ot, dt))
return trades
# ==================== 结果分析 ====================
def analyze(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[3] for t in trades)
net = total_pnl - NFEE * n
wins = len([t for t in trades if t[3]>0]); wr = wins/n*100
total_reb = REB * n
monthly = defaultdict(lambda: {'n':0,'net':0,'w':0})
for t in trades:
k = t[7].strftime('%Y-%m')
monthly[k]['n']+=1; monthly[k]['net']+=t[3]-NFEE
if t[3]>0: monthly[k]['w']+=1
cum=0;peak=0;dd=0
for t in trades:
cum+=t[3]-NFEE
if cum>peak: peak=cum
if peak-cum>dd: dd=peak-cum
pm = len([m for m in monthly.values() if m['net']>0])
reasons = defaultdict(int)
for t in trades: reasons[t[5]]+=1
print(f"\n{'='*75}", flush=True)
print(f" {label}", flush=True)
print(f"{'='*75}", flush=True)
print(f" 方向盈亏: {total_pnl:>+10.0f} USDT", flush=True)
print(f" 返佣: {total_reb:>+10.0f} USDT", flush=True)
print(f" 净手续费: {NFEE*n:>10.0f} USDT", flush=True)
print(f" ================================", flush=True)
print(f" 年净利: {net:>+10.0f} USDT (月均 {net/12:>+.0f})", flush=True)
print(f" 交易: {n}笔 | 胜率: {wr:.1f}% | 盈利月: {pm}/12", flush=True)
print(f" 最大回撤: {dd:>.0f} USDT", flush=True)
if wins > 0:
avg_win = sum(t[3] for t in trades if t[3]>0) / wins
avg_loss = sum(t[3] for t in trades if t[3]<=0) / (n-wins) if n>wins else 0
print(f" 均赢: {avg_win:>+.2f} | 均亏: {avg_loss:>+.2f} | 盈亏比: {abs(avg_win/avg_loss) if avg_loss!=0 else 999:.2f}", flush=True)
print(f"\n 平仓原因:", flush=True)
for r, cnt in sorted(reasons.items(), key=lambda x:-x[1]):
print(f" {r:<10} {cnt:>5}笔 ({cnt/n*100:.1f}%)", flush=True)
print(f"\n 月度:", flush=True)
for m in sorted(monthly.keys()):
d = monthly[m]; wr_m=d['w']/d['n']*100 if d['n']>0 else 0
bar = "+" * min(30, max(0, int(d['net']/100))) + "-" * min(30, max(0, int(-d['net']/100)))
print(f" {m} {d['n']:>4}{d['net']:>+8.0f} {wr_m:>4.0f}% {bar}", flush=True)
print(f" {'合计':>7} {n:>4}{net:>+8.0f}", flush=True)
print(f"\n 仓位放大:", flush=True)
for margin in [100, 300, 500, 800, 1000]:
scale = margin * 100 / notional
print(f" {margin}U: 月均 {net*scale/12:>+.0f} USDT {'<<< 达标' if net*scale/12>=1000 else ''}", flush=True)
print(f"{'='*75}", flush=True)
return net
# ==================== 主函数 ====================
def main():
t0 = _time.time()
print("="*75, flush=True)
print(" AI/ML 交易策略 — LightGBM + 30+技术指标", flush=True)
print("="*75, flush=True)
print("\n[1/4] 加载数据...", flush=True)
df = load_data()
print(f" {len(df):,} 根 1分钟K线", flush=True)
print("\n[2/4] 特征工程 (30+指标)...", flush=True)
df = add_features(df)
feature_cols = get_feature_cols(df)
print(f" 生成 {len(feature_cols)} 个特征", flush=True)
# 测试不同的前瞻期和阈值
configs = [
# (forward_bars, threshold, prob_threshold, sl, tp, label)
(5, 0.001, 0.42, 0.003, 0.004, "AI-v1: 5bar前瞻 阈值0.1%"),
(10, 0.002, 0.42, 0.004, 0.006, "AI-v2: 10bar前瞻 阈值0.2%"),
(10, 0.002, 0.45, 0.004, 0.006, "AI-v3: 10bar 高置信0.45"),
(10, 0.003, 0.45, 0.005, 0.008, "AI-v4: 10bar 阈值0.3% 宽SL"),
(20, 0.003, 0.42, 0.005, 0.008, "AI-v5: 20bar前瞻 阈值0.3%"),
(20, 0.004, 0.45, 0.005, 0.010, "AI-v6: 20bar 阈值0.4% 大TP"),
]
best_net = -999999; best_label = ""
for fb, thresh, prob_th, sl, tp, label in configs:
print(f"\n{'='*75}", flush=True)
print(f" [{label}]", flush=True)
print(f" 前瞻={fb}bar 方向阈值={thresh*100:.1f}% 概率阈值={prob_th} SL={sl*100:.1f}% TP={tp*100:.1f}%", flush=True)
print(f"{'='*75}", flush=True)
print("\n[3/4] 生成标签...", flush=True)
df_labeled = add_labels(df.copy(), forward_bars=fb, threshold=thresh)
labels = df_labeled['label']
print(f" 多={int((labels==1).sum()):,} 空={int((labels==-1).sum()):,} 中性={int((labels==0).sum()):,}", flush=True)
print("\n[4/4] 滚动训练+预测...", flush=True)
proba_long, proba_short = train_predict_walkforward(df_labeled, feature_cols, train_months=3)
print("\n 回测...", flush=True)
trades = backtest(df_labeled, proba_long, proba_short,
notional=10000.0, prob_threshold=prob_th,
sl_pct=sl, tp_pct=tp)
net = analyze(trades, 10000.0, label)
if net > best_net:
best_net = net; best_label = label
elapsed = _time.time() - t0
print(f"\n\n{'='*75}", flush=True)
print(f" 总结 | 耗时 {elapsed:.0f}s", flush=True)
print(f"{'='*75}", flush=True)
print(f" 最佳: {best_label}", flush=True)
print(f" 年净利: {best_net:+.0f} USDT = 月均 {best_net/12:+.0f} USDT", flush=True)
if best_net > 0:
needed = int(12000 / best_net * 100) + 1
print(f" 达到1000U/月需保证金: ~{needed}U", flush=True)
print(f"{'='*75}", flush=True)
if __name__ == '__main__':
main()

187
交易/bitmart-优化v2.py Normal file
View File

@@ -0,0 +1,187 @@
"""
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()

View File

@@ -0,0 +1,376 @@
"""
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()

View File

@@ -0,0 +1,363 @@
"""
多策略组合回测 — 目标: 1000 USDT/月
思路: 同时运行多个不同参数的 EMA 策略,它们在不同时间段产生信号,
彼此信号不重叠时各自独立开仓,等于"多个策略并行跑"
条件:
- 每笔: 100 USDT 保证金, 100x 杠杆, 名义 10,000 USDT
- 90% 返佣
- 最低持仓 > 3 分钟
- ETH 合约, 2025 全年
测试方案:
A) 单策略加大仓位 (500U/1000U)
B) 多策略组合 (3-5个不同参数策略并行)
C) 降低 ATR 门槛 + 更宽止损
D) 综合最优方案
"""
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 run_strategy(data, fp, sp, bp, atr_min, sl_pct, mh, notional=10000.0):
"""单策略回测,返回交易列表"""
ef=EMA(fp); es=EMA(sp); eb=EMA(bp)
H=[]; L=[]; C=[]
pf_=None; ps_=None
pos=0; op=0.0; ot=None; pend=None
hsl=sl_pct*1.5; AP=14
FEE_RATE=0.0006; REB_RATE=0.90; MIN_H=200
trades=[]
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 is not None:
pp=(p-op)/op if pos==1 else (op-p)/op
hsec=(dt-ot).total_seconds()
if -pp>=hsl:
pnl=notional*pp; fee=notional*FEE_RATE*2; reb=fee*REB_RATE
trades.append((pos, op, p, pnl, fee, reb, hsec, ot, dt))
pos=0; op=0; ot=None; pend=None; continue
if hsec>=MIN_H:
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:
pnl=notional*pp; fee=notional*FEE_RATE*2; reb=fee*REB_RATE
trades.append((pos, op, p, pnl, fee, reb, hsec, ot, dt))
pos=0; op=0; ot=None; pend=None
if atr_pct>=atr_min:
if (cd or fast<slow) and p<big: pos=-1; op=p; ot=dt
elif (cu or fast>slow) and p>big: pos=1; op=p; ot=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: pos=1; op=p; ot=dt
elif cd and p<big: pos=-1; op=p; ot=dt
if pos!=0:
p=data[-1][4]; dt=data[-1][0]
pp=(p-op)/op if pos==1 else (op-p)/op
pnl=notional*pp; fee=notional*FEE_RATE*2; reb=fee*REB_RATE
trades.append((pos, op, p, pnl, fee, reb, (dt-ot).total_seconds(), ot, dt))
return trades
def analyze(trades, label, notional=10000.0):
"""分析交易结果,返回摘要字典"""
if not trades:
return {'label': label, 'n': 0, 'net': 0, 'pnl': 0, 'reb': 0, 'fee': 0, 'dd': 0, 'monthly': {}}
n = len(trades)
total_pnl = sum(t[3] for t in trades)
total_fee = sum(t[4] for t in trades)
total_reb = sum(t[5] for t in trades)
net = total_pnl - (total_fee - total_reb)
# 最大回撤
cum=0; peak=0; dd=0
for t in trades:
cum += t[3] - (t[4] - t[5])
if cum > peak: peak = cum
if peak - cum > dd: dd = peak - cum
# 月度
monthly = {}
for t in trades:
k = t[8].strftime('%Y-%m')
if k not in monthly: monthly[k] = {'n': 0, 'net': 0}
monthly[k]['n'] += 1
monthly[k]['net'] += t[3] - (t[4] - t[5])
wr = len([t for t in trades if t[3]>0]) / n * 100
return {'label': label, 'n': n, 'net': net, 'pnl': total_pnl, 'reb': total_reb,
'fee': total_fee, 'dd': dd, 'wr': wr, 'monthly': monthly}
def merge_trades(all_trade_lists):
"""合并多个策略的交易(检查时间冲突:同一时间只能有一个持仓)"""
# 简单合并:按开仓时间排序,跳过与已有持仓重叠的交易
all_trades = []
for trades in all_trade_lists:
for t in trades:
all_trades.append(t)
all_trades.sort(key=lambda x: x[7]) # 按开仓时间排序
merged = []
last_close = None
for t in all_trades:
open_time = t[7]
close_time = t[8]
if last_close is None or open_time >= last_close:
merged.append(t)
last_close = close_time
return merged
def print_comparison(results):
"""打印对比表"""
print(f"\n{'='*110}", flush=True)
print(f" COMPARISON: Target = 1000 USDT/month = 12,000 USDT/year", flush=True)
print(f"{'='*110}", flush=True)
print(f" {'方案':<40} {'交易数':>6} {'年净利':>10} {'月均':>8} {'胜率':>6} {'返佣':>10} {'最大回撤':>10} {'达标':>4}", flush=True)
print(f" {'-'*104}", flush=True)
for r in results:
monthly_avg = r['net'] / 12
ok = "Yes" if monthly_avg >= 1000 else "No"
print(f" {r['label']:<40} {r['n']:>6} {r['net']:>+10.0f} {monthly_avg:>+8.0f} {r.get('wr',0):>5.1f}% {r['reb']:>10.0f} {r['dd']:>10.0f} {ok:>4}", flush=True)
print(f"{'='*110}", flush=True)
# 打印最佳方案的月度明细
best = max(results, key=lambda x: x['net'])
print(f"\n 最佳方案: {best['label']}", flush=True)
print(f" 年净利: {best['net']:+.0f} USDT | 月均: {best['net']/12:+.0f} USDT\n", flush=True)
if best['monthly']:
print(f" {'月份':<8} {'笔数':>5} {'净利润':>10}", flush=True)
print(f" {'-'*28}", flush=True)
for m in sorted(best['monthly'].keys()):
d = best['monthly'][m]
print(f" {m:<8} {d['n']:>5} {d['net']:>+10.0f}", flush=True)
print(f" {'-'*28}", flush=True)
print(f" {'合计':<8} {best['n']:>5} {best['net']:>+10.0f}", flush=True)
def main():
print("Loading data...", flush=True)
data = load()
print(f"{len(data)} bars loaded\n", flush=True)
NOTIONAL = 10000.0 # 100U * 100x
results = []
# ============================
# 方案 1: 原始策略(基线)
# ============================
t1 = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, NOTIONAL)
results.append(analyze(t1, "A1: EMA(8/21) ATR>0.3% [基线]", NOTIONAL))
print(f" A1 done: {len(t1)} trades", flush=True)
# ============================
# 方案 2: 加大仓位 - 500U (5倍)
# ============================
t2 = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, 50000.0)
results.append(analyze(t2, "A2: 基线 x5 (500U保证金)", 50000.0))
print(f" A2 done: {len(t2)} trades", flush=True)
# ============================
# 方案 3: 加大仓位 - 1000U (10倍)
# ============================
t3 = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, 100000.0)
results.append(analyze(t3, "A3: 基线 x10 (1000U保证金)", 100000.0))
print(f" A3 done: {len(t3)} trades", flush=True)
# ============================
# 方案 4: 降低ATR到0.15%(更多交易)
# ============================
t4 = run_strategy(data, 8, 21, 120, 0.0015, 0.004, 1800, NOTIONAL)
results.append(analyze(t4, "B1: ATR>0.15% (更频繁)", NOTIONAL))
print(f" B1 done: {len(t4)} trades", flush=True)
# ============================
# 方案 5: ATR>0.1% + 宽止损0.8%
# ============================
t5 = run_strategy(data, 8, 21, 120, 0.001, 0.008, 1800, NOTIONAL)
results.append(analyze(t5, "B2: ATR>0.1% SL=0.8%", NOTIONAL))
print(f" B2 done: {len(t5)} trades", flush=True)
# ============================
# 方案 6: ATR>0.2% + SL=0.8% + 更长持仓3600s
# ============================
t6 = run_strategy(data, 8, 21, 120, 0.002, 0.008, 3600, NOTIONAL)
results.append(analyze(t6, "B3: ATR>0.2% SL=0.8% MH=3600", NOTIONAL))
print(f" B3 done: {len(t6)} trades", flush=True)
# ============================
# 方案 7: 多策略组合3个不同EMA参数并行无时间重叠
# ============================
s1 = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, NOTIONAL)
s2 = run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, NOTIONAL)
s3 = run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, NOTIONAL)
merged3 = merge_trades([s1, s2, s3])
results.append(analyze(merged3, "C1: 3策略组合 (不重叠)", NOTIONAL))
print(f" C1 done: {len(s1)}+{len(s2)}+{len(s3)} -> {len(merged3)} merged", flush=True)
# ============================
# 方案 8: 5策略组合
# ============================
s4 = run_strategy(data, 20, 55, 120, 0.002, 0.006, 1800, NOTIONAL)
s5 = run_strategy(data, 8, 34, 200, 0.002, 0.005, 1800, NOTIONAL)
merged5 = merge_trades([s1, s2, s3, s4, s5])
results.append(analyze(merged5, "C2: 5策略组合 (不重叠)", NOTIONAL))
print(f" C2 done: -> {len(merged5)} merged", flush=True)
# ============================
# 方案 9: 3策略组合 + 加大仓位到 300U
# ============================
s1b = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, 30000.0)
s2b = run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, 30000.0)
s3b = run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, 30000.0)
merged3b = merge_trades([s1b, s2b, s3b])
results.append(analyze(merged3b, "D1: 3策略+300U仓位", 30000.0))
print(f" D1 done: -> {len(merged3b)} merged", flush=True)
# ============================
# 方案 10: 3策略组合 + 500U仓位
# ============================
s1c = run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, 50000.0)
s2c = run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, 50000.0)
s3c = run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, 50000.0)
merged3c = merge_trades([s1c, s2c, s3c])
results.append(analyze(merged3c, "D2: 3策略+500U仓位", 50000.0))
print(f" D2 done: -> {len(merged3c)} merged", flush=True)
# ============================
# 方案 11: 多策略并行(允许同时持仓,各策略独立运行)
# ============================
# 如果账户允许同时持有多个仓位(不同策略各自独立)
s1_r = analyze(s1, "sub1")
s2_r = analyze(run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, NOTIONAL), "sub2")
s3_r = analyze(run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, NOTIONAL), "sub3")
s4_r = analyze(run_strategy(data, 20, 55, 120, 0.002, 0.006, 1800, NOTIONAL), "sub4")
s5_r = analyze(run_strategy(data, 8, 34, 200, 0.002, 0.005, 1800, NOTIONAL), "sub5")
# 合并月度(允许重叠 = 各自独立计算再加总)
parallel_net = s1_r['net'] + s2_r['net'] + s3_r['net'] + s4_r['net'] + s5_r['net']
parallel_reb = s1_r['reb'] + s2_r['reb'] + s3_r['reb'] + s4_r['reb'] + s5_r['reb']
parallel_fee = s1_r['fee'] + s2_r['fee'] + s3_r['fee'] + s4_r['fee'] + s5_r['fee']
parallel_pnl = s1_r['pnl'] + s2_r['pnl'] + s3_r['pnl'] + s4_r['pnl'] + s5_r['pnl']
parallel_n = s1_r['n'] + s2_r['n'] + s3_r['n'] + s4_r['n'] + s5_r['n']
parallel_dd = max(s1_r['dd'], s2_r['dd'], s3_r['dd'], s4_r['dd'], s5_r['dd']) * 2 # 保守估计
# 合并月度
all_months = set()
for sr in [s1_r, s2_r, s3_r, s4_r, s5_r]:
all_months.update(sr['monthly'].keys())
parallel_monthly = {}
for m in all_months:
n_m = 0; net_m = 0
for sr in [s1_r, s2_r, s3_r, s4_r, s5_r]:
if m in sr['monthly']:
n_m += sr['monthly'][m]['n']
net_m += sr['monthly'][m]['net']
parallel_monthly[m] = {'n': n_m, 'net': net_m}
results.append({
'label': "E1: 5策略并行(允许同时持仓) 100U each",
'n': parallel_n, 'net': parallel_net, 'pnl': parallel_pnl,
'reb': parallel_reb, 'fee': parallel_fee, 'dd': parallel_dd,
'wr': 0, 'monthly': parallel_monthly
})
print(f" E1 done: {parallel_n} total trades (parallel)", flush=True)
# ============================
# 方案 12: 5策略并行 + 200U仓位
# ============================
N2 = 20000.0
ps1 = analyze(run_strategy(data, 8, 21, 120, 0.003, 0.004, 1800, N2), "p1")
ps2 = analyze(run_strategy(data, 13, 55, 200, 0.002, 0.005, 1800, N2), "p2")
ps3 = analyze(run_strategy(data, 30, 80, 200, 0.002, 0.008, 3600, N2), "p3")
ps4 = analyze(run_strategy(data, 20, 55, 120, 0.002, 0.006, 1800, N2), "p4")
ps5 = analyze(run_strategy(data, 8, 34, 200, 0.002, 0.005, 1800, N2), "p5")
p_net = ps1['net']+ps2['net']+ps3['net']+ps4['net']+ps5['net']
p_reb = ps1['reb']+ps2['reb']+ps3['reb']+ps4['reb']+ps5['reb']
p_fee = ps1['fee']+ps2['fee']+ps3['fee']+ps4['fee']+ps5['fee']
p_pnl = ps1['pnl']+ps2['pnl']+ps3['pnl']+ps4['pnl']+ps5['pnl']
p_n = ps1['n']+ps2['n']+ps3['n']+ps4['n']+ps5['n']
p_dd = max(ps1['dd'],ps2['dd'],ps3['dd'],ps4['dd'],ps5['dd'])*2
p_monthly = {}
for m in all_months:
n_m=0; net_m=0
for sr in [ps1,ps2,ps3,ps4,ps5]:
if m in sr['monthly']:
n_m+=sr['monthly'][m]['n']; net_m+=sr['monthly'][m]['net']
p_monthly[m] = {'n': n_m, 'net': net_m}
results.append({
'label': "E2: 5策略并行 200U each",
'n': p_n, 'net': p_net, 'pnl': p_pnl,
'reb': p_reb, 'fee': p_fee, 'dd': p_dd,
'wr': 0, 'monthly': p_monthly
})
print(f" E2 done: {p_n} total trades (parallel 200U)", flush=True)
# ============================
# 打印对比
# ============================
print_comparison(results)
# 保存
csv = Path(__file__).parent.parent / 'multi_strategy_results.csv'
with open(csv, 'w', encoding='utf-8-sig') as f:
f.write("方案,交易数,年净利,月均,返佣,最大回撤\n")
for r in results:
f.write(f"{r['label']},{r['n']},{r['net']:.0f},{r['net']/12:.0f},{r['reb']:.0f},{r['dd']:.0f}\n")
print(f"\nSaved: {csv}", flush=True)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,289 @@
"""
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()

View File

@@ -0,0 +1,673 @@
"""
BitMart 返佣策略回测 — 双策略对比
策略A: 网格交易 (Grid Trading)
- 围绕EMA中轨设定网格价格触及网格线时开仓
- 固定止盈(1格)、固定止损(3格)
- 趋势过滤:只在趋势方向开仓
策略B: EMA趋势跟随 (EMA Trend Following)
- 快慢EMA金叉做多、死叉做空
- 始终持仓,信号反转时反手
- 大级别趋势过滤避免逆势
两个策略都:
- 严格执行 >3分钟最低持仓
- 计算90%返佣收益
- 输出详细对比报告
"""
import time
import datetime
import statistics
import sqlite3
from pathlib import Path
from dataclasses import dataclass
from typing import List
# ========================= 简易 Logger =========================
class _L:
@staticmethod
def info(m): print(f"[INFO] {m}")
@staticmethod
def ok(m): print(f"[ OK ] {m}")
@staticmethod
def warn(m): print(f"[WARN] {m}")
@staticmethod
def err(m): print(f"[ERR ] {m}")
log = _L()
# ========================= 交易记录 =========================
@dataclass
class Trade:
open_time: datetime.datetime
close_time: datetime.datetime
direction: str
open_price: float
close_price: float
size: float
pnl: float
pnl_pct: float
fee: float
rebate: float
hold_seconds: float
close_reason: str
# ========================= 数据加载 =========================
def load_1m_klines(start_date='2025-01-01', end_date='2025-12-31'):
db_path = Path(__file__).parent.parent / 'models' / 'database.db'
start_dt = datetime.datetime.strptime(start_date, '%Y-%m-%d')
end_dt = datetime.datetime.strptime(end_date, '%Y-%m-%d') + datetime.timedelta(days=1)
start_ms = int(start_dt.timestamp()) * 1000
end_ms = int(end_dt.timestamp()) * 1000
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute(
"SELECT id, open, high, low, close FROM bitmart_eth_1m "
"WHERE id >= ? AND id < ? ORDER BY id",
(start_ms, end_ms)
)
rows = cursor.fetchall()
conn.close()
data = []
for r in rows:
dt = datetime.datetime.fromtimestamp(r[0] / 1000.0)
data.append({
'datetime': dt,
'open': r[1], 'high': r[2], 'low': r[3], 'close': r[4],
})
log.info(f"Loaded {len(data)} bars ({start_date} ~ {end_date})")
return data
# ========================= EMA 工具 =========================
class EMA:
def __init__(self, period):
self.period = 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
# ========================= 基础回测引擎 =========================
class BaseBacktest:
def __init__(self, name, initial_balance=1000.0, leverage=50,
risk_pct=0.005, taker_fee=0.0006, rebate_rate=0.90,
min_hold_sec=200, max_hold_sec=1800):
self.name = name
self.initial_balance = initial_balance
self.balance = initial_balance
self.leverage = leverage
self.risk_pct = risk_pct
self.taker_fee = taker_fee
self.rebate_rate = rebate_rate
self.min_hold_sec = min_hold_sec
self.max_hold_sec = max_hold_sec
self.position = 0
self.open_price = 0.0
self.open_time = None
self.pos_size = 0.0
self.trades: List[Trade] = []
self.equity_curve = []
self.peak_equity = initial_balance
self.max_dd_pct = 0.0
def _open(self, direction, price, dt):
self.pos_size = self.balance * self.risk_pct * self.leverage
if self.pos_size < 1:
return False
fee = self.pos_size * self.taker_fee
self.balance -= fee
self.position = 1 if direction == 'long' else -1
self.open_price = price
self.open_time = dt
return True
def _close(self, price, dt, reason):
if self.position == 0:
return None
if self.position == 1:
pnl_pct = (price - self.open_price) / self.open_price
else:
pnl_pct = (self.open_price - price) / self.open_price
pnl = self.pos_size * pnl_pct
close_val = self.pos_size * (1 + pnl_pct)
close_fee = close_val * self.taker_fee
open_fee = self.pos_size * self.taker_fee
total_fee = open_fee + close_fee
rebate = total_fee * self.rebate_rate
self.balance += pnl - close_fee + rebate
hold_sec = (dt - self.open_time).total_seconds()
trade = Trade(
open_time=self.open_time, close_time=dt,
direction='long' if self.position == 1 else 'short',
open_price=self.open_price, close_price=price,
size=self.pos_size, pnl=pnl, pnl_pct=pnl_pct,
fee=total_fee, rebate=rebate,
hold_seconds=hold_sec, close_reason=reason,
)
self.trades.append(trade)
self.position = 0
self.open_price = 0.0
self.open_time = None
self.pos_size = 0.0
return trade
def hold_seconds(self, dt):
if self.open_time is None:
return 0
return (dt - self.open_time).total_seconds()
def can_close(self, dt):
return self.hold_seconds(dt) >= self.min_hold_sec
def cur_pnl_pct(self, price):
if self.position == 1:
return (price - self.open_price) / self.open_price
elif self.position == -1:
return (self.open_price - price) / self.open_price
return 0
def track_equity(self, dt, price, every_n=60, bar_idx=0):
if bar_idx % every_n != 0:
return
eq = self.balance
if self.position != 0 and self.open_price > 0:
eq += self.pos_size * self.cur_pnl_pct(price)
self.equity_curve.append({'datetime': dt, 'equity': eq})
if eq > self.peak_equity:
self.peak_equity = eq
dd = (self.peak_equity - eq) / self.peak_equity if self.peak_equity > 0 else 0
if dd > self.max_dd_pct:
self.max_dd_pct = dd
# ========================= 策略A: 网格交易 =========================
class GridStrategy(BaseBacktest):
"""
网格交易 + 趋势过滤
- 用 EMA(120) 判断趋势方向
- 网格间距 = grid_pct (如 0.20%)
- 顺势开仓:上涨趋势中价格回落到网格线做多,下跌趋势中价格反弹到网格线做空
- TP: tp_grids 格 (如 1格 = 0.20%)
- SL: sl_grids 格 (如 3格 = 0.60%)
- 最低持仓 200 秒
"""
def __init__(self, grid_pct=0.0020, tp_grids=1, sl_grids=3,
trend_ema_period=120, **kwargs):
super().__init__(name="Grid+Trend", **kwargs)
self.grid_pct = grid_pct
self.tp_pct = grid_pct * tp_grids
self.sl_pct = grid_pct * sl_grids
self.hard_sl_pct = grid_pct * (sl_grids + 1)
self.trend_ema = EMA(trend_ema_period)
self.last_grid_cross = None # 上一次穿越的网格线价格
self.cooldown_until = None # 冷却期
def get_grid_level(self, price, direction='below'):
"""获取价格最近的网格线"""
grid_size = price * self.grid_pct
if grid_size == 0:
return price
if direction == 'below':
return price - (price % grid_size)
else:
return price - (price % grid_size) + grid_size
def run(self, data):
log.info(f"[{self.name}] Starting... grid={self.grid_pct*100:.2f}% TP={self.tp_pct*100:.2f}% SL={self.sl_pct*100:.2f}%")
t0 = time.time()
prev_close = None
for i, bar in enumerate(data):
price = bar['close']
high = bar['high']
low = bar['low']
dt = bar['datetime']
ema_val = self.trend_ema.update(price)
# 冷却期检查
if self.cooldown_until and dt < self.cooldown_until:
self.track_equity(dt, price, bar_idx=i)
prev_close = price
continue
self.cooldown_until = None
# === 有持仓:检查平仓 ===
if self.position != 0:
p = self.cur_pnl_pct(price)
hs = self.hold_seconds(dt)
# 硬止损(不受时间限制)
if -p >= self.hard_sl_pct:
self._close(price, dt, f"hard_SL ({p*100:+.3f}%)")
self.cooldown_until = dt + datetime.timedelta(seconds=120)
self.track_equity(dt, price, bar_idx=i)
prev_close = price
continue
# 满足最低持仓后
if self.can_close(dt):
# 止盈
if p >= self.tp_pct:
self._close(price, dt, f"TP ({p*100:+.3f}%)")
prev_close = price
self.track_equity(dt, price, bar_idx=i)
continue
# 止损
if -p >= self.sl_pct:
self._close(price, dt, f"SL ({p*100:+.3f}%)")
self.cooldown_until = dt + datetime.timedelta(seconds=120)
prev_close = price
self.track_equity(dt, price, bar_idx=i)
continue
# 超时
if hs >= self.max_hold_sec:
self._close(price, dt, f"timeout ({hs:.0f}s)")
prev_close = price
self.track_equity(dt, price, bar_idx=i)
continue
# === 无持仓:检查开仓 ===
if self.position == 0 and prev_close is not None:
grid_below = self.get_grid_level(prev_close, 'below')
grid_above = self.get_grid_level(prev_close, 'above')
# 上涨趋势:价格回落到下方网格线 → 做多
if price > ema_val and low <= grid_below and prev_close > grid_below:
self._open('long', price, dt)
# 下跌趋势:价格反弹到上方网格线 → 做空
elif price < ema_val and high >= grid_above and prev_close < grid_above:
self._open('short', price, dt)
self.track_equity(dt, price, bar_idx=i)
prev_close = price
if i > 0 and i % (len(data) // 10) == 0:
log.info(f" [{self.name}] {i/len(data)*100:.0f}% | bal={self.balance:.2f} | trades={len(self.trades)}")
# 强制平仓
if self.position != 0:
self._close(data[-1]['close'], data[-1]['datetime'], "backtest_end")
log.ok(f"[{self.name}] Done in {time.time()-t0:.1f}s | {len(self.trades)} trades")
return self.trades
# ========================= 策略B: EMA趋势跟随 =========================
class EMATrendStrategy(BaseBacktest):
"""
EMA 趋势跟随
- 快线 EMA(8),慢线 EMA(21)
- 大级别过滤 EMA(120)
- 金叉且价格在大EMA上方 → 做多
- 死叉且价格在大EMA下方 → 做空
- 反向交叉时反手(满足持仓时间后)
- 加入 ATR 波动率过滤,低波动时不交易
"""
def __init__(self, fast_period=8, slow_period=21, big_period=120,
atr_period=14, atr_min_pct=0.0003, **kwargs):
super().__init__(name="EMA-Trend", **kwargs)
self.ema_fast = EMA(fast_period)
self.ema_slow = EMA(slow_period)
self.ema_big = EMA(big_period)
self.atr_period = atr_period
self.atr_min_pct = atr_min_pct # 最低波动率过滤
self.highs = []
self.lows = []
self.closes = []
self.prev_fast = None
self.prev_slow = None
self.pending_signal = None # 等待持仓时间满足后执行的信号
def calc_atr(self):
if len(self.highs) < self.atr_period + 1:
return None
trs = []
for i in range(-self.atr_period, 0):
h = self.highs[i]
l = self.lows[i]
pc = self.closes[i - 1]
tr = max(h - l, abs(h - pc), abs(l - pc))
trs.append(tr)
return sum(trs) / len(trs)
def run(self, data):
log.info(f"[{self.name}] Starting... fast=EMA8 slow=EMA21 big=EMA120")
t0 = time.time()
stop_loss_pct = 0.004 # 0.4% 止损
hard_sl_pct = 0.006 # 0.6% 硬止损
for i, bar in enumerate(data):
price = bar['close']
dt = bar['datetime']
self.highs.append(bar['high'])
self.lows.append(bar['low'])
self.closes.append(price)
fast = self.ema_fast.update(price)
slow = self.ema_slow.update(price)
big = self.ema_big.update(price)
# ATR 波动率过滤
atr = self.calc_atr()
if atr is not None and price > 0:
atr_pct = atr / price
else:
atr_pct = 0
# 检测交叉
cross_up = (self.prev_fast is not None and
self.prev_fast <= self.prev_slow and fast > slow)
cross_down = (self.prev_fast is not None and
self.prev_fast >= self.prev_slow and fast < slow)
self.prev_fast = fast
self.prev_slow = slow
# === 有持仓 ===
if self.position != 0:
p = self.cur_pnl_pct(price)
# 硬止损
if -p >= hard_sl_pct:
self._close(price, dt, f"hard_SL ({p*100:+.3f}%)")
self.track_equity(dt, price, bar_idx=i)
continue
if self.can_close(dt):
# 止损
if -p >= stop_loss_pct:
self._close(price, dt, f"SL ({p*100:+.3f}%)")
self.track_equity(dt, price, bar_idx=i)
continue
# 超时
hs = self.hold_seconds(dt)
if hs >= self.max_hold_sec:
self._close(price, dt, f"timeout ({hs:.0f}s)")
self.track_equity(dt, price, bar_idx=i)
continue
# 反手信号:持多遇到死叉 → 平多
if self.position == 1 and cross_down:
self._close(price, dt, "cross_reverse")
if price < big and atr_pct >= self.atr_min_pct:
self._open('short', price, dt)
self.track_equity(dt, price, bar_idx=i)
continue
# 反手信号:持空遇到金叉 → 平空
if self.position == -1 and cross_up:
self._close(price, dt, "cross_reverse")
if price > big and atr_pct >= self.atr_min_pct:
self._open('long', price, dt)
self.track_equity(dt, price, bar_idx=i)
continue
else:
# 未满最低持仓时间,记录待处理信号
if self.position == 1 and cross_down:
self.pending_signal = 'close_long'
elif self.position == -1 and cross_up:
self.pending_signal = 'close_short'
# 处理待处理信号(持仓时间刚好满足)
if self.pending_signal and self.can_close(dt):
if self.pending_signal == 'close_long' and self.position == 1:
self._close(price, dt, "delayed_cross")
if fast < slow and price < big and atr_pct >= self.atr_min_pct:
self._open('short', price, dt)
elif self.pending_signal == 'close_short' and self.position == -1:
self._close(price, dt, "delayed_cross")
if fast > slow and price > big and atr_pct >= self.atr_min_pct:
self._open('long', price, dt)
self.pending_signal = None
# === 无持仓:检查开仓 ===
if self.position == 0 and atr_pct >= self.atr_min_pct:
if cross_up and price > big:
self._open('long', price, dt)
elif cross_down and price < big:
self._open('short', price, dt)
self.track_equity(dt, price, bar_idx=i)
if i > 0 and i % (len(data) // 10) == 0:
log.info(f" [{self.name}] {i/len(data)*100:.0f}% | bal={self.balance:.2f} | trades={len(self.trades)}")
if self.position != 0:
self._close(data[-1]['close'], data[-1]['datetime'], "backtest_end")
log.ok(f"[{self.name}] Done in {time.time()-t0:.1f}s | {len(self.trades)} trades")
return self.trades
# ========================= 报告生成 =========================
def print_report(strategy: BaseBacktest):
trades = strategy.trades
if not trades:
print(f"\n[{strategy.name}] No trades.")
return
n = len(trades)
wins = [t for t in trades if t.pnl > 0]
losses = [t for t in trades if t.pnl <= 0]
wr = len(wins) / n * 100
total_pnl = sum(t.pnl for t in trades)
total_fee = sum(t.fee for t in trades)
total_rebate = sum(t.rebate for t in trades)
net = strategy.balance - strategy.initial_balance
total_vol = sum(t.size for t in trades) * 2
avg_pnl = total_pnl / n
avg_win = statistics.mean([t.pnl for t in wins]) if wins else 0
avg_loss = statistics.mean([t.pnl for t in losses]) if losses else 0
avg_hold = statistics.mean([t.hold_seconds for t in trades])
pf_num = sum(t.pnl for t in wins)
pf_den = abs(sum(t.pnl for t in losses))
pf = pf_num / pf_den if pf_den > 0 else float('inf')
# 连续亏损
max_streak = 0
cur = 0
for t in trades:
if t.pnl <= 0:
cur += 1
max_streak = max(max_streak, cur)
else:
cur = 0
long_t = [t for t in trades if t.direction == 'long']
short_t = [t for t in trades if t.direction == 'short']
long_wr = len([t for t in long_t if t.pnl > 0]) / len(long_t) * 100 if long_t else 0
short_wr = len([t for t in short_t if t.pnl > 0]) / len(short_t) * 100 if short_t else 0
# 平仓原因
reasons = {}
for t in trades:
r = t.close_reason.split(' (')[0]
reasons[r] = reasons.get(r, 0) + 1
under_3m = len([t for t in trades if t.hold_seconds < 180])
w = 65
print(f"\n{'='*w}")
print(f" [{strategy.name}] Backtest Report")
print(f"{'='*w}")
print(f"\n--- Account ---")
print(f" Initial: {strategy.initial_balance:>12.2f} USDT")
print(f" Final: {strategy.balance:>12.2f} USDT")
print(f" Net P&L: {net:>+12.2f} USDT ({net/strategy.initial_balance*100:+.2f}%)")
print(f" Max Drawdown: {strategy.max_dd_pct*100:>11.2f}%")
print(f"\n--- Trades ---")
print(f" Total: {n:>8}")
print(f" Wins: {len(wins):>8} ({wr:.1f}%)")
print(f" Losses: {len(losses):>8} ({100-wr:.1f}%)")
print(f" Long: {len(long_t):>8} (WR {long_wr:.1f}%)")
print(f" Short: {len(short_t):>8} (WR {short_wr:.1f}%)")
print(f" Profit Factor: {pf:>8.2f}")
print(f" Max Loss Streak:{max_streak:>8}")
print(f"\n--- P&L ---")
print(f" Direction P&L: {total_pnl:>+12.4f} USDT")
print(f" Avg per trade: {avg_pnl:>+12.4f} USDT")
print(f" Avg win: {avg_win:>+12.4f} USDT")
print(f" Avg loss: {avg_loss:>+12.4f} USDT")
print(f" Best trade: {max(t.pnl for t in trades):>+12.4f} USDT")
print(f" Worst trade: {min(t.pnl for t in trades):>+12.4f} USDT")
print(f"\n--- Fees & Rebate ---")
print(f" Volume: {total_vol:>12.2f} USDT")
print(f" Total Fees: {total_fee:>12.4f} USDT")
print(f" Rebate (90%): {total_rebate:>+12.4f} USDT")
print(f" Net Fee Cost: {total_fee - total_rebate:>12.4f} USDT")
print(f"\n--- Hold Time ---")
print(f" Average: {avg_hold:>8.0f}s ({avg_hold/60:.1f}min)")
print(f" Shortest: {min(t.hold_seconds for t in trades):>8.0f}s")
print(f" Longest: {max(t.hold_seconds for t in trades):>8.0f}s")
print(f" Under 3min: {under_3m:>8} (hard SL only)")
print(f"\n--- Close Reasons ---")
for r, c in sorted(reasons.items(), key=lambda x: -x[1]):
print(f" {r:<22} {c:>6} ({c/n*100:.1f}%)")
# 月度统计
print(f"\n--- Monthly ---")
print(f" {'Month':<10} {'Trades':>6} {'Dir PnL':>10} {'Rebate':>10} {'Net':>10} {'WR':>6}")
print(f" {'-'*54}")
monthly = {}
for t in trades:
k = t.close_time.strftime('%Y-%m')
if k not in monthly:
monthly[k] = {'n': 0, 'pnl': 0, 'rebate': 0, 'fee': 0, 'wins': 0}
monthly[k]['n'] += 1
monthly[k]['pnl'] += t.pnl
monthly[k]['rebate'] += t.rebate
monthly[k]['fee'] += t.fee
if t.pnl > 0:
monthly[k]['wins'] += 1
for month in sorted(monthly.keys()):
m = monthly[month]
net_m = m['pnl'] - m['fee'] + m['rebate'] # 正确的月度净收益
wr_m = m['wins'] / m['n'] * 100 if m['n'] > 0 else 0
print(f" {month:<10} {m['n']:>6} {m['pnl']:>+10.2f} {m['rebate']:>10.2f} {net_m:>+10.2f} {wr_m:>5.1f}%")
print(f"{'='*w}")
# 保存CSV
csv_path = Path(__file__).parent.parent / f'{strategy.name}_trades.csv'
with open(csv_path, 'w', encoding='utf-8-sig') as f:
f.write("open_time,close_time,dir,open_px,close_px,size,pnl,pnl_pct,fee,rebate,hold_sec,reason\n")
for t in trades:
f.write(f"{t.open_time},{t.close_time},{t.direction},"
f"{t.open_price:.2f},{t.close_price:.2f},{t.size:.2f},"
f"{t.pnl:.4f},{t.pnl_pct*100:.4f}%,{t.fee:.4f},{t.rebate:.4f},"
f"{t.hold_seconds:.0f},{t.close_reason}\n")
log.ok(f"Trades saved: {csv_path}")
# ========================= 主函数 =========================
def main():
data = load_1m_klines('2025-01-01', '2025-12-31')
if not data:
log.err("No data!")
return
common = dict(
initial_balance=1000.0,
leverage=50,
risk_pct=0.005,
taker_fee=0.0006,
rebate_rate=0.90,
min_hold_sec=200,
max_hold_sec=1800,
)
# === 策略A: 网格交易 ===
grid = GridStrategy(
grid_pct=0.0020, # 0.20% 网格间距
tp_grids=1, # 止盈1格 (0.20%)
sl_grids=3, # 止损3格 (0.60%)
trend_ema_period=120, # 2小时EMA趋势过滤
**common,
)
grid.run(data)
print_report(grid)
# === 策略B: EMA趋势跟随 ===
ema = EMATrendStrategy(
fast_period=8,
slow_period=21,
big_period=120,
atr_period=14,
atr_min_pct=0.0003, # 最低波动率过滤
**common,
)
ema.run(data)
print_report(ema)
# === 对比摘要 ===
print(f"\n{'='*65}")
print(f" COMPARISON SUMMARY")
print(f"{'='*65}")
print(f" {'Metric':<25} {'Grid+Trend':>18} {'EMA-Trend':>18}")
print(f" {'-'*61}")
for s in [grid, ema]:
s._net = s.balance - s.initial_balance
s._trades_n = len(s.trades)
s._wr = len([t for t in s.trades if t.pnl > 0]) / len(s.trades) * 100 if s.trades else 0
s._dir_pnl = sum(t.pnl for t in s.trades)
s._rebate = sum(t.rebate for t in s.trades)
s._fee = sum(t.fee for t in s.trades)
s._vol = sum(t.size for t in s.trades) * 2
rows = [
("Net P&L (USDT)", f"{grid._net:+.2f}", f"{ema._net:+.2f}"),
("Net P&L (%)", f"{grid._net/grid.initial_balance*100:+.2f}%", f"{ema._net/ema.initial_balance*100:+.2f}%"),
("Max Drawdown", f"{grid.max_dd_pct*100:.2f}%", f"{ema.max_dd_pct*100:.2f}%"),
("Total Trades", f"{grid._trades_n}", f"{ema._trades_n}"),
("Win Rate", f"{grid._wr:.1f}%", f"{ema._wr:.1f}%"),
("Direction P&L", f"{grid._dir_pnl:+.2f}", f"{ema._dir_pnl:+.2f}"),
("Total Volume", f"{grid._vol:,.0f}", f"{ema._vol:,.0f}"),
("Total Fees", f"{grid._fee:.2f}", f"{ema._fee:.2f}"),
("Rebate Income", f"{grid._rebate:+.2f}", f"{ema._rebate:+.2f}"),
]
for label, v1, v2 in rows:
print(f" {label:<25} {v1:>18} {v2:>18}")
print(f"{'='*65}")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,293 @@
"""
最终回测 - Top 3 盈利参数 + 不同仓位大小
Top 3 profitable combos from scan:
1. EMA(8/21/120) ATR>0.30% SL=0.4% MH=1800s → +2.88%
2. EMA(30/80/200) ATR>0.20% SL=0.8% MH=3600s → +2.53%
3. EMA(8/21/120) ATR>0.20% SL=0.8% MH=1800s → +1.94%
每组参数测试 risk_pct = [0.005, 0.01, 0.02, 0.03, 0.05]
输出详细月度报告和交易明细
"""
import sys, time, datetime, sqlite3, statistics
from pathlib import Path
from dataclasses import dataclass
from typing import List
@dataclass
class Trade:
open_time: datetime.datetime
close_time: datetime.datetime
direction: str
open_price: float
close_price: float
size: float
pnl: float
pnl_pct: float
fee: float
rebate: float
hold_seconds: float
close_reason: str
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 run_detailed(data, fp, sp, bp, atr_min, sl_pct, mh, risk_pct=0.005,
leverage=50, taker_fee=0.0006, rebate_rate=0.90, min_hold=200):
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; pslow = None
trades: List[Trade] = []
hsl = sl_pct * 1.5
eq_curve = []; peak = 1000.0; max_dd = 0.0
def _close(price, dt, reason):
nonlocal bal, pos, op_, ot_, ps_, pend
pp = (price - op_) / op_ if pos == 1 else (op_ - price) / op_
pnl = ps_ * pp
cv = ps_ * (1 + pp); cf = cv * taker_fee; of = ps_ * taker_fee
tf = of + cf; rb = tf * rebate_rate
bal += pnl - cf + rb
hs = (dt - ot_).total_seconds()
trades.append(Trade(ot_, dt, 'long' if pos == 1 else 'short',
op_, price, ps_, pnl, pp, tf, rb, hs, reason))
pos = 0; op_ = 0; ot_ = None; ps_ = 0; pend = None
def _open(d, price, dt):
nonlocal bal, pos, op_, ot_, ps_
ns = bal * risk_pct * leverage
if ns < 1: return
bal -= ns * taker_fee
pos = 1 if d == 'L' else -1; op_ = price; ot_ = dt; ps_ = ns
for i, (dt, o, h, l, c) in enumerate(data):
H.append(h); L.append(l); C.append(c)
fast = ef.update(c); slow = es.update(c); big = eb.update(c)
atr_pct = 0.0
AP = 14
if len(H) > AP + 1:
s = 0.0
for j in range(-AP, 0):
tr = H[j] - L[j]; d1 = abs(H[j] - C[j-1]); d2 = abs(L[j] - C[j-1])
if d1 > tr: tr = d1
if d2 > tr: tr = d2
s += tr
atr_pct = s / (AP * c) if c > 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_:
pp = (c - op_) / op_ if pos == 1 else (op_ - c) / op_
hsec = (dt - ot_).total_seconds()
if -pp >= hsl:
_close(c, dt, f"hard_SL({pp*100:+.2f}%)")
continue
if hsec >= min_hold:
dc = False; reason = ""
if -pp >= sl_pct: dc = True; reason = f"SL({pp*100:+.2f}%)"
elif hsec >= mh: dc = True; reason = f"timeout({hsec:.0f}s)"
elif pos == 1 and cd: dc = True; reason = "cross_rev"
elif pos == -1 and cu: dc = True; reason = "cross_rev"
elif pend == 'cl' and pos == 1: dc = True; reason = "delayed_cross"
elif pend == 'cs' and pos == -1: dc = True; reason = "delayed_cross"
if dc:
_close(c, dt, reason)
if atr_pct >= atr_min:
if (cd or fast < slow) and c < big: _open('S', c, dt)
elif (cu or fast > slow) and c > big: _open('L', c, 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 c > big: _open('L', c, dt)
elif cd and c < big: _open('S', c, dt)
# equity tracking every hour
if i % 60 == 0:
eq = bal
if pos != 0 and op_ > 0:
pp = (c - op_) / op_ if pos == 1 else (op_ - c) / op_
eq += ps_ * pp
eq_curve.append((dt, eq))
if eq > peak: peak = eq
dd = (peak - eq) / peak if peak > 0 else 0
if dd > max_dd: max_dd = dd
if pos != 0: _close(data[-1][4], data[-1][0], "backtest_end")
return trades, bal, max_dd, eq_curve
def print_report(name, trades, balance, max_dd, init=1000.0):
if not trades:
print(f"\n[{name}] No trades.\n", flush=True)
return
n = len(trades)
wins = [t for t in trades if t.pnl > 0]
losses = [t for t in trades if t.pnl <= 0]
wr = len(wins) / n * 100
net = balance - init
tot_pnl = sum(t.pnl for t in trades)
tot_fee = sum(t.fee for t in trades)
tot_reb = sum(t.rebate for t in trades)
avg_hold = statistics.mean([t.hold_seconds for t in trades])
vol = sum(t.size for t in trades) * 2
pf_n = sum(t.pnl for t in wins) if wins else 0
pf_d = abs(sum(t.pnl for t in losses)) if losses else 0
pf = pf_n / pf_d if pf_d > 0 else float('inf')
long_t = [t for t in trades if t.direction == 'long']
short_t = [t for t in trades if t.direction == 'short']
long_wr = len([t for t in long_t if t.pnl > 0]) / len(long_t) * 100 if long_t else 0
short_wr = len([t for t in short_t if t.pnl > 0]) / len(short_t) * 100 if short_t else 0
reasons = {}
for t in trades:
r = t.close_reason.split('(')[0]
reasons[r] = reasons.get(r, 0) + 1
print(f"\n{'='*70}", flush=True)
print(f" [{name}]", flush=True)
print(f"{'='*70}", flush=True)
print(f" Initial: {init:>10.2f} USDT", flush=True)
print(f" Final: {balance:>10.2f} USDT", flush=True)
print(f" Net P&L: {net:>+10.2f} USDT ({net/init*100:+.2f}%)", flush=True)
print(f" Max Drawdown: {max_dd*100:>9.2f}%", flush=True)
print(f"\n Trades: {n:>6} (Long {len(long_t)} WR={long_wr:.0f}% | Short {len(short_t)} WR={short_wr:.0f}%)", flush=True)
print(f" Win Rate: {wr:>5.1f}%", flush=True)
print(f" Profit Factor:{pf:>6.2f}", flush=True)
print(f" Avg Hold: {avg_hold:>5.0f}s ({avg_hold/60:.1f}min)", flush=True)
print(f"\n Dir P&L: {tot_pnl:>+10.2f}", flush=True)
print(f" Total Fee: {tot_fee:>10.2f}", flush=True)
print(f" Rebate(90%): {tot_reb:>+10.2f}", flush=True)
print(f" Net Fee: {tot_fee - tot_reb:>10.2f}", flush=True)
print(f" Volume: {vol:>10.0f}", flush=True)
if wins:
print(f"\n Avg Win: {statistics.mean([t.pnl for t in wins]):>+10.4f}", flush=True)
if losses:
print(f" Avg Loss: {statistics.mean([t.pnl for t in losses]):>+10.4f}", flush=True)
print(f" Best: {max(t.pnl for t in trades):>+10.4f}", flush=True)
print(f" Worst: {min(t.pnl for t in trades):>+10.4f}", flush=True)
print(f"\n Close Reasons:", flush=True)
for r, c in sorted(reasons.items(), key=lambda x: -x[1]):
print(f" {r:<20} {c:>5} ({c/n*100:.1f}%)", flush=True)
# Monthly
print(f"\n {'Month':<8} {'Trd':>5} {'DirPnL':>9} {'Rebate':>9} {'Net':>9} {'WR':>6}", flush=True)
print(f" {'-'*50}", flush=True)
monthly = {}
for t in trades:
k = t.close_time.strftime('%Y-%m')
if k not in monthly:
monthly[k] = {'n': 0, 'pnl': 0, 'reb': 0, 'fee': 0, 'w': 0}
monthly[k]['n'] += 1
monthly[k]['pnl'] += t.pnl
monthly[k]['reb'] += t.rebate
monthly[k]['fee'] += t.fee
if t.pnl > 0: monthly[k]['w'] += 1
for m in sorted(monthly.keys()):
d = monthly[m]
net_m = d['pnl'] - d['fee'] + d['reb']
wr_m = d['w'] / d['n'] * 100 if d['n'] > 0 else 0
print(f" {m:<8} {d['n']:>5} {d['pnl']:>+9.2f} {d['reb']:>9.2f} {net_m:>+9.2f} {wr_m:>5.1f}%", flush=True)
print(f"{'='*70}", flush=True)
def main():
print("Loading data...", flush=True)
data = load()
print(f"{len(data)} bars\n", flush=True)
# Top 3 parameter combos
configs = [
{"name": "A", "fp": 8, "sp": 21, "bp": 120, "atr": 0.003, "sl": 0.004, "mh": 1800},
{"name": "B", "fp": 30, "sp": 80, "bp": 200, "atr": 0.002, "sl": 0.008, "mh": 3600},
{"name": "C", "fp": 8, "sp": 21, "bp": 120, "atr": 0.002, "sl": 0.008, "mh": 1800},
]
risk_levels = [0.005, 0.01, 0.02, 0.03, 0.05]
print(f"{'='*100}", flush=True)
print(f" RISK LEVEL COMPARISON", flush=True)
print(f"{'='*100}", flush=True)
print(f" {'Config':<50} {'Risk%':>6} {'Net%':>7} {'Net$':>9} {'Trades':>7} {'MaxDD':>7}", flush=True)
print(f" {'-'*96}", flush=True)
best_result = None
best_net = -9999
for cfg in configs:
for rp in risk_levels:
label = f"EMA({cfg['fp']}/{cfg['sp']}/{cfg['bp']}) ATR>{cfg['atr']*100:.1f}% SL={cfg['sl']*100:.1f}% MH={cfg['mh']}"
trades, bal, mdd, eq = run_detailed(
data, cfg['fp'], cfg['sp'], cfg['bp'],
cfg['atr'], cfg['sl'], cfg['mh'], risk_pct=rp
)
net = bal - 1000.0
mk = " <<<" if net > 0 else ""
print(f" {label:<50} {rp*100:>5.1f}% {net/10:>+6.2f}% {net:>+8.2f} {len(trades):>7} {mdd*100:>6.2f}%{mk}", flush=True)
if net > best_net:
best_net = net
best_result = (cfg, rp, trades, bal, mdd, eq)
print(f"{'='*100}\n", flush=True)
# Detailed report for best
if best_result:
cfg, rp, trades, bal, mdd, eq = best_result
label = f"EMA({cfg['fp']}/{cfg['sp']}/{cfg['bp']}) ATR>{cfg['atr']*100:.1f}% SL={cfg['sl']*100:.1f}% MH={cfg['mh']} Risk={rp*100:.1f}%"
print_report(f"BEST: {label}", trades, bal, mdd)
# Save trades CSV
csv = Path(__file__).parent.parent / 'best_trades.csv'
with open(csv, 'w', encoding='utf-8-sig') as f:
f.write("open_time,close_time,dir,open_px,close_px,size,pnl,pnl_pct,fee,rebate,hold_sec,reason\n")
for t in trades:
f.write(f"{t.open_time},{t.close_time},{t.direction},"
f"{t.open_price:.2f},{t.close_price:.2f},{t.size:.2f},"
f"{t.pnl:.4f},{t.pnl_pct*100:.4f}%,{t.fee:.4f},{t.rebate:.4f},"
f"{t.hold_seconds:.0f},{t.close_reason}\n")
print(f"\nBest trades saved: {csv}", flush=True)
# Save equity curve
eq_csv = Path(__file__).parent.parent / 'best_equity.csv'
with open(eq_csv, 'w', encoding='utf-8-sig') as f:
f.write("time,equity\n")
for dt, e in eq:
f.write(f"{dt},{e:.2f}\n")
print(f"Equity curve saved: {eq_csv}", flush=True)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,315 @@
"""
盈利信号组合策略 — 只保留回测验证盈利的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()

View File

@@ -0,0 +1,184 @@
"""
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()

View File

@@ -0,0 +1,399 @@
"""
多信号组合策略回测 — 目标 1000 USDT/月
5种信号源同一时间只持一个仓位每笔100U保证金100x杠杆
信号1: EMA金叉死叉 + ATR过滤 + 大趋势方向(已验证盈利)
信号2: 三分之一策略 — 前K线实体的1/3作为触发价动量突破
信号3: 布林带反弹 — 价格触及上下轨 + RSI确认均值回归
信号4: 吞没形态 — 当前K线完全包裹前K线反转信号
信号5: Pin Bar — 长影线蜡烛(拒绝信号)
所有信号共用:
- 大趋势过滤 EMA(120)
- 最低持仓 200秒 (>3分钟)
- 各自独立的止盈止损参数
- 90% 手续费返佣
"""
import sys, time, datetime, sqlite3, statistics
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
gains = 0; losses = 0
for i in range(-period, 0):
d = closes[i] - closes[i-1]
if d > 0: gains += d
else: losses -= d
if losses == 0: return 100.0
rs = (gains/period) / (losses/period)
return 100 - 100 / (1 + rs)
def main():
print("Loading...", flush=True)
data = load()
N = len(data)
print(f"{N} bars loaded\n", flush=True)
# ===== 参数 =====
NOTIONAL = 10000.0 # 100U * 100x
FEE_RATE = 0.0006
REB_RATE = 0.90
MIN_HOLD = 200 # 秒
FEE_PER_TRADE = NOTIONAL * FEE_RATE * 2 # 12 USDT
REB_PER_TRADE = FEE_PER_TRADE * REB_RATE # 10.8 USDT
NET_FEE = FEE_PER_TRADE - REB_PER_TRADE # 1.2 USDT
# 信号参数
# 信号1: EMA交叉
EMA_FAST = 8; EMA_SLOW = 21; EMA_BIG = 120
ATR_MIN = 0.003; ATR_P = 14
SL1 = 0.004; HSL1 = 0.006
# 信号2: 三分之一 (用5分钟聚合K线)
BODY_MIN = 0.0008 # 最小实体占价格比例 0.08%
SL2 = 0.003; TP2 = 0.004 # 三分之一策略:小止损高胜率
# 信号3: 布林带反弹
BB_P = 20; BB_STD = 2.0; RSI_P = 14
RSI_LONG = 30; RSI_SHORT = 70
SL3 = 0.003; TP3 = 0.002 # 均值回归:回到中轨附近
# 信号4: 吞没形态
ENGULF_MIN_BODY = 0.001 # 最小吞没实体占比
SL4 = 0.004; TP4 = 0.005
# 信号5: Pin Bar
PIN_SHADOW_RATIO = 2.0 # 影线 >= 2倍实体
PIN_MIN_SHADOW = 0.001 # 最小影线占价格比例
SL5 = 0.003; TP5 = 0.004
MAX_HOLD = 1800 # 所有信号共用最大持仓
# ===== 状态 =====
ema_f = EMA(EMA_FAST); ema_s = EMA(EMA_SLOW); ema_b = EMA(EMA_BIG)
prev_fast = None; prev_slow = None
closes_buf = []; highs_buf = []; lows_buf = []
# 5分钟K线聚合
bar5_open = None; bar5_high = None; bar5_low = None; bar5_close = None
bar5_count = 0; bars5 = [] # 完成的5分钟K线
pos = 0; op = 0.0; ot = None; sig_type = ""; sl_pct = 0; tp_pct = 0
trades = []
def do_close(price, dt_, reason):
nonlocal pos, op, ot, sig_type
pp = (price - op) / op if pos == 1 else (op - price) / op
pnl = NOTIONAL * pp
fee = FEE_PER_TRADE; reb = REB_PER_TRADE
hsec = (dt_ - ot).total_seconds()
trades.append((sig_type, 'long' if pos==1 else 'short', op, price,
pnl, fee, reb, hsec, reason, ot, dt_))
pos = 0; op = 0; ot = None; sig_type = ""
def do_open(direction, price, dt_, stype, sl, tp):
nonlocal pos, op, ot, sig_type, sl_pct, tp_pct
pos = 1 if direction == 'long' else -1
op = price; ot = dt_; sig_type = stype; sl_pct = sl; tp_pct = tp
for i in range(N):
dt, o_, h_, l_, c_ = data[i]
p = c_
# 更新缓存
closes_buf.append(p); highs_buf.append(h_); lows_buf.append(l_)
if len(closes_buf) > 300:
closes_buf = closes_buf[-300:]
highs_buf = highs_buf[-300:]
lows_buf = lows_buf[-300:]
# EMA更新
fast = ema_f.update(p); slow = ema_s.update(p); big = ema_b.update(p)
# ATR
atr_pct = 0.0
if len(highs_buf) > ATR_P + 1:
s = 0.0
for j in range(-ATR_P, 0):
tr = highs_buf[j] - lows_buf[j]
d1 = abs(highs_buf[j] - closes_buf[j-1])
d2 = abs(lows_buf[j] - closes_buf[j-1])
if d1 > tr: tr = d1
if d2 > tr: tr = d2
s += tr
atr_pct = s / (ATR_P * p) if p > 0 else 0
# EMA交叉
ema_cross_up = prev_fast is not None and prev_fast <= prev_slow and fast > slow
ema_cross_dn = prev_fast is not None and prev_fast >= prev_slow and fast < slow
prev_fast = fast; prev_slow = slow
# 布林带 & RSI
bb_upper, bb_mid, bb_lower = calc_bb(closes_buf, BB_P, BB_STD)
rsi = calc_rsi(closes_buf, RSI_P)
# 5分钟K线聚合
if bar5_open is None:
bar5_open = o_; bar5_high = h_; bar5_low = l_; bar5_close = c_; bar5_count = 1
else:
bar5_high = max(bar5_high, h_)
bar5_low = min(bar5_low, l_)
bar5_close = c_
bar5_count += 1
new_bar5 = None
if bar5_count >= 5:
new_bar5 = {'open': bar5_open, 'high': bar5_high, 'low': bar5_low, 'close': bar5_close}
bars5.append(new_bar5)
if len(bars5) > 50: bars5 = bars5[-50:]
bar5_open = None; bar5_count = 0
# K线形态用1分钟线
prev_bar = data[i-1] if i > 0 else None
prev2_bar = data[i-2] if i > 1 else None
# ===== 有持仓:检查平仓 =====
if pos != 0 and ot is not None:
pp = (p - op) / op if pos == 1 else (op - p) / op
hsec = (dt - ot).total_seconds()
# 硬止损
hard_sl = max(sl_pct * 1.5, 0.006)
if -pp >= hard_sl:
do_close(p, dt, f"硬止损({pp*100:+.2f}%)"); continue
if hsec >= MIN_HOLD:
# 止损
if -pp >= sl_pct:
do_close(p, dt, f"止损({pp*100:+.2f}%)"); continue
# 止盈
if tp_pct > 0 and pp >= tp_pct:
do_close(p, dt, f"止盈({pp*100:+.2f}%)"); continue
# 超时
if hsec >= MAX_HOLD:
do_close(p, dt, f"超时({hsec:.0f}s)"); continue
# EMA信号反转平仓仅EMA信号开的仓
if sig_type == "EMA":
if pos == 1 and ema_cross_dn:
do_close(p, dt, "EMA反转"); continue
if pos == -1 and ema_cross_up:
do_close(p, dt, "EMA反转"); continue
# BB信号回到中轨平仓
if sig_type == "BB" and bb_mid is not None:
if pos == 1 and p >= bb_mid:
do_close(p, dt, "BB回中轨"); continue
if pos == -1 and p <= bb_mid:
do_close(p, dt, "BB回中轨"); continue
# ===== 无持仓:检查开仓(按优先级) =====
if pos == 0 and i > 20:
signal = None; s_sl = 0; s_tp = 0; s_type = ""
# 优先级1: EMA交叉最高质量
if atr_pct >= ATR_MIN:
if ema_cross_up and p > big:
signal = 'long'; s_type = "EMA"; s_sl = SL1; s_tp = 0
elif ema_cross_dn and p < big:
signal = 'short'; s_type = "EMA"; s_sl = SL1; s_tp = 0
# 优先级2: 三分之一策略5分钟K线动量
if signal is None and len(bars5) >= 2 and new_bar5 is not None:
prev5 = bars5[-2]
body5 = abs(prev5['close'] - prev5['open'])
body5_pct = body5 / prev5['close'] if prev5['close'] > 0 else 0
if body5_pct >= BODY_MIN:
trigger_up = prev5['close'] + body5 / 3
trigger_dn = prev5['close'] - body5 / 3
cur5 = bars5[-1]
if cur5['high'] >= trigger_up and p > big:
signal = 'long'; s_type = "1/3"; s_sl = SL2; s_tp = TP2
elif cur5['low'] <= trigger_dn and p < big:
signal = 'short'; s_type = "1/3"; s_sl = SL2; s_tp = TP2
# 优先级3: 吞没形态
if signal is None and prev_bar is not None:
pb_o, pb_c = prev_bar[1], prev_bar[4]
cb_o, cb_c = o_, c_
pb_body = abs(pb_c - pb_o)
cb_body = abs(cb_c - cb_o)
pb_body_pct = pb_body / p if p > 0 else 0
cb_body_pct = cb_body / p if p > 0 else 0
if cb_body_pct >= ENGULF_MIN_BODY and cb_body > pb_body * 1.5:
# 看涨吞没:前阴后阳,当前完全包裹
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_pct >= 0.001:
signal = 'long'; s_type = "吞没"; s_sl = SL4; s_tp = TP4
# 看跌吞没:前阳后阴
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_pct >= 0.001:
signal = 'short'; s_type = "吞没"; s_sl = SL4; s_tp = TP4
# 优先级4: Pin Bar长影线反转
if signal is None and prev_bar is not None:
pb_o, pb_h, pb_l, pb_c = prev_bar[1], prev_bar[2], prev_bar[3], prev_bar[4]
pb_body = abs(pb_c - pb_o)
upper_shadow = pb_h - max(pb_o, pb_c)
lower_shadow = min(pb_o, pb_c) - pb_l
if pb_body > 0:
# 看涨Pin Bar长下影线
if lower_shadow >= PIN_SHADOW_RATIO * pb_body:
ls_pct = lower_shadow / p if p > 0 else 0
if ls_pct >= PIN_MIN_SHADOW and p > big and atr_pct >= 0.001:
signal = 'long'; s_type = "PinBar"; s_sl = SL5; s_tp = TP5
# 看跌Pin Bar长上影线
if upper_shadow >= PIN_SHADOW_RATIO * pb_body:
us_pct = upper_shadow / p if p > 0 else 0
if us_pct >= PIN_MIN_SHADOW and p < big and atr_pct >= 0.001:
signal = 'short'; s_type = "PinBar"; s_sl = SL5; s_tp = TP5
# 优先级5: 布林带反弹
if signal is None and bb_upper is not None and rsi is not None:
if p <= bb_lower and rsi < RSI_LONG and p > big and atr_pct >= 0.0008:
signal = 'long'; s_type = "BB"; s_sl = SL3; s_tp = TP3
elif p >= bb_upper and rsi > RSI_SHORT and p < big and atr_pct >= 0.0008:
signal = 'short'; s_type = "BB"; s_sl = SL3; s_tp = TP3
if signal:
do_open(signal, p, dt, s_type, s_sl, s_tp)
# 强制平仓
if pos != 0:
p = data[-1][4]; dt = data[-1][0]
do_close(p, dt, "回测结束")
# ===== 分析结果 =====
if not trades:
print("No trades!", flush=True); return
n = len(trades)
total_pnl = sum(t[4] for t in trades)
total_fee = FEE_PER_TRADE * n
total_reb = REB_PER_TRADE * n
net = total_pnl - (total_fee - total_reb)
wins = [t for t in trades if t[4] > 0]
wr = len(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, 'pnl':0, 'reb':0, 'fee':0, 'w':0})
for t in trades:
k = t[10].strftime('%Y-%m')
monthly[k]['n'] += 1
monthly[k]['pnl'] += t[4]
monthly[k]['reb'] += REB_PER_TRADE
monthly[k]['fee'] += FEE_PER_TRADE
if t[4] > 0: monthly[k]['w'] += 1
# 最大回撤
cum=0; peak=0; dd=0
for t in trades:
cum += t[4] - NET_FEE
if cum > peak: peak = cum
if peak - cum > dd: dd = peak - cum
print("=" * 75, flush=True)
print(" 多信号组合策略回测 | 100U x 100倍 | 目标1000U/月", flush=True)
print("=" * 75, flush=True)
print(f"\n --- 核心收益 ---", flush=True)
print(f" 方向盈亏: {total_pnl:>+12.2f} USDT", flush=True)
print(f" 返佣(90%): {total_reb:>+12.2f} USDT", flush=True)
print(f" 净手续费(10%): {total_fee-total_reb:>12.2f} USDT", flush=True)
print(f" ================================", flush=True)
print(f" 年净利润: {net:>+12.2f} USDT", flush=True)
print(f" 月均净利: {net/12:>+12.2f} USDT", flush=True)
print(f" 最大回撤: {dd:>12.2f} USDT", flush=True)
print(f"\n --- 交易统计 ---", flush=True)
print(f" 总交易: {n} 笔 | 胜率: {wr:.1f}% | 月均: {n/12:.0f}", flush=True)
print(f"\n --- 按信号类型 ---", flush=True)
print(f" {'类型':<10} {'笔数':>6} {'方向盈亏':>10} {'净利':>10} {'胜率':>6} {'每笔均利':>10}", flush=True)
print(f" {'-'*56}", flush=True)
for stype in sorted(by_type.keys()):
d = by_type[stype]
net_t = d['pnl'] - NET_FEE * d['n']
avg = net_t / d['n'] if d['n'] > 0 else 0
wr_t = d['w'] / d['n'] * 100 if d['n'] > 0 else 0
mark = " ++" if net_t > 0 else " --"
print(f" {stype:<10} {d['n']:>6} {d['pnl']:>+10.2f} {net_t:>+10.2f} {wr_t:>5.1f}% {avg:>+10.2f}{mark}", flush=True)
print(f"\n --- 月度明细 ---", flush=True)
print(f" {'月份':<8} {'笔数':>5} {'方向盈亏':>10} {'返佣':>8} {'净利润':>10} {'胜率':>6}", flush=True)
print(f" {'-'*52}", flush=True)
for m in sorted(monthly.keys()):
d = monthly[m]
net_m = d['pnl'] - (d['fee'] - d['reb'])
wr_m = d['w'] / d['n'] * 100 if d['n'] > 0 else 0
print(f" {m:<8} {d['n']:>5} {d['pnl']:>+10.2f} {d['reb']:>8.2f} {net_m:>+10.2f} {wr_m:>5.1f}%", flush=True)
print(f" {'-'*52}", flush=True)
print(f" {'合计':<8} {n:>5} {total_pnl:>+10.2f} {total_reb:>8.2f} {net:>+10.2f} {wr:>5.1f}%", flush=True)
# ===== 测试不同仓位大小达到1000U/月需要多少 =====
print(f"\n --- 仓位放大测试 ---", flush=True)
print(f" {'保证金':>8} {'杠杆':>4} {'名义价值':>12} {'年净利':>10} {'月均':>8} {'达标':>4}", flush=True)
print(f" {'-'*52}", flush=True)
for margin in [100, 200, 300, 500, 800, 1000]:
lev = 100
notional_test = margin * lev
scale = notional_test / NOTIONAL
net_scaled = net * scale
monthly_avg = net_scaled / 12
ok = "Yes" if monthly_avg >= 1000 else "No"
print(f" {margin:>7}U {lev:>3}x {notional_test:>11,}U {net_scaled:>+10.0f} {monthly_avg:>+8.0f} {ok:>4}", flush=True)
print(f"\n{'='*75}", flush=True)
# 保存CSV
csv = Path(__file__).parent.parent / 'combo_trades.csv'
with open(csv, 'w', encoding='utf-8-sig') as f:
f.write("信号,方向,开仓价,平仓价,方向盈亏,手续费,返佣,持仓秒,原因,开仓时间,平仓时间\n")
for t in trades:
f.write(f"{t[0]},{t[1]},{t[2]:.2f},{t[3]:.2f},{t[4]:.2f},"
f"{t[5]:.2f},{t[6]:.2f},{t[7]:.0f},{t[8]},{t[9]},{t[10]}\n")
print(f"\n Saved: {csv}", flush=True)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,338 @@
"""
终极组合策略回测 — 多时间框架 + 波动率自适应
核心改进:
1. 吞没形态用5分钟K线减少噪音提高质量
2. 加入突破策略N根K线新高新低突破
3. 波动率自适应:高波动用趋势跟踪,低波动用均值回归
4. 动态止盈使用ATR倍数作为止盈目标而非固定比例
5. 趋势强度过滤EMA斜率+ADX概念
信号:
A) EMA(8/21) 金叉死叉 + ATR>0.3% + EMA(120)趋势方向
B) 5分钟吞没形态 + EMA趋势方向 + ATR确认
C) 20根K线高低突破 + 趋势方向
D) BB反弹 + RSI极值 + 趋势方向(仅逆趋势小单)
条件: 同时只持1个仓, 90%返佣, >3分钟持仓
"""
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 run(data, notional):
N = len(data)
FEE = notional * 0.0006 * 2
REB = FEE * 0.9
NFEE = FEE - REB
MIN_HOLD = 200; MAX_HOLD = 1800
# EMA
ef = EMA(8); es = EMA(21); eb = EMA(120); e50 = EMA(50)
pf_ = None; ps_ = None
CB = []; HB = []; LB = []
# 5分钟聚合
b5_o=None; b5_h=None; b5_l=None; b5_c=None; b5_cnt=0
bars5 = []
# 突破追踪
high_20 = []; low_20 = []
pos=0; op=0.0; ot=None; st=""; sl=0; tp_atr=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=ef.update(p); slow=es.update(p); big=eb.update(p); mid50=e50.update(p)
# ATR
atr=0.0; atr_val=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_val = s/14
atr = atr_val/p if p>0 else 0
# EMA交叉
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
# EMA趋势强度斜率
trend_up = fast > slow and slow > big # 三线多头
trend_dn = fast < slow and slow < big # 三线空头
# BB & RSI
bb_u=None; bb_m=None; bb_l=None; rsi=None
if len(CB)>=20:
rec=CB[-20:]; mid=sum(rec)/20
var=sum((x-mid)**2 for x in rec)/20; std=var**0.5
bb_u=mid+2*std; bb_m=mid; bb_l=mid-2*std
if len(CB)>=15:
g=0; l=0
for j in range(-14,0):
d=CB[j]-CB[j-1]
if d>0: g+=d
else: l-=d
rsi = 100-100/(1+(g/14)/(l/14)) if l>0 else 100
# 5分钟聚合
if b5_o is None:
b5_o=o_; b5_h=h_; b5_l=l_; b5_c=c_; b5_cnt=1
else:
b5_h=max(b5_h,h_); b5_l=min(b5_l,l_); b5_c=c_; b5_cnt+=1
new_b5 = False
if b5_cnt>=5:
bars5.append({'o':b5_o,'h':b5_h,'l':b5_l,'c':b5_c})
if len(bars5)>30: bars5=bars5[-30:]
b5_o=None; b5_cnt=0; new_b5=True
# 20根K线高低用于突破
high_20.append(h_); low_20.append(l_)
if len(high_20)>21: high_20=high_20[-21:]; low_20=low_20[-21:]
breakout_high = max(high_20[:-1]) if len(high_20)>1 else None
breakout_low = min(low_20[:-1]) if len(low_20)>1 else None
# ===== 持仓管理 =====
if pos!=0 and ot:
pp=(p-op)/op if pos==1 else (op-p)/op
hsec=(dt-ot).total_seconds()
# 动态止损/止盈基于ATR
hard_sl = max(sl*1.5, 0.006)
if -pp>=hard_sl: close_(p,dt,"硬止损"); continue
if hsec>=MIN_HOLD:
if -pp>=sl: close_(p,dt,"止损"); continue
# 动态止盈:盈利超过 tp_atr 倍ATR值
if tp_atr>0 and atr_val>0:
tp_target = tp_atr * atr_val / op # 转为百分比
if pp>=tp_target: close_(p,dt,f"ATR止盈({pp*100:.2f}%)"); continue
if hsec>=MAX_HOLD: close_(p,dt,"超时"); continue
# 趋势跟踪出场
if st in ("EMA","突破"):
if pos==1 and cd: close_(p,dt,"反向交叉"); continue
if pos==-1 and cu: close_(p,dt,"反向交叉"); continue
# 价格跌破慢线止盈
if pos==1 and p<slow and pp>0.001: close_(p,dt,"破慢线止盈"); continue
if pos==-1 and p>slow and pp>0.001: close_(p,dt,"破慢线止盈"); 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 st=="5m吞没":
if pos==1 and cd: close_(p,dt,"反向交叉"); continue
if pos==-1 and cu: close_(p,dt,"反向交叉"); continue
# ===== 开仓 =====
if pos==0 and i>120:
sig=None; s_sl=0; s_tp_atr=0; s_type=""
# 信号A: EMA交叉最高质量
if atr>=0.003:
if cu and p>big:
sig='L'; s_type="EMA"; s_sl=0.004; s_tp_atr=3.0
elif cd and p<big:
sig='S'; s_type="EMA"; s_sl=0.004; s_tp_atr=3.0
# 信号B: 5分钟吞没仅在新5分钟K线完成时检查
if sig is None and new_b5 and len(bars5)>=3:
prev5 = bars5[-2]; cur5 = bars5[-1]
pb = abs(prev5['c']-prev5['o'])
cb = abs(cur5['c']-cur5['o'])
cb_pct = cb/p if p>0 else 0
if cb_pct>=0.0015 and cb>pb*1.5 and atr>=0.0015:
# 看涨吞没
if prev5['c']<prev5['o'] and cur5['c']>cur5['o']:
if cur5['c']>prev5['o'] and cur5['o']<=prev5['c']:
if p>big:
sig='L'; s_type="5m吞没"; s_sl=0.005; s_tp_atr=2.5
# 看跌吞没
elif prev5['c']>prev5['o'] and cur5['c']<cur5['o']:
if cur5['c']<prev5['o'] and cur5['o']>=prev5['c']:
if p<big:
sig='S'; s_type="5m吞没"; s_sl=0.005; s_tp_atr=2.5
# 信号C: 突破20根K线新高新低
if sig is None and breakout_high and atr>=0.002:
if h_>breakout_high and trend_up:
sig='L'; s_type="突破"; s_sl=0.005; s_tp_atr=2.5
elif l_<breakout_low and trend_dn:
sig='S'; s_type="突破"; s_sl=0.005; s_tp_atr=2.5
# 信号D: BB反弹要求RSI极值 + ATR适中
if sig is None and bb_u and rsi is not None and 0.001<=atr<=0.004:
if p<=bb_l and rsi<25 and p>big:
sig='L'; s_type="BB"; s_sl=0.003; s_tp_atr=0
elif p>=bb_u and rsi>75 and p<big:
sig='S'; s_type="BB"; s_sl=0.003; s_tp_atr=0
if sig:
pos=1 if sig=='L' else -1; op=p; ot=dt
st=s_type; sl=s_sl; tp_atr=s_tp_atr
if pos!=0: close_(data[-1][4], data[-1][0], "结束")
return trades
def report(trades, notional, label):
if not trades: print(f" [{label}] No trades"); 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
pm=len([m for m in monthly.values() if m['net']>0])
min_m=min(monthly.values(),key=lambda x:x['net'])['net']
max_m=max(monthly.values(),key=lambda x:x['net'])['net']
print(f"\n{'='*75}", flush=True)
print(f" {label} | 名义值={notional:,.0f}U", flush=True)
print(f"{'='*75}", flush=True)
print(f" 年净利: {net:>+10.0f} | 月均: {net/12:>+8.0f} | 交易: {n}笔 | 胜率: {wr:.1f}%", flush=True)
print(f" 返佣: {total_reb:>.0f} | 回撤: {dd:>.0f} | 盈利月: {pm}/12", flush=True)
print(f" 最佳月: {max_m:>+.0f} | 最差月: {min_m:>+.0f}", 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
avg=nt/d['n'] if d['n']>0 else 0
mk="+" if nt>0 else "-"
print(f" {st:<8} {d['n']:>5}笔 净{nt:>+8.0f}{wt:.0f}% 均{avg:>+.1f}/笔 {mk}", flush=True)
print(f"\n 月度:", flush=True)
for m in sorted(monthly.keys()):
d=monthly[m]; wr_m=d['w']/d['n']*100 if d['n']>0 else 0
bar = "+" * max(0, int(d['net']/200)) + "-" * max(0, int(-d['net']/200))
print(f" {m} {d['n']:>4}{d['net']:>+8.0f} {wr_m:>4.0f}% {bar}", flush=True)
print(f" {'合计':>7} {n:>4}{net:>+8.0f}", flush=True)
print(f"{'='*75}", flush=True)
return net, monthly
def main():
print("Loading...", flush=True)
data = load()
print(f"{len(data)} bars\n", flush=True)
# 测试不同仓位大小
margins = [100, 200, 300, 500, 800, 1000]
print("="*80, flush=True)
print(" 不同保证金下的收益 (100x杠杆)", flush=True)
print("="*80, flush=True)
all_results = []
for margin in margins:
notional = margin * 100
trades = run(data, notional)
net, monthly = report(trades, notional, f"{margin}U保证金")
all_results.append((margin, notional, len(trades), net, monthly))
# 总览
print(f"\n\n{'='*80}", flush=True)
print(f" 总览 — 目标: 每月 1000 USDT", flush=True)
print(f"{'='*80}", flush=True)
print(f" {'保证金':>6} {'杠杆':>4} {'名义值':>10} {'交易':>5} {'年净利':>10} {'月均':>8} {'达标':>4}", flush=True)
print(f" {'-'*52}", flush=True)
for margin, notional, n, net, monthly in all_results:
mavg = net/12
ok = "YES" if mavg>=1000 else "no"
print(f" {margin:>5}U {100:>3}x {notional:>9,}U {n:>5} {net:>+10.0f} {mavg:>+8.0f} {ok:>4}", flush=True)
# 找到达标的最小保证金
print(f"\n 结论:", flush=True)
for margin, notional, n, net, monthly in all_results:
if net/12 >= 1000:
print(f" >>> {margin}U 保证金即可达到月均 {net/12:.0f} USDT <<<", flush=True)
# 打印该配置的月度
print(f"\n {margin}U配置月度明细:", flush=True)
pm = 0
for m in sorted(monthly.keys()):
d = monthly[m]
status = "" if d['net']>0 else ""
print(f" {m}: {d['net']:>+8.0f} USDT ({d['n']}笔) [{status}]", flush=True)
if d['net'] > 0: pm += 1
print(f" 盈利月份: {pm}/12", flush=True)
break
else:
# 没有达标的,计算需要多少
base_net = all_results[0][3] # 100U的净利
if base_net > 0:
needed = int(12000 / base_net * 100) + 1
print(f" 100U净利={base_net:.0f}/年 → 达标需约 {needed}U 保证金", flush=True)
else:
print(f" 策略本身不盈利,需要继续优化信号质量", flush=True)
print(f"{'='*80}", flush=True)
# 保存最佳配置的交易记录
best = max(all_results, key=lambda x: x[3])
margin, notional = best[0], best[1]
trades = run(data, notional)
csv = Path(__file__).parent.parent / 'final_trades.csv'
with open(csv, 'w', encoding='utf-8-sig') as f:
f.write("信号,方向,开仓价,平仓价,盈亏,持仓秒,原因,开仓时间,平仓时间\n")
for t in trades:
f.write(f"{t[0]},{t[1]},{t[2]:.2f},{t[3]:.2f},{t[4]:.2f},{t[5]:.0f},{t[6]},{t[7]},{t[8]}\n")
print(f"\n 交易记录: {csv}", flush=True)
if __name__=='__main__':
main()

View File

@@ -0,0 +1,542 @@
"""
BitMart 布林带均值回归返佣策略 — 回测脚本
从 SQLite 数据库读取 2025 年全年 1 分钟 K 线数据,模拟策略运行。
输出:交易次数、胜率、盈亏、返佣收入、净收益、最大回撤、月度统计等。
用法:
python 交易/bitmart-返佣策略-回测.py
"""
import time
import datetime
import statistics
import sqlite3
from pathlib import Path
from dataclasses import dataclass, field
from typing import List, Optional
# 简易 logger避免依赖 loguru
class _Logger:
@staticmethod
def info(msg): print(f"[INFO] {msg}")
@staticmethod
def success(msg): print(f"[OK] {msg}")
@staticmethod
def warning(msg): print(f"[WARN] {msg}")
@staticmethod
def error(msg): print(f"[ERR] {msg}")
logger = _Logger()
# ========================= 交易记录 =========================
@dataclass
class Trade:
"""单笔交易记录"""
open_time: datetime.datetime # 开仓时间
close_time: datetime.datetime # 平仓时间
direction: str # 'long' / 'short'
open_price: float # 开仓价
close_price: float # 平仓价
size: float # 仓位大小 (USDT)
pnl: float # 盈亏 (USDT)
pnl_pct: float # 盈亏百分比
fee: float # 手续费
rebate: float # 返佣
hold_seconds: float # 持仓时间(秒)
close_reason: str # 平仓原因
# ========================= 回测引擎 =========================
class RebateBacktest:
def __init__(
self,
# 布林带参数
bb_period: int = 20,
bb_std: float = 2.0,
rsi_period: int = 14,
rsi_long_threshold: float = 35,
rsi_short_threshold: float = 65,
# 持仓管理
min_hold_seconds: int = 200,
max_hold_seconds: int = 900,
stop_loss_pct: float = 0.003,
hard_stop_pct: float = 0.0045,
take_profit_pct: float = 0.0002,
# 仓位 & 费用
initial_balance: float = 1000.0,
leverage: int = 50,
risk_percent: float = 0.005,
taker_fee_rate: float = 0.0006,
rebate_rate: float = 0.90,
# 时间范围
start_date: str = '2025-01-01',
end_date: str = '2025-12-31',
):
# 策略参数
self.bb_period = bb_period
self.bb_std = bb_std
self.rsi_period = rsi_period
self.rsi_long_threshold = rsi_long_threshold
self.rsi_short_threshold = rsi_short_threshold
self.min_hold_seconds = min_hold_seconds
self.max_hold_seconds = max_hold_seconds
self.stop_loss_pct = stop_loss_pct
self.hard_stop_pct = hard_stop_pct
self.take_profit_pct = take_profit_pct
self.initial_balance = initial_balance
self.leverage = leverage
self.risk_percent = risk_percent
self.taker_fee_rate = taker_fee_rate
self.rebate_rate = rebate_rate
self.start_date = start_date
self.end_date = end_date
# 状态
self.balance = initial_balance
self.position = 0 # -1 空, 0 无, 1 多
self.open_price = 0.0
self.open_time = None # datetime
self.position_size = 0.0 # 开仓金额 (USDT)
# 结果
self.trades: List[Trade] = []
self.equity_curve: List[dict] = [] # [{datetime, equity}]
self.peak_equity = initial_balance
self.max_drawdown = 0.0
self.max_drawdown_pct = 0.0
# ========================= 技术指标 =========================
@staticmethod
def calc_bb(closes: list, period: int, num_std: float):
"""布林带"""
if len(closes) < period:
return None, None, None
recent = closes[-period:]
mid = statistics.mean(recent)
std = statistics.stdev(recent)
return mid + num_std * std, mid, mid - num_std * std
@staticmethod
def calc_rsi(closes: list, period: int):
"""RSI"""
if len(closes) < period + 1:
return None
gains, losses = [], []
for i in range(-period, 0):
change = closes[i] - closes[i - 1]
gains.append(max(change, 0))
losses.append(max(-change, 0))
avg_gain = sum(gains) / period
avg_loss = sum(losses) / period
if avg_loss == 0:
return 100.0
rs = avg_gain / avg_loss
return 100 - (100 / (1 + rs))
# ========================= 数据加载 =========================
def load_data(self) -> list:
"""从 SQLite 读取 1 分钟 K 线"""
db_path = Path(__file__).parent.parent / 'models' / 'database.db'
if not db_path.exists():
raise FileNotFoundError(f"数据库不存在: {db_path}")
start_dt = datetime.datetime.strptime(self.start_date, '%Y-%m-%d')
end_dt = datetime.datetime.strptime(self.end_date, '%Y-%m-%d') + datetime.timedelta(days=1)
start_ms = int(start_dt.timestamp()) * 1000
end_ms = int(end_dt.timestamp()) * 1000
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute(
"SELECT id, open, high, low, close FROM bitmart_eth_1m "
"WHERE id >= ? AND id < ? ORDER BY id",
(start_ms, end_ms)
)
rows = cursor.fetchall()
conn.close()
data = []
for row in rows:
ts_ms = row[0]
ts_sec = ts_ms / 1000.0
dt = datetime.datetime.fromtimestamp(ts_sec)
data.append({
'datetime': dt,
'timestamp': ts_sec,
'open': row[1],
'high': row[2],
'low': row[3],
'close': row[4],
})
logger.info(f"加载数据: {len(data)} 条 1分钟K线 ({self.start_date} ~ {self.end_date})")
if data:
logger.info(f" 起始: {data[0]['datetime']} | 结束: {data[-1]['datetime']}")
return data
# ========================= 开平仓模拟 =========================
def _calc_size(self):
"""计算开仓金额"""
return self.balance * self.risk_percent * self.leverage
def _open(self, direction: str, price: float, dt: datetime.datetime):
"""模拟开仓"""
self.position_size = self._calc_size()
if self.position_size < 1:
return False
# 扣手续费
fee = self.position_size * self.taker_fee_rate
self.balance -= fee
self.position = 1 if direction == 'long' else -1
self.open_price = price
self.open_time = dt
return True
def _close(self, price: float, dt: datetime.datetime, reason: str):
"""模拟平仓,返回 Trade"""
if self.position == 0:
return None
# 计算盈亏
if self.position == 1:
pnl_pct = (price - self.open_price) / self.open_price
else:
pnl_pct = (self.open_price - price) / self.open_price
pnl = self.position_size * pnl_pct
# 平仓手续费
close_value = self.position_size * (1 + pnl_pct)
fee = close_value * self.taker_fee_rate
# 总手续费(开+平)
open_fee = self.position_size * self.taker_fee_rate
total_fee = open_fee + fee
# 返佣
rebate = total_fee * self.rebate_rate
# 更新余额:加上盈亏 - 平仓手续费 + 返佣
self.balance += pnl - fee + rebate
hold_seconds = (dt - self.open_time).total_seconds()
trade = Trade(
open_time=self.open_time,
close_time=dt,
direction='long' if self.position == 1 else 'short',
open_price=self.open_price,
close_price=price,
size=self.position_size,
pnl=pnl,
pnl_pct=pnl_pct,
fee=total_fee,
rebate=rebate,
hold_seconds=hold_seconds,
close_reason=reason,
)
self.trades.append(trade)
# 重置持仓
self.position = 0
self.open_price = 0.0
self.open_time = None
self.position_size = 0.0
return trade
# ========================= 回测主循环 =========================
def run(self):
"""运行回测"""
data = self.load_data()
if len(data) < self.bb_period + self.rsi_period + 1:
logger.error("数据不足,无法回测")
return
closes = []
total_bars = len(data)
log_interval = total_bars // 20 # 打印 20 次进度
logger.info(f"开始回测... 共 {total_bars} 根 K 线")
t0 = time.time()
for i, bar in enumerate(data):
price = bar['close']
dt = bar['datetime']
closes.append(price)
# 需要足够数据才能计算指标
if len(closes) < max(self.bb_period, self.rsi_period + 1) + 1:
continue
# 计算指标
upper, middle, lower = self.calc_bb(closes, self.bb_period, self.bb_std)
rsi = self.calc_rsi(closes, self.rsi_period)
if upper is None or rsi is None:
continue
# —— 有持仓:检查平仓 ——
if self.position != 0 and self.open_time:
hold_sec = (dt - self.open_time).total_seconds()
# 计算当前浮动盈亏百分比
if self.position == 1:
cur_pnl_pct = (price - self.open_price) / self.open_price
else:
cur_pnl_pct = (self.open_price - price) / self.open_price
# ① 硬止损(不受持仓时间限制)
if -cur_pnl_pct >= self.hard_stop_pct:
self._close(price, dt, f"硬止损 ({cur_pnl_pct*100:+.3f}%)")
continue
# ② 满足最低持仓时间后的平仓条件
if hold_sec >= self.min_hold_seconds:
# 止盈:回归中轨
if self.position == 1 and price >= middle * (1 - self.take_profit_pct):
self._close(price, dt, "止盈回归中轨")
continue
if self.position == -1 and price <= middle * (1 + self.take_profit_pct):
self._close(price, dt, "止盈回归中轨")
continue
# 止损
if -cur_pnl_pct >= self.stop_loss_pct:
self._close(price, dt, f"止损 ({cur_pnl_pct*100:+.3f}%)")
continue
# 超时
if hold_sec >= self.max_hold_seconds:
self._close(price, dt, f"超时 ({hold_sec:.0f}s)")
continue
# —— 无持仓:检查开仓 ——
if self.position == 0:
if price <= lower and rsi < self.rsi_long_threshold:
self._open('long', price, dt)
elif price >= upper and rsi > self.rsi_short_threshold:
self._open('short', price, dt)
# 记录权益曲线(每小时记录一次)
if i % 60 == 0:
equity = self.balance
if self.position != 0 and self.open_price > 0:
if self.position == 1:
unrealized = self.position_size * (price - self.open_price) / self.open_price
else:
unrealized = self.position_size * (self.open_price - price) / self.open_price
equity += unrealized
self.equity_curve.append({'datetime': dt, 'equity': equity})
# 最大回撤
if equity > self.peak_equity:
self.peak_equity = equity
dd = (self.peak_equity - equity) / self.peak_equity
if dd > self.max_drawdown_pct:
self.max_drawdown_pct = dd
self.max_drawdown = self.peak_equity - equity
# 进度
if log_interval > 0 and i % log_interval == 0 and i > 0:
pct = i / total_bars * 100
logger.info(f" 进度 {pct:.0f}% | 余额 {self.balance:.2f} | 交易 {len(self.trades)}")
elapsed = time.time() - t0
logger.info(f"回测完成,耗时 {elapsed:.1f}s")
# 如果还有持仓,强制平仓
if self.position != 0:
last_bar = data[-1]
self._close(last_bar['close'], last_bar['datetime'], "回测结束强制平仓")
self.print_results()
# ========================= 结果输出 =========================
def print_results(self):
"""打印详细回测结果"""
trades = self.trades
if not trades:
logger.warning("无交易记录")
return
# 基础统计
total_trades = len(trades)
wins = [t for t in trades if t.pnl > 0]
losses = [t for t in trades if t.pnl <= 0]
win_rate = len(wins) / total_trades * 100
total_pnl = sum(t.pnl for t in trades)
total_fee = sum(t.fee for t in trades)
total_rebate = sum(t.rebate for t in trades)
net_profit = self.balance - self.initial_balance
avg_pnl = total_pnl / total_trades
avg_win = statistics.mean([t.pnl for t in wins]) if wins else 0
avg_loss = statistics.mean([t.pnl for t in losses]) if losses else 0
avg_hold = statistics.mean([t.hold_seconds for t in trades])
total_volume = sum(t.size for t in trades) * 2 # 开+平
# 盈亏比
profit_factor = (sum(t.pnl for t in wins) / abs(sum(t.pnl for t in losses))) if losses and sum(t.pnl for t in losses) != 0 else float('inf')
# 连续亏损
max_consecutive_loss = 0
current_loss_streak = 0
for t in trades:
if t.pnl <= 0:
current_loss_streak += 1
max_consecutive_loss = max(max_consecutive_loss, current_loss_streak)
else:
current_loss_streak = 0
# 长/空统计
long_trades = [t for t in trades if t.direction == 'long']
short_trades = [t for t in trades if t.direction == 'short']
long_wins = len([t for t in long_trades if t.pnl > 0])
short_wins = len([t for t in short_trades if t.pnl > 0])
# 平仓原因统计
close_reasons = {}
for t in trades:
r = t.close_reason.split(' (')[0] # 去掉括号部分
close_reasons[r] = close_reasons.get(r, 0) + 1
print("\n" + "=" * 70)
print(f" 布林带均值回归返佣策略 — 回测报告")
print(f" 回测区间: {self.start_date} ~ {self.end_date}")
print("=" * 70)
print(f"\n{''*35} 账户 {''*35}")
print(f" 初始资金: {self.initial_balance:>12.2f} USDT")
print(f" 最终余额: {self.balance:>12.2f} USDT")
print(f" 净收益: {net_profit:>+12.2f} USDT ({net_profit/self.initial_balance*100:+.2f}%)")
print(f" 最大回撤: {self.max_drawdown:>12.2f} USDT ({self.max_drawdown_pct*100:.2f}%)")
print(f"\n{''*35} 交易 {''*35}")
print(f" 总交易次数: {total_trades:>8}")
print(f" 盈利次数: {len(wins):>8} ({win_rate:.1f}%)")
print(f" 亏损次数: {len(losses):>8} ({100-win_rate:.1f}%)")
print(f" 做多交易: {len(long_trades):>8} (胜率 {long_wins/len(long_trades)*100:.1f}%)" if long_trades else "")
print(f" 做空交易: {len(short_trades):>8} (胜率 {short_wins/len(short_trades)*100:.1f}%)" if short_trades else "")
print(f" 盈亏比: {profit_factor:>8.2f}")
print(f" 最大连续亏损: {max_consecutive_loss:>8}")
print(f"\n{''*35} 盈亏 {''*35}")
print(f" 交易总盈亏: {total_pnl:>+12.4f} USDT")
print(f" 平均每笔盈亏: {avg_pnl:>+12.4f} USDT")
print(f" 平均盈利: {avg_win:>+12.4f} USDT")
print(f" 平均亏损: {avg_loss:>+12.4f} USDT")
print(f" 最大单笔盈利: {max(t.pnl for t in trades):>+12.4f} USDT")
print(f" 最大单笔亏损: {min(t.pnl for t in trades):>+12.4f} USDT")
print(f"\n{''*35} 手续费 & 返佣 {''*28}")
print(f" 交易总额: {total_volume:>12.2f} USDT")
print(f" 总手续费: {total_fee:>12.4f} USDT")
print(f" 总返佣 (90%): {total_rebate:>+12.4f} USDT")
print(f" 净手续费成本: {total_fee - total_rebate:>12.4f} USDT")
print(f" 返佣占净收益: {total_rebate/net_profit*100:.1f}%" if net_profit != 0 else " 返佣占净收益: N/A")
print(f"\n{''*35} 持仓 {''*35}")
print(f" 平均持仓时间: {avg_hold:>8.0f} 秒 ({avg_hold/60:.1f} 分钟)")
print(f" 最短持仓: {min(t.hold_seconds for t in trades):>8.0f}")
print(f" 最长持仓: {max(t.hold_seconds for t in trades):>8.0f}")
# 持仓<3分钟的交易数应该只有硬止损的
under_3min = len([t for t in trades if t.hold_seconds < 180])
print(f" 持仓<3分钟: {under_3min:>8} 笔 (仅硬止损触发)")
print(f"\n{''*35} 平仓原因 {''*31}")
for reason, count in sorted(close_reasons.items(), key=lambda x: -x[1]):
print(f" {reason:<20} {count:>6} 笔 ({count/total_trades*100:.1f}%)")
# ========================= 月度统计 =========================
print(f"\n{''*35} 月度统计 {''*31}")
print(f" {'月份':<10} {'交易数':>6} {'盈利':>10} {'返佣':>10} {'净收益':>10} {'胜率':>6}")
print(f" {''*56}")
monthly = {}
for t in trades:
key = t.close_time.strftime('%Y-%m')
if key not in monthly:
monthly[key] = {'trades': 0, 'pnl': 0, 'rebate': 0, 'wins': 0}
monthly[key]['trades'] += 1
monthly[key]['pnl'] += t.pnl
monthly[key]['rebate'] += t.rebate
if t.pnl > 0:
monthly[key]['wins'] += 1
for month in sorted(monthly.keys()):
m = monthly[month]
net = m['pnl'] + m['rebate'] - (sum(t.fee for t in trades if t.close_time.strftime('%Y-%m') == month) * (1 - self.rebate_rate))
wr = m['wins'] / m['trades'] * 100 if m['trades'] > 0 else 0
print(f" {month:<10} {m['trades']:>6} {m['pnl']:>+10.2f} {m['rebate']:>10.2f} {m['pnl']+m['rebate']:>+10.2f} {wr:>5.1f}%")
print("=" * 70)
# ========================= 保存交易记录到 CSV =========================
csv_path = Path(__file__).parent.parent / '回测结果.csv'
try:
with open(csv_path, 'w', encoding='utf-8-sig') as f:
f.write("开仓时间,平仓时间,方向,开仓价,平仓价,仓位(USDT),盈亏(USDT),盈亏%,手续费,返佣,持仓秒数,平仓原因\n")
for t in trades:
f.write(
f"{t.open_time},{t.close_time},{t.direction},"
f"{t.open_price:.2f},{t.close_price:.2f},{t.size:.2f},"
f"{t.pnl:.4f},{t.pnl_pct*100:.4f}%,{t.fee:.4f},{t.rebate:.4f},"
f"{t.hold_seconds:.0f},{t.close_reason}\n"
)
logger.info(f"交易记录已保存到: {csv_path}")
except Exception as e:
logger.error(f"保存 CSV 失败: {e}")
# ========================= 保存权益曲线 =========================
equity_path = Path(__file__).parent.parent / '权益曲线.csv'
try:
with open(equity_path, 'w', encoding='utf-8-sig') as f:
f.write("时间,权益\n")
for e in self.equity_curve:
f.write(f"{e['datetime']},{e['equity']:.2f}\n")
logger.info(f"权益曲线已保存到: {equity_path}")
except Exception as e:
logger.error(f"保存权益曲线失败: {e}")
if __name__ == '__main__':
bt = RebateBacktest(
# 布林带参数
bb_period=20,
bb_std=2.0,
rsi_period=14,
rsi_long_threshold=35,
rsi_short_threshold=65,
# 持仓管理
min_hold_seconds=200, # >3分钟
max_hold_seconds=900, # 15分钟
stop_loss_pct=0.003, # 0.3% 止损
hard_stop_pct=0.0045, # 0.45% 硬止损
take_profit_pct=0.0002, # 中轨容差
# 仓位 & 费用
initial_balance=1000.0, # 初始 1000 USDT
leverage=50,
risk_percent=0.005, # 0.5%
taker_fee_rate=0.0006, # 0.06%
rebate_rate=0.90, # 90% 返佣
# 时间范围
start_date='2025-01-01',
end_date='2025-12-31',
)
bt.run()

View File

@@ -0,0 +1,763 @@
"""
BitMart EMA趋势跟随返佣策略
核心思路:
你有交易所 90% 手续费返佣,因此每笔交易的"真实成本"极低0.012%/round trip
策略使用 EMA 金叉/死叉 捕捉 1 分钟级别的微趋势,配合大级别 EMA 趋势过滤
和 ATR 波动率过滤,只在高波动顺势环境中开仓。
持仓必须 >= 3 分钟,避免被判定为刷量。
回测表现2025全年 1 分钟 K 线):
- 参数EMA(8/21/120) ATR>0.3% SL=0.4% MaxHold=1800s
- Risk=2% → 年化 +10.11%,最大回撤 10.92%
- Risk=3% → 年化 +13.66%,最大回撤 15.96%
- 全年约 227 笔交易,平均 1.6 天 1 笔
- 胜率 ~29%,盈亏比 ~2.7:1低胜率高赔率模式
策略规则:
1. 使用 1 分钟 K 线计算 EMA(8)快线、EMA(21)慢线、EMA(120)大趋势线
2. 计算 ATR(14) 波动率,仅在 ATR > 0.3% 时交易(过滤低波动区间)
3. 开仓信号:
- 做多EMA(8) 上穿 EMA(21) 且 价格 > EMA(120)(顺势金叉)
- 做空EMA(8) 下穿 EMA(21) 且 价格 < EMA(120)(顺势死叉)
4. 平仓条件:
a) 反向交叉信号(满足最低持仓后,可同时反手开仓)
b) 止损:浮亏 >= 0.4%(硬止损 0.6%,不受持仓时间限制)
c) 超时:持仓 >= 30 分钟强制平仓
5. 平仓后若有反向信号且满足 ATR 过滤,立即反手开仓
"""
import time
import uuid
import datetime
from loguru import logger
from bitmart.api_contract import APIContract
from bitmart.lib.cloud_exceptions import APIException
from 交易.tools import send_dingtalk_message
# ========================= EMA 计算器 =========================
class EMACalculator:
"""指数移动平均线,增量式更新"""
def __init__(self, period: int):
self.period = period
self.k = 2.0 / (period + 1)
self.value = None
def update(self, price: float) -> float:
if self.value is None:
self.value = price
else:
self.value = price * self.k + self.value * (1 - self.k)
return self.value
def reset(self):
self.value = None
class BitmartRebateStrategy:
def __init__(self):
self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
self.memo = "合约交易"
self.contract_symbol = "ETHUSDT"
self.contractAPI = APIContract(self.api_key, self.secret_key, self.memo, timeout=(5, 15))
# ========================= 持仓状态 =========================
self.start = 0 # -1 空, 0 无, 1 多
self.open_avg_price = None
self.current_amount = None
self.position_cross = None
self.open_time = None # 开仓时间戳(用于计算持仓时长)
# ========================= 杠杆 & 仓位 =========================
self.leverage = "50" # 杠杆倍数
self.open_type = "cross" # 全仓模式
self.risk_percent = 0.02 # 每次开仓使用可用余额的 2%(回测最优)
# ========================= EMA 参数(回测最优) =========================
self.ema_fast_period = 8 # 快线 EMA 周期
self.ema_slow_period = 21 # 慢线 EMA 周期
self.ema_big_period = 120 # 大趋势 EMA 周期2小时
self.atr_period = 14 # ATR 周期
self.atr_min_pct = 0.003 # ATR 最低阈值 0.3%(过滤低波动)
# ========================= 持仓管理参数(回测最优) =========================
self.min_hold_seconds = 200 # 最低持仓时间(秒),>3分钟
self.max_hold_seconds = 1800 # 最长持仓时间30分钟
self.stop_loss_pct = 0.004 # 止损百分比 0.4%
self.hard_stop_pct = 0.006 # 硬止损 0.6%(不受时间限制)
# ========================= EMA 状态 =========================
self.ema_fast = EMACalculator(self.ema_fast_period)
self.ema_slow = EMACalculator(self.ema_slow_period)
self.ema_big = EMACalculator(self.ema_big_period)
self.prev_fast = None # 上一根K线的快线值
self.prev_slow = None # 上一根K线的慢线值
self.pending_signal = None # 等待最低持仓后执行的延迟信号
# ========================= K线缓存用于 ATR 计算) =========================
self.highs = []
self.lows = []
self.closes = []
self.last_kline_time = None # 最新处理过的K线时间戳
# ========================= 运行控制 =========================
self.check_interval = 5 # 主循环检测间隔(秒)
# ========================= 统计 =========================
self.trade_count = 0 # 当日交易次数
self.total_pnl = 0.0 # 当日累计盈亏
self.total_volume = 0.0 # 当日累计交易额(用于估算返佣)
self.start_time = time.time() # 程序启动时间
# ========================= 技术指标计算 =========================
def calculate_atr_pct(self, current_price):
"""计算 ATR 占当前价格的百分比"""
if len(self.highs) < self.atr_period + 1:
return 0.0
trs = []
for i in range(-self.atr_period, 0):
h = self.highs[i]
l = self.lows[i]
pc = self.closes[i - 1]
tr = max(h - l, abs(h - pc), abs(l - pc))
trs.append(tr)
atr = sum(trs) / self.atr_period
return atr / current_price if current_price > 0 else 0.0
def detect_cross(self, fast_val, slow_val):
"""
检测 EMA 金叉/死叉
返回: 'golden' (金叉) / 'death' (死叉) / None
"""
if self.prev_fast is None or self.prev_slow is None:
return None
# 金叉:快线从下方穿越慢线
if self.prev_fast <= self.prev_slow and fast_val > slow_val:
return "golden"
# 死叉:快线从上方穿越慢线
if self.prev_fast >= self.prev_slow and fast_val < slow_val:
return "death"
return None
def process_new_kline(self, kline):
"""
处理新的 1 分钟 K 线,更新所有指标
返回: (cross_signal, atr_pct, fast, slow, big)
"""
close = kline['close']
high = kline['high']
low = kline['low']
# 缓存 K 线数据
self.highs.append(high)
self.lows.append(low)
self.closes.append(close)
# 只保留最近 200 根(节省内存)
if len(self.highs) > 200:
self.highs = self.highs[-200:]
self.lows = self.lows[-200:]
self.closes = self.closes[-200:]
# 更新 EMA
fast_val = self.ema_fast.update(close)
slow_val = self.ema_slow.update(close)
big_val = self.ema_big.update(close)
# 检测交叉
cross = self.detect_cross(fast_val, slow_val)
# 保存当前值供下次对比
self.prev_fast = fast_val
self.prev_slow = slow_val
# 计算 ATR
atr_pct = self.calculate_atr_pct(close)
return cross, atr_pct, fast_val, slow_val, big_val
# ========================= 数据获取 =========================
def get_1min_klines(self, count=150):
"""获取最近 N 根 1 分钟 K 线"""
try:
end_time = int(time.time())
start_time = end_time - 60 * count * 2 # 多取一些保证够用
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=1, # 1 分钟
start_time=start_time,
end_time=end_time
)[0]
if response['code'] != 1000:
logger.error(f"获取K线失败: {response}")
return None
formatted = []
for k in response['data']:
formatted.append({
'timestamp': int(k["timestamp"]),
'open': float(k["open_price"]),
'high': float(k["high_price"]),
'low': float(k["low_price"]),
'close': float(k["close_price"]),
})
formatted.sort(key=lambda x: x['timestamp'])
# 只保留最近 count 根
return formatted[-count:]
except Exception as e:
logger.error(f"获取1分钟K线异常: {e}")
return None
def get_current_price(self):
"""获取当前最新价格"""
try:
end_time = int(time.time())
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=1,
start_time=end_time - 300,
end_time=end_time
)[0]
if response['code'] == 1000 and response['data']:
return float(response['data'][-1]["close_price"])
return None
except Exception as e:
logger.error(f"获取价格异常: {e}")
return None
def get_available_balance(self):
"""获取合约账户可用 USDT 余额"""
try:
response = self.contractAPI.get_assets_detail()[0]
if response['code'] == 1000:
data = response['data']
if isinstance(data, dict):
return float(data.get('available_balance', 0))
elif isinstance(data, list):
for asset in data:
if asset.get('currency') == 'USDT':
return float(asset.get('available_balance', 0))
return None
except Exception as e:
logger.error(f"余额查询异常: {e}")
return None
def get_position_status(self):
"""获取当前持仓状态"""
try:
response = self.contractAPI.get_position(contract_symbol=self.contract_symbol)[0]
if response['code'] == 1000:
positions = response['data']
if not positions:
self.start = 0
self.open_avg_price = None
self.current_amount = None
self.position_cross = None
return True
self.start = 1 if positions[0]['position_type'] == 1 else -1
self.open_avg_price = float(positions[0]['open_avg_price'])
self.current_amount = float(positions[0]['current_amount'])
self.position_cross = positions[0].get("position_cross")
return True
return False
except Exception as e:
logger.error(f"持仓查询异常: {e}")
return False
# ========================= 交易执行 =========================
def set_leverage(self):
"""设置全仓 + 杠杆"""
try:
response = self.contractAPI.post_submit_leverage(
contract_symbol=self.contract_symbol,
leverage=self.leverage,
open_type=self.open_type
)[0]
if response['code'] == 1000:
logger.success(f"全仓模式 + {self.leverage}x 杠杆设置成功")
return True
else:
logger.error(f"杠杆设置失败: {response}")
return False
except Exception as e:
logger.error(f"设置杠杆异常: {e}")
return False
def calculate_size(self, price):
"""计算开仓张数"""
balance = self.get_available_balance()
if not balance or balance < 10:
logger.warning(f"余额不足: {balance}")
return 0
leverage = int(self.leverage)
margin = balance * self.risk_percent
# ETHUSDT 1张 ≈ 0.001 ETH
size = int((margin * leverage) / (price * 0.001))
size = max(1, size)
logger.info(f"余额 {balance:.2f} USDT → 保证金 {margin:.2f} USDT → 开仓 {size} 张 (价格≈{price:.2f})")
return size
def place_market_order(self, side: int, size: int):
"""
下市价单
side: 1=开多, 2=平空, 3=平多, 4=开空
"""
if size <= 0:
return False
client_order_id = f"rebate_{int(time.time())}_{uuid.uuid4().hex[:8]}"
side_names = {1: "开多", 2: "平空", 3: "平多", 4: "开空"}
try:
response = self.contractAPI.post_submit_order(
contract_symbol=self.contract_symbol,
client_order_id=client_order_id,
side=side,
mode=1,
type='market',
leverage=self.leverage,
open_type=self.open_type,
size=size
)[0]
if response['code'] == 1000:
logger.success(f"下单成功: {side_names.get(side, side)} {size}")
return True
else:
logger.error(f"下单失败: {response}")
return False
except APIException as e:
logger.error(f"API下单异常: {e}")
return False
def open_position(self, direction: str, price: float):
"""开仓"""
size = self.calculate_size(price)
if size == 0:
return False
if direction == "long":
if self.place_market_order(1, size):
self.start = 1
self.open_avg_price = price
self.open_time = time.time()
self.current_amount = size
self.pending_signal = None
# 统计交易额
volume = size * 0.001 * price
self.total_volume += volume
self.trade_count += 1
logger.success(f"开多 {size} 张 @ {price:.2f}")
self.ding(f"开多 {size} 张 @ {price:.2f}")
return True
elif direction == "short":
if self.place_market_order(4, size):
self.start = -1
self.open_avg_price = price
self.open_time = time.time()
self.current_amount = size
self.pending_signal = None
volume = size * 0.001 * price
self.total_volume += volume
self.trade_count += 1
logger.success(f"开空 {size} 张 @ {price:.2f}")
self.ding(f"开空 {size} 张 @ {price:.2f}")
return True
return False
def close_position(self, reason: str, current_price: float):
"""平仓"""
if self.start == 0:
return False
close_side = 3 if self.start == 1 else 2 # 3=平多, 2=平空
direction_str = "" if self.start == 1 else ""
if self.place_market_order(close_side, 999999):
# 计算本次盈亏
if self.open_avg_price and self.current_amount:
if self.start == 1:
pnl = self.current_amount * 0.001 * (current_price - self.open_avg_price)
else:
pnl = self.current_amount * 0.001 * (self.open_avg_price - current_price)
self.total_pnl += pnl
# 统计平仓交易额
volume = self.current_amount * 0.001 * current_price
self.total_volume += volume
hold_seconds = time.time() - self.open_time if self.open_time else 0
logger.success(
f"{direction_str} @ {current_price:.2f} | "
f"原因: {reason} | 持仓 {hold_seconds:.0f}s | "
f"本次盈亏: {pnl:+.4f} USDT"
)
self.ding(
f"{direction_str} @ {current_price:.2f}\n"
f"原因: {reason}\n持仓 {hold_seconds:.0f}s\n"
f"本次盈亏: {pnl:+.4f} USDT"
)
self.start = 0
self.open_avg_price = None
self.open_time = None
self.current_amount = None
self.pending_signal = None
self.trade_count += 1
return True
return False
# ========================= 信号检测 =========================
def check_open_signal(self, current_price, cross, atr_pct, big_val):
"""
检查开仓信号
返回: 'long' / 'short' / None
"""
# ATR 过滤:波动率不足时不开仓
if atr_pct < self.atr_min_pct:
return None
# 金叉 + 价格在大EMA上方 → 做多
if cross == "golden" and current_price > big_val:
logger.info(
f"做多信号 | 金叉 + 价格 {current_price:.2f} > EMA120 {big_val:.2f} | ATR {atr_pct*100:.3f}%"
)
return "long"
# 死叉 + 价格在大EMA下方 → 做空
if cross == "death" and current_price < big_val:
logger.info(
f"做空信号 | 死叉 + 价格 {current_price:.2f} < EMA120 {big_val:.2f} | ATR {atr_pct*100:.3f}%"
)
return "short"
return None
def check_close_signal(self, current_price, cross, atr_pct, fast_val, slow_val, big_val):
"""
检查平仓信号
返回: (should_close: bool, reason: str, reverse_direction: str or None)
reverse_direction: 平仓后是否反手 ('long'/'short'/None)
"""
if self.start == 0 or not self.open_avg_price or not self.open_time:
return False, "", None
hold_seconds = time.time() - self.open_time
# 计算浮动盈亏
if self.start == 1:
loss_pct = (self.open_avg_price - current_price) / self.open_avg_price
else:
loss_pct = (current_price - self.open_avg_price) / self.open_avg_price
# ① 硬止损:不受持仓时间限制
if loss_pct >= self.hard_stop_pct:
return True, f"硬止损 (亏损 {loss_pct*100:.3f}%)", None
# ② 未满足最低持仓时间
if hold_seconds < self.min_hold_seconds:
# 记录延迟信号(等持仓时间到了再处理)
if self.start == 1 and cross == "death":
self.pending_signal = "close_long"
logger.info(f"检测到死叉但持仓时间不足,记录延迟信号 | 还需 {self.min_hold_seconds - hold_seconds:.0f}s")
elif self.start == -1 and cross == "golden":
self.pending_signal = "close_short"
logger.info(f"检测到金叉但持仓时间不足,记录延迟信号 | 还需 {self.min_hold_seconds - hold_seconds:.0f}s")
remaining = self.min_hold_seconds - hold_seconds
if int(hold_seconds) % 30 == 0:
logger.info(f"持仓中... 还需等待 {remaining:.0f}s")
return False, "", None
# ③ 满足持仓时间后的平仓检查
reverse = None
# 止损
if loss_pct >= self.stop_loss_pct:
return True, f"止损 (亏损 {loss_pct*100:.3f}%)", None
# 超时
if hold_seconds >= self.max_hold_seconds:
return True, f"超时平仓 (持仓 {hold_seconds:.0f}s)", None
# 反向交叉 → 平仓 + 可能反手
if self.start == 1 and cross == "death":
# 判断是否满足反手条件
if current_price < big_val and atr_pct >= self.atr_min_pct:
reverse = "short"
return True, "EMA死叉反转", reverse
if self.start == -1 and cross == "golden":
if current_price > big_val and atr_pct >= self.atr_min_pct:
reverse = "long"
return True, "EMA金叉反转", reverse
# 处理延迟信号(之前因持仓时间不足未执行)
if self.pending_signal == "close_long" and self.start == 1:
if fast_val < slow_val and current_price < big_val and atr_pct >= self.atr_min_pct:
reverse = "short"
self.pending_signal = None
return True, "延迟死叉平仓", reverse
if self.pending_signal == "close_short" and self.start == -1:
if fast_val > slow_val and current_price > big_val and atr_pct >= self.atr_min_pct:
reverse = "long"
self.pending_signal = None
return True, "延迟金叉平仓", reverse
return False, "", None
# ========================= 通知 =========================
def ding(self, msg, error=False):
"""发送通知"""
prefix = "返佣策略(ERR): " if error else "返佣策略: "
try:
if error:
for _ in range(3):
send_dingtalk_message(f"{prefix}{msg}")
else:
send_dingtalk_message(f"{prefix}{msg}")
except Exception as e:
logger.warning(f"通知发送失败: {e}")
def print_daily_stats(self):
"""打印当日统计"""
elapsed = time.time() - self.start_time
hours = elapsed / 3600
# 预估返佣90% 的手续费)
fee_rate = 0.0006 # taker 0.06%
total_fee = self.total_volume * fee_rate
rebate = total_fee * 0.9 # 90% 返佣
now = datetime.datetime.now().strftime("%H:%M:%S")
stats = (
f"\n{'='*50}\n"
f"[{now}] EMA趋势返佣策略统计\n"
f"运行时长: {hours:.1f} 小时\n"
f"交易次数: {self.trade_count}\n"
f"交易总额: {self.total_volume:.2f} USDT\n"
f"交易盈亏: {self.total_pnl:+.4f} USDT\n"
f"预估手续费: {total_fee:.4f} USDT\n"
f"预估返佣收入: {rebate:.4f} USDT\n"
f"预估净收益: {self.total_pnl + rebate:.4f} USDT\n"
f"{'='*50}"
)
logger.info(stats)
# ========================= 初始化指标 =========================
def init_indicators(self):
"""用历史K线初始化 EMA 和 ATR避免冷启动"""
logger.info("正在加载历史K线初始化指标...")
klines = self.get_1min_klines(count=150)
if not klines or len(klines) < self.ema_big_period:
logger.warning(f"历史K线不足 {self.ema_big_period} 根,指标将在运行中逐步初始化")
return False
# 除最后一根当前未完成的K线全部用于初始化
for kline in klines[:-1]:
self.process_new_kline(kline)
self.last_kline_time = kline['timestamp']
logger.info(
f"指标初始化完成 | {len(klines)-1} 根K线 | "
f"EMA8={self.ema_fast.value:.2f} EMA21={self.ema_slow.value:.2f} "
f"EMA120={self.ema_big.value:.2f}"
)
return True
# ========================= 主循环 =========================
def action(self):
"""主循环"""
# 设置杠杆
if not self.set_leverage():
logger.error("杠杆设置失败,退出")
self.ding("杠杆设置失败", error=True)
return
# 启动时获取持仓状态
if not self.get_position_status():
logger.warning("初始持仓状态获取失败,假设无仓位")
else:
if self.start != 0:
self.open_time = time.time() # 如果已有仓位,设置开仓时间为当前
logger.info(f"检测到已有持仓: {'' if self.start == 1 else ''}")
# 初始化技术指标
self.init_indicators()
logger.info(
f"EMA趋势返佣策略启动\n"
f" 交易对: {self.contract_symbol}\n"
f" 杠杆: {self.leverage}x 全仓\n"
f" EMA: 快{self.ema_fast_period} / 慢{self.ema_slow_period} / 大{self.ema_big_period}\n"
f" ATR过滤: > {self.atr_min_pct*100:.1f}% | 止损: {self.stop_loss_pct*100:.1f}%\n"
f" 最低持仓: {self.min_hold_seconds}s | 最长持仓: {self.max_hold_seconds}s\n"
f" 仓位比例: {self.risk_percent*100:.1f}%\n"
)
self.ding(
f"策略启动 | {self.contract_symbol} | {self.leverage}x\n"
f"EMA({self.ema_fast_period}/{self.ema_slow_period}/{self.ema_big_period}) "
f"ATR>{self.atr_min_pct*100:.1f}%"
)
stats_timer = time.time()
while True:
try:
# ① 获取最新 K 线
klines = self.get_1min_klines(count=5)
if not klines:
logger.warning("K线数据获取失败等待...")
time.sleep(self.check_interval)
continue
# ② 检查是否有新的完成K线需要处理
latest_completed = klines[-2] if len(klines) >= 2 else None
current_bar = klines[-1]
current_price = current_bar['close']
new_bar_processed = False
if latest_completed and latest_completed['timestamp'] != self.last_kline_time:
# 新K线完成处理信号
cross, atr_pct, fast_val, slow_val, big_val = self.process_new_kline(latest_completed)
self.last_kline_time = latest_completed['timestamp']
new_bar_processed = True
# ③ 有持仓 → 检查平仓
if self.start != 0:
should_close, reason, reverse = self.check_close_signal(
current_price, cross, atr_pct, fast_val, slow_val, big_val
)
if should_close:
self.close_position(reason, current_price)
# 反手开仓
if reverse:
time.sleep(1)
self.open_position(reverse, current_price)
time.sleep(1)
continue
# ④ 无持仓 → 检查开仓
if self.start == 0:
signal = self.check_open_signal(current_price, cross, atr_pct, big_val)
if signal:
self.open_position(signal, current_price)
time.sleep(1)
continue
# ⑤ 未收到新K线时仅检查止损/硬止损(实时保护)
if not new_bar_processed and self.start != 0 and self.open_avg_price:
if self.start == 1:
loss_pct = (self.open_avg_price - current_price) / self.open_avg_price
else:
loss_pct = (current_price - self.open_avg_price) / self.open_avg_price
# 硬止损实时检查
if loss_pct >= self.hard_stop_pct:
self.close_position(f"硬止损 (亏损 {loss_pct*100:.3f}%)", current_price)
time.sleep(1)
continue
# 满足持仓时间后的止损检查
if self.open_time and (time.time() - self.open_time) >= self.min_hold_seconds:
if loss_pct >= self.stop_loss_pct:
self.close_position(f"止损 (亏损 {loss_pct*100:.3f}%)", current_price)
time.sleep(1)
continue
# 超时检查
hold_sec = time.time() - self.open_time
if hold_sec >= self.max_hold_seconds:
self.close_position(f"超时平仓 ({hold_sec:.0f}s)", current_price)
time.sleep(1)
continue
# 延迟信号处理
if self.pending_signal and self.open_time and \
(time.time() - self.open_time) >= self.min_hold_seconds:
atr_pct = self.calculate_atr_pct(current_price)
fast_val = self.ema_fast.value
slow_val = self.ema_slow.value
big_val = self.ema_big.value
should_close, reason, reverse = self.check_close_signal(
current_price, None, atr_pct, fast_val, slow_val, big_val
)
if should_close:
self.close_position(reason, current_price)
if reverse:
time.sleep(1)
self.open_position(reverse, current_price)
time.sleep(1)
continue
# ⑥ 定期打印统计(每 10 分钟)
if time.time() - stats_timer >= 600:
self.print_daily_stats()
stats_timer = time.time()
# ⑦ 每轮日志
hold_info = ""
if self.start != 0 and self.open_time:
hold_seconds = time.time() - self.open_time
direction = "" if self.start == 1 else ""
if self.open_avg_price:
if self.start == 1:
pnl_pct = (current_price - self.open_avg_price) / self.open_avg_price * 100
else:
pnl_pct = (self.open_avg_price - current_price) / self.open_avg_price * 100
hold_info = f" | 持{direction} {hold_seconds:.0f}s PnL:{pnl_pct:+.3f}%"
if self.ema_fast.value and self.ema_slow.value and self.ema_big.value:
logger.debug(
f"价格 {current_price:.2f} | "
f"EMA [{self.ema_fast.value:.2f}/{self.ema_slow.value:.2f}/{self.ema_big.value:.2f}] | "
f"ATR {self.calculate_atr_pct(current_price)*100:.3f}%{hold_info}"
)
time.sleep(self.check_interval)
except KeyboardInterrupt:
logger.info("用户中断")
self.print_daily_stats()
# 中断时如果有仓位,提示手动处理
if self.start != 0:
logger.warning("当前仍有持仓,请手动处理!")
break
except Exception as e:
logger.error(f"主循环异常: {e}")
time.sleep(10)
if __name__ == '__main__':
BitmartRebateStrategy().action()