Files
lm_code/交易/bitmart-100u回测.py
Your Name b5af5b07f3 哈哈
2026-02-15 02:16:45 +08:00

308 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()