308 lines
12 KiB
Python
308 lines
12 KiB
Python
"""
|
||
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()
|