Files
lm_code/weex/长期持有信号/30分钟,加入爆仓条件.py
2025-10-22 16:22:36 +08:00

344 lines
15 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"""
量化交易回测系统 - 仅15分钟K线 & 信号续持/反手/单根反色平仓逻辑(包含逐仓爆仓逻辑)
"""
import datetime
from dataclasses import dataclass
from typing import List, Dict, Optional
from loguru import logger
from models.weex import Weex30 # 替换为你的15分钟K线模型
# ========================= 工具函数 =========================
def is_bullish(c): # 阳线
return float(c['close']) > float(c['open'])
def is_bearish(c): # 阴线
return float(c['close']) < float(c['open'])
def check_signal(prev, curr):
"""
包住形态信号判定仅15分钟K线
- 前跌后涨包住 -> 做多
- 前涨后跌包住 -> 做空
"""
p_open, p_close = float(prev['open']), float(prev['close'])
c_open, c_close = float(curr['open']), float(curr['close'])
# 前跌后涨包住 -> 做多
if is_bullish(curr) and is_bearish(prev) and c_open <= p_close and c_close >= p_open:
return "long", "bear_bull_engulf"
# 前涨后跌包住 -> 做空
if is_bearish(curr) and is_bullish(prev) and c_open >= p_close and c_close <= p_open:
return "short", "bull_bear_engulf"
return None, None
def get_data_by_date(model, date_str: str):
"""按天获取指定表的数据15分钟"""
try:
target_date = datetime.datetime.strptime(date_str, '%Y-%m-%d')
except ValueError:
logger.error("日期格式不正确,请使用 YYYY-MM-DD 格式。")
return []
start_ts = int(target_date.timestamp() * 1000)
end_ts = int((target_date + datetime.timedelta(days=1)).timestamp() * 1000) - 1
query = model.select().where(model.id.between(start_ts, end_ts)).order_by(model.id.asc())
return [{'id': i.id, 'open': i.open, 'high': i.high, 'low': i.low, 'close': i.close} for i in query]
# ========================= 回测逻辑(加入逐仓爆仓) =========================
def backtest_15m_trend_optimized(dates: List[str], initial_margin=100.0, leverage=100.0):
"""
initial_margin: 每笔开仓的保证金USD例如 100
leverage: 杠杆倍数,例如 100 -> notional = initial_margin * leverage
说明:
- 逐仓:每笔保证金单独计,若该仓爆仓只损失该笔保证金(不影响其它仓位/账户余额)
- 爆仓判断当价格触及爆仓价entry_price +/- entry_price/leverage视为爆仓
- 爆仓后:设置 waiting_for_next_signal 标记,消费下一个信号但不在该信号开仓(满足“等下一个信号来了再开仓”)
"""
notional_per_trade = initial_margin * leverage # 合约名义价值
# 统计结构保留你原来的字段语义total_profit 仍记录价差price units
stats = {
'bear_bull_engulf': {'count': 0, 'wins': 0, 'total_profit': 0.0, 'name': '涨包跌'},
'bull_bear_engulf': {'count': 0, 'wins': 0, 'total_profit': 0.0, 'name': '跌包涨'},
}
all_data: List[Dict] = []
for d in dates:
all_data.extend(get_data_by_date(Weex30, d))
if not all_data:
return [], stats
all_data.sort(key=lambda x: x['id'])
trades: List[Dict] = []
current_position: Optional[Dict] = None # 开仓信息
waiting_for_next_signal = False # 爆仓后等待“下一个信号”才可重新开仓
idx = 1
while idx < len(all_data) - 1:
prev, curr, next_bar = all_data[idx - 1], all_data[idx], all_data[idx + 1]
direction, signal_key = check_signal(prev, curr)
# ========== 空仓逻辑 ==========
if current_position is None and direction:
# 如果之前发生过爆仓,按照需求:爆仓后“等下一个信号来了再开仓”。
# 实现策略:爆仓后第一个出现的 signal 会被消费(不打开仓),真正开仓需要后续的信号。
if waiting_for_next_signal:
# consume this signal but do not open; reset flag and continue
waiting_for_next_signal = False
idx += 1
continue
# 正常开仓:使用 next_bar 的开盘价作为入场价(保持和原逻辑一致)
entry_price = float(next_bar['open'])
# 计算该仓的爆仓价(逐仓,简化模型)
# delta_price = initial_margin * entry_price / notional_per_trade = entry_price / leverage
delta_price = entry_price / leverage
if direction == 'long':
liq_price = entry_price - delta_price
else: # short
liq_price = entry_price + delta_price
current_position = {
'direction': direction,
'signal': stats[signal_key]['name'],
'signal_key': signal_key,
'entry_price': entry_price,
'entry_time': next_bar['id'],
'liq_price': liq_price,
'initial_margin': initial_margin,
'leverage': leverage,
'notional': notional_per_trade
}
stats[signal_key]['count'] += 1
idx += 1
continue
# ========== 有仓位时的处理 ==========
if current_position:
pos_dir = current_position['direction']
pos_sig_key = current_position['signal_key']
# 1) 检查在当前currK是否触及爆仓价使用当根的 high/low 判断)
liq_price = current_position['liq_price']
was_liquidated = False
liquidated_at_price = None
# 对多仓:若当根 low <= liq_price -> 爆仓
if pos_dir == 'long' and float(curr['low']) <= liq_price:
was_liquidated = True
liquidated_at_price = liq_price
# 对空仓:若当根 high >= liq_price -> 爆仓
if pos_dir == 'short' and float(curr['high']) >= liq_price:
was_liquidated = True
liquidated_at_price = liq_price
if was_liquidated:
# 记录一笔爆仓交易:损失等于 initial_margin 对应的价差price units
# 计算价差price units注意方向对多仓是 exit - entry通常为负
entry_price = current_position['entry_price']
exit_price = liquidated_at_price
if pos_dir == 'long':
diff = exit_price - entry_price
else:
diff = entry_price - exit_price
trades.append({
'entry_time': datetime.datetime.fromtimestamp(current_position['entry_time'] / 1000),
'exit_time': datetime.datetime.fromtimestamp(curr['id'] / 1000),
'signal': current_position['signal'],
'direction': '做多' if pos_dir == 'long' else '做空',
'entry': entry_price,
'exit': exit_price,
'diff': diff,
'liquidated': True
})
stats[pos_sig_key]['total_profit'] += diff
if diff > 0:
stats[pos_sig_key]['wins'] += 1
# 爆仓后按需求:不立即再开仓,等下一个信号才可开
current_position = None
waiting_for_next_signal = True
idx += 1
continue
# 2) 反向信号 -> 下一根开盘平仓 + 同价反手(保持原逻辑)
if direction and direction != pos_dir:
# 用 next_bar 的开盘价作为出场价(与你原逻辑保持一致)
exit_price = float(next_bar['open'])
diff = (exit_price - current_position['entry_price']) if pos_dir == 'long' else (
current_position['entry_price'] - exit_price)
trades.append({
'entry_time': datetime.datetime.fromtimestamp(current_position['entry_time'] / 1000),
'exit_time': datetime.datetime.fromtimestamp(next_bar['id'] / 1000),
'signal': current_position['signal'],
'direction': '做多' if pos_dir == 'long' else '做空',
'entry': current_position['entry_price'],
'exit': exit_price,
'diff': diff,
'liquidated': False
})
stats[pos_sig_key]['total_profit'] += diff
if diff > 0: stats[pos_sig_key]['wins'] += 1
# 同价反手开仓(保持原逻辑)
current_position = {
'direction': direction,
'signal': stats[signal_key]['name'],
'signal_key': signal_key,
'entry_price': exit_price,
'entry_time': next_bar['id'],
# recompute liq_price for new position
'liq_price': (exit_price - exit_price / leverage) if direction == 'long' else (
exit_price + exit_price / leverage),
'initial_margin': initial_margin,
'leverage': leverage,
'notional': notional_per_trade
}
stats[signal_key]['count'] += 1
idx += 1
continue
# 3) 同向信号 -> 续持(不做任何改动)
if direction and direction == pos_dir:
idx += 1
continue
# 4) 单根反色K线 -> 判断后续是否能组成信号(保留原逻辑)
curr_is_opposite = (pos_dir == 'long' and is_bearish(curr)) or (pos_dir == 'short' and is_bullish(curr))
if curr_is_opposite:
can_peek = idx + 1 < len(all_data)
if can_peek:
lookahead_dir, _ = check_signal(curr, all_data[idx + 1])
if lookahead_dir is not None:
idx += 1
continue # 后续可组成信号,等待信号处理
# 否则按收盘价平仓(与原逻辑一致)
exit_price = float(next_bar['close'])
diff = (exit_price - current_position['entry_price']) if pos_dir == 'long' else (
current_position['entry_price'] - exit_price)
trades.append({
'entry_time': datetime.datetime.fromtimestamp(current_position['entry_time'] / 1000),
'exit_time': datetime.datetime.fromtimestamp(all_data[idx + 1]['id'] / 1000),
'signal': current_position['signal'],
'direction': '做多' if pos_dir == 'long' else '做空',
'entry': current_position['entry_price'],
'exit': exit_price,
'diff': diff,
'liquidated': False
})
stats[pos_sig_key]['total_profit'] += diff
if diff > 0: stats[pos_sig_key]['wins'] += 1
current_position = None
idx += 1
# ========== 尾仓:最后一根收盘价平仓(如仍有仓位) ==========
if current_position:
last = all_data[-1]
exit_price = float(last['close'])
pos_dir = current_position['direction']
diff = (exit_price - current_position['entry_price']) if pos_dir == 'long' else (
current_position['entry_price'] - exit_price)
trades.append({
'entry_time': datetime.datetime.fromtimestamp(current_position['entry_time'] / 1000),
'exit_time': datetime.datetime.fromtimestamp(last['id'] / 1000),
'signal': current_position['signal'],
'direction': '做多' if pos_dir == 'long' else '做空',
'entry': current_position['entry_price'],
'exit': exit_price,
'diff': diff,
'liquidated': False
})
stats[current_position['signal_key']]['total_profit'] += diff
if diff > 0: stats[current_position['signal_key']]['wins'] += 1
return trades, stats
# ========================= 运行示例(包含爆仓) =========================
if __name__ == '__main__':
dates = []
for m in range(1, 11):
for d in range(1, 31):
dates.append(f"2025-{f'0{m}' if len(str(m)) < 2 else m}-{d}")
# 参数:初始保证金 100 USD100 倍
trades, stats = backtest_15m_trend_optimized(dates, initial_margin=100.0, leverage=100.0)
logger.info("===== 每笔交易详情 =====")
# === 手续费/合约规模设定(沿用你原有实现) ===
contract_size = 10000 # 合约规模1手对应多少基础货币你原来这里等于 notional_per_trade100*100
open_fee_fixed = 5 # 固定开仓手续费
close_fee_rate = 0.0005 # 按成交额比例的平仓手续费率
total_points_profit = 0 # 累计点差
total_money_profit = 0 # 累计金额盈利
total_fee = 0 # 累计手续费
for t in trades:
entry = t['entry']
exit = t['exit']
direction = t['direction']
# === 1⃣ 原始价差(点差) ===
point_diff = (exit - entry) if direction == '做多' else (entry - exit)
# === 2⃣ 金额盈利(考虑合约规模) ===
money_profit = point_diff / entry * contract_size # 利润以基础货币计例如USD
# === 3⃣ 手续费计算 ===
fee = open_fee_fixed + (contract_size / entry * exit * close_fee_rate)
# === 4⃣ 净利润 ===
net_profit = money_profit - fee
# 保存计算结果
t.update({
'point_diff': point_diff,
'raw_profit': money_profit,
'fee': fee,
'net_profit': net_profit
})
total_points_profit += point_diff
total_money_profit += money_profit
total_fee += fee
if point_diff < -50:
logger.info(
f"{t['entry_time']} {direction}({t['signal']}) "
f"入={entry:.6f} 出={exit:.6f} 差价={point_diff:.6f} "
f"原始盈利={money_profit:.2f} 手续费={fee:.2f} 净利润={net_profit:.2f} "
f"{'(LIQ)' if t.get('liquidated') else ''} {t['exit_time']}"
)
# === 汇总统计 ===
total_net_profit = total_money_profit - total_fee
print(f"\n一共交易笔数:{len(trades)}")
print(f"总点差:{total_points_profit:.6f}")
print(f"总原始盈利(未扣费):{total_money_profit:.2f}")
print(f"总手续费:{total_fee:.2f}")
print(f"总净利润:{total_net_profit:.2f}\n")
print("===== 信号统计 =====")
for k, v in stats.items():
name, count, wins, total_p = v['name'], v['count'], v['wins'], v['total_profit']
win_rate = (wins / count * 100) if count > 0 else 0.0
avg_p = (total_p / count) if count > 0 else 0.0
print(f"{name}: 次数={count} 胜率={win_rate:.2f}% 总价差={total_p:.6f} 平均价差={avg_p:.6f}")