哈哈
This commit is contained in:
307
交易/bitmart-100u回测.py
Normal file
307
交易/bitmart-100u回测.py
Normal 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()
|
||||
376
交易/bitmart-AI优化回测.py
Normal file
376
交易/bitmart-AI优化回测.py
Normal 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()
|
||||
316
交易/bitmart-AI快速优化.py
Normal file
316
交易/bitmart-AI快速优化.py
Normal 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()
|
||||
258
交易/bitmart-AI最佳回测.py
Normal file
258
交易/bitmart-AI最佳回测.py
Normal 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()
|
||||
442
交易/bitmart-AI策略回测.py
Normal file
442
交易/bitmart-AI策略回测.py
Normal 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
187
交易/bitmart-优化v2.py
Normal 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()
|
||||
376
交易/bitmart-参数优化.py
Normal file
376
交易/bitmart-参数优化.py
Normal 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()
|
||||
363
交易/bitmart-多策略回测.py
Normal file
363
交易/bitmart-多策略回测.py
Normal 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()
|
||||
289
交易/bitmart-快速优化.py
Normal file
289
交易/bitmart-快速优化.py
Normal 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()
|
||||
673
交易/bitmart-新策略回测.py
Normal file
673
交易/bitmart-新策略回测.py
Normal 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()
|
||||
293
交易/bitmart-最终回测.py
Normal file
293
交易/bitmart-最终回测.py
Normal 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()
|
||||
315
交易/bitmart-盈利组合回测.py
Normal file
315
交易/bitmart-盈利组合回测.py
Normal 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()
|
||||
184
交易/bitmart-精准优化.py
Normal file
184
交易/bitmart-精准优化.py
Normal 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()
|
||||
399
交易/bitmart-组合策略回测.py
Normal file
399
交易/bitmart-组合策略回测.py
Normal 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()
|
||||
338
交易/bitmart-终极策略回测.py
Normal file
338
交易/bitmart-终极策略回测.py
Normal 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()
|
||||
542
交易/bitmart-返佣策略-回测.py
Normal file
542
交易/bitmart-返佣策略-回测.py
Normal 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()
|
||||
763
交易/bitmart-返佣策略.py
Normal file
763
交易/bitmart-返佣策略.py
Normal 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()
|
||||
Reference in New Issue
Block a user