1090 lines
42 KiB
Python
1090 lines
42 KiB
Python
"""
|
||
RaptorBT + Mt5Bridge 集成测试
|
||
|
||
从 Mt5Bridge API 拉取真实 K 线数据,用 RaptorBT 跑回测。
|
||
|
||
运行前确保:
|
||
pip install raptorbt requests numpy pandas
|
||
|
||
运行:
|
||
python test_with_mt5.py
|
||
"""
|
||
|
||
import os
|
||
import requests
|
||
import numpy as np
|
||
import pandas as pd
|
||
import raptorbt
|
||
from datetime import datetime, timedelta, timezone
|
||
|
||
BRIDGE = "http://61.164.252.86:13485"
|
||
API_KEY = "UiHMqtaYLZzwBdcuS4RFmEGhgDO8N2eI"
|
||
|
||
SYMBOL = "XAUUSD"
|
||
TIMEFRAME = "TIMEFRAME_H1"
|
||
BARS = 500
|
||
|
||
|
||
def api_get(path, params=None):
|
||
resp = requests.get(
|
||
f"{BRIDGE}{path}",
|
||
params=params,
|
||
headers={"X-API-Key": API_KEY},
|
||
timeout=15,
|
||
)
|
||
resp.raise_for_status()
|
||
return resp.json()
|
||
|
||
|
||
def check_health():
|
||
print("=" * 60)
|
||
print("1. 健康检查")
|
||
print("=" * 60)
|
||
data = api_get("/health")
|
||
print(f" 状态: {data.get('status')}")
|
||
print(f" MT5 连接: {data.get('mt5_connected')}")
|
||
if not data.get("mt5_connected"):
|
||
print(" ⚠️ MT5 未连接,后续可能失败")
|
||
return data
|
||
|
||
|
||
def fetch_account():
|
||
print("\n" + "=" * 60)
|
||
print("2. 账户信息")
|
||
print("=" * 60)
|
||
data = api_get("/account")["data"][0]
|
||
print(f" 余额: {data['balance']:.2f} {data['currency']}")
|
||
print(f" 净值: {data['equity']:.2f}")
|
||
print(f" 浮动盈亏: {data['profit']:.2f}")
|
||
print(f" 杠杆: 1:{data['leverage']}")
|
||
return data
|
||
|
||
|
||
def fetch_klines():
|
||
print("\n" + "=" * 60)
|
||
print(f"3. 拉取 K 线数据 ({SYMBOL}, {TIMEFRAME}, 最近 {BARS} 根)")
|
||
print("=" * 60)
|
||
|
||
date_to = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||
date_from = (datetime.now(timezone.utc) - timedelta(days=BARS // 24 + 30)).strftime("%Y-%m-%d")
|
||
|
||
data = api_get("/rates/from-date", params={
|
||
"symbol": SYMBOL,
|
||
"timeframe": TIMEFRAME,
|
||
"date_from": date_from,
|
||
"date_to": date_to,
|
||
})
|
||
|
||
rows = data.get("data", [])
|
||
if not rows:
|
||
print(" ⚠️ 没有拉到数据,尝试 /rates/from-pos ...")
|
||
data = api_get("/rates/from-pos", params={
|
||
"symbol": SYMBOL,
|
||
"timeframe": TIMEFRAME,
|
||
"start_pos": 0,
|
||
"count": BARS,
|
||
})
|
||
rows = data.get("data", [])
|
||
|
||
if not rows:
|
||
raise RuntimeError("两种方式都没拉到 K 线,检查品种名或 MT5 连接")
|
||
|
||
df = pd.DataFrame(rows)
|
||
df["time"] = pd.to_datetime(df["time"])
|
||
df = df.sort_values("time").reset_index(drop=True)
|
||
|
||
print(f" 拉到 {len(df)} 根 K 线")
|
||
print(f" 时间范围: {df['time'].iloc[0]} ~ {df['time'].iloc[-1]}")
|
||
print(f" Close 范围: {df['close'].min():.2f} ~ {df['close'].max():.2f}")
|
||
return df
|
||
|
||
|
||
def run_backtest(df):
|
||
print("\n" + "=" * 60)
|
||
print("4. RaptorBT 回测 (SMA 交叉策略)")
|
||
print("=" * 60)
|
||
|
||
close = df["close"].values.astype(np.float64)
|
||
open_ = df["open"].values.astype(np.float64)
|
||
high = df["high"].values.astype(np.float64)
|
||
low = df["low"].values.astype(np.float64)
|
||
volume = df["tick_volume"].values.astype(np.float64)
|
||
timestamps = df["time"].values.astype("int64")
|
||
|
||
sma_fast = raptorbt.sma(close, period=10)
|
||
sma_slow = raptorbt.sma(close, period=20)
|
||
|
||
entries = (sma_fast > sma_slow) & np.roll(sma_fast <= sma_slow, 1)
|
||
exits = (sma_fast < sma_slow) & np.roll(sma_fast >= sma_slow, 1)
|
||
entries[:20] = False
|
||
exits[:20] = False
|
||
|
||
entries = entries.astype(bool)
|
||
exits = exits.astype(bool)
|
||
|
||
n_entries = int(entries.sum())
|
||
n_exits = int(exits.sum())
|
||
print(f" SMA(10)/SMA(20) 交叉信号: {n_entries} 次入场, {n_exits} 次出场")
|
||
|
||
config = raptorbt.PyBacktestConfig(
|
||
initial_capital=100000.0,
|
||
fees=0.001,
|
||
slippage=0.0005,
|
||
)
|
||
config.set_fixed_stop(0.02)
|
||
config.set_fixed_target(0.04)
|
||
|
||
result = raptorbt.run_single_backtest(
|
||
timestamps=timestamps,
|
||
open=open_,
|
||
high=high,
|
||
low=low,
|
||
close=close,
|
||
volume=volume,
|
||
entries=entries,
|
||
exits=exits,
|
||
direction=1,
|
||
weight=1.0,
|
||
symbol=SYMBOL,
|
||
config=config,
|
||
)
|
||
|
||
m = result.metrics
|
||
print(f"\n {'─' * 40}")
|
||
print(f" 回测结果")
|
||
print(f" {'─' * 40}")
|
||
print(f" 总收益率: {m.total_return_pct:>10.2f} %")
|
||
print(f" 夏普比率: {m.sharpe_ratio:>10.2f}")
|
||
print(f" 索提诺比率: {m.sortino_ratio:>10.2f}")
|
||
print(f" 卡玛比率: {m.calmar_ratio:>10.2f}")
|
||
print(f" 最大回撤: {m.max_drawdown_pct:>10.2f} %")
|
||
print(f" 总交易数: {m.total_trades:>10d}")
|
||
print(f" 胜率: {m.win_rate_pct:>10.1f} %")
|
||
print(f" 盈利因子: {m.profit_factor:>10.2f}")
|
||
print(f" 平均交易收益: {m.avg_trade_return_pct:>10.2f} %")
|
||
print(f" 平均盈利: {m.avg_win_pct:>10.2f} %")
|
||
print(f" 平均亏损: {m.avg_loss_pct:>10.2f} %")
|
||
print(f" 最佳交易: {m.best_trade_pct:>10.2f} %")
|
||
print(f" 最差交易: {m.worst_trade_pct:>10.2f} %")
|
||
print(f" 期望值: {m.expectancy:>10.2f}")
|
||
print(f" SQN: {m.sqn:>10.2f}")
|
||
print(f" 总手续费: {m.total_fees_paid:>10.2f}")
|
||
print(f" 市场暴露: {m.exposure_pct:>10.1f} %")
|
||
|
||
trades = result.trades()
|
||
if trades:
|
||
print(f"\n 最近 5 笔交易:")
|
||
print(f" {'ID':>6} {'方向':>4} {'入场价':>12} {'出场价':>12} {'收益%':>10} {'出场原因':>12}")
|
||
for t in trades[-5:]:
|
||
direction = "多" if t.direction == 1 else "空"
|
||
print(f" {t.id:>6} {direction:>4} {t.entry_price:>12.5f} {t.exit_price:>12.5f} {t.return_pct:>10.2f} {t.exit_reason:>12}")
|
||
|
||
return result
|
||
|
||
|
||
def run_multi_indicator_backtest(df):
|
||
print("\n" + "=" * 60)
|
||
print("5. 多策略回测 (SMA交叉 + RSI均值回归)")
|
||
print("=" * 60)
|
||
|
||
close = df["close"].values.astype(np.float64)
|
||
open_ = df["open"].values.astype(np.float64)
|
||
high = df["high"].values.astype(np.float64)
|
||
low = df["low"].values.astype(np.float64)
|
||
volume = df["tick_volume"].values.astype(np.float64)
|
||
timestamps = df["time"].values.astype("int64")
|
||
|
||
sma_fast = raptorbt.sma(close, period=10)
|
||
sma_slow = raptorbt.sma(close, period=20)
|
||
rsi = raptorbt.rsi(close, period=14)
|
||
|
||
entries_sma = ((sma_fast > sma_slow) & np.roll(sma_fast <= sma_slow, 1)).astype(bool)
|
||
exits_sma = ((sma_fast < sma_slow) & np.roll(sma_fast >= sma_slow, 1)).astype(bool)
|
||
|
||
entries_rsi = (rsi < 30).astype(bool)
|
||
exits_rsi = (rsi > 70).astype(bool)
|
||
|
||
entries_sma[:20] = False
|
||
exits_sma[:20] = False
|
||
entries_rsi[:14] = False
|
||
exits_rsi[:14] = False
|
||
|
||
strategies = [
|
||
(entries_sma, exits_sma, 1, 0.6, "SMA_Cross"),
|
||
(entries_rsi, exits_rsi, 1, 0.4, "RSI_MeanRev"),
|
||
]
|
||
|
||
config = raptorbt.PyBacktestConfig(
|
||
initial_capital=100000.0,
|
||
fees=0.001,
|
||
slippage=0.0005,
|
||
)
|
||
config.set_trailing_stop(0.03)
|
||
|
||
result = raptorbt.run_multi_backtest(
|
||
timestamps=timestamps,
|
||
open=open_,
|
||
high=high,
|
||
low=low,
|
||
close=close,
|
||
volume=volume,
|
||
strategies=strategies,
|
||
config=config,
|
||
combine_mode="any",
|
||
)
|
||
|
||
m = result.metrics
|
||
print(f" 总收益率: {m.total_return_pct:.2f}% 夏普: {m.sharpe_ratio:.2f} "
|
||
f"最大回撤: {m.max_drawdown_pct:.2f}% 交易数: {m.total_trades} 胜率: {m.win_rate_pct:.1f}%")
|
||
return result
|
||
|
||
|
||
def show_indicators(df):
|
||
print("\n" + "=" * 60)
|
||
print("6. 内置指标演示")
|
||
print("=" * 60)
|
||
|
||
close = df["close"].values.astype(np.float64)
|
||
high = df["high"].values.astype(np.float64)
|
||
low = df["low"].values.astype(np.float64)
|
||
volume = df["tick_volume"].values.astype(np.float64)
|
||
|
||
indicators = {
|
||
"SMA(20)": raptorbt.sma(close, period=20),
|
||
"EMA(20)": raptorbt.ema(close, period=20),
|
||
"RSI(14)": raptorbt.rsi(close, period=14),
|
||
"ATR(14)": raptorbt.atr(high, low, close, period=14),
|
||
"ADX(14)": raptorbt.adx(high, low, close, period=14),
|
||
"VWAP": raptorbt.vwap(high, low, close, volume),
|
||
}
|
||
|
||
macd_line, signal_line, hist = raptorbt.macd(close, 12, 26, 9)
|
||
indicators["MACD_Line"] = macd_line
|
||
indicators["MACD_Signal"] = signal_line
|
||
|
||
stoch_k, stoch_d = raptorbt.stochastic(high, low, close, k_period=14, d_period=3)
|
||
indicators["Stoch_K"] = stoch_k
|
||
indicators["Stoch_D"] = stoch_d
|
||
|
||
upper, middle, lower = raptorbt.bollinger_bands(close, period=20, std_dev=2.0)
|
||
indicators["BB_Upper"] = upper
|
||
indicators["BB_Lower"] = lower
|
||
|
||
supertrend_val, supertrend_dir = raptorbt.supertrend(high, low, close, period=10, multiplier=3.0)
|
||
indicators["Supertrend"] = supertrend_val
|
||
|
||
indicators["Rolling_Min(20)"] = raptorbt.rolling_min(low, period=20)
|
||
indicators["Rolling_Max(20)"] = raptorbt.rolling_max(high, period=20)
|
||
|
||
last_idx = -1
|
||
print(f"\n 最新一根 K 线的指标值:")
|
||
print(f" {'指标':<20} {'值':>15}")
|
||
print(f" {'─' * 40}")
|
||
for name, arr in indicators.items():
|
||
val = arr[last_idx]
|
||
if np.isnan(val):
|
||
print(f" {name:<20} {'NaN':>15}")
|
||
else:
|
||
print(f" {name:<20} {val:>15.4f}")
|
||
|
||
|
||
def run_atr_stop_backtest(df):
|
||
print("\n" + "=" * 60)
|
||
print("7. ATR 动态止损 + 风险回报比止盈")
|
||
print("=" * 60)
|
||
|
||
close = df["close"].values.astype(np.float64)
|
||
open_ = df["open"].values.astype(np.float64)
|
||
high = df["high"].values.astype(np.float64)
|
||
low = df["low"].values.astype(np.float64)
|
||
volume = df["tick_volume"].values.astype(np.float64)
|
||
timestamps = df["time"].values.astype("int64")
|
||
|
||
sma_fast = raptorbt.sma(close, period=10)
|
||
sma_slow = raptorbt.sma(close, period=20)
|
||
|
||
entries = ((sma_fast > sma_slow) & np.roll(sma_fast <= sma_slow, 1)).astype(bool)
|
||
exits = ((sma_fast < sma_slow) & np.roll(sma_fast >= sma_slow, 1)).astype(bool)
|
||
entries[:20] = False
|
||
exits[:20] = False
|
||
|
||
config = raptorbt.PyBacktestConfig(
|
||
initial_capital=100000.0,
|
||
fees=0.001,
|
||
slippage=0.0005,
|
||
)
|
||
config.set_atr_stop(multiplier=2.0, period=14)
|
||
config.set_risk_reward_target(ratio=2.0)
|
||
|
||
result = raptorbt.run_single_backtest(
|
||
timestamps=timestamps,
|
||
open=open_, high=high, low=low, close=close,
|
||
volume=volume,
|
||
entries=entries, exits=exits,
|
||
direction=1, weight=1.0, symbol=SYMBOL,
|
||
config=config,
|
||
)
|
||
|
||
m = result.metrics
|
||
print(f" 策略: SMA 交叉 + 2×ATR 止损 + 2:1 风险回报止盈")
|
||
print(f" 总收益率: {m.total_return_pct:.2f}% 夏普: {m.sharpe_ratio:.2f} "
|
||
f"最大回撤: {m.max_drawdown_pct:.2f}% 交易数: {m.total_trades} 胜率: {m.win_rate_pct:.1f}%")
|
||
|
||
trades = result.trades()
|
||
exit_reasons = {}
|
||
for t in trades:
|
||
r = t.exit_reason
|
||
exit_reasons[r] = exit_reasons.get(r, 0) + 1
|
||
print(f" 出场原因分布: {exit_reasons}")
|
||
return result
|
||
|
||
|
||
def run_basket_backtest(df):
|
||
print("\n" + "=" * 60)
|
||
print("8. 篮子回测 (多标的同步信号)")
|
||
print("=" * 60)
|
||
|
||
other_symbols = ["XAUUSD", "EURUSD", "GBPUSD"]
|
||
dfs = {}
|
||
for sym in other_symbols:
|
||
try:
|
||
date_to = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||
date_from = (datetime.now(timezone.utc) - timedelta(days=BARS // 24 + 30)).strftime("%Y-%m-%d")
|
||
data = api_get("/rates/from-date", params={
|
||
"symbol": sym,
|
||
"timeframe": TIMEFRAME,
|
||
"date_from": date_from,
|
||
"date_to": date_to,
|
||
})
|
||
rows = data.get("data", [])
|
||
if not rows:
|
||
print(f" ⚠️ {sym} 无数据,跳过")
|
||
continue
|
||
sdf = pd.DataFrame(rows)
|
||
sdf["time"] = pd.to_datetime(sdf["time"])
|
||
sdf = sdf.sort_values("time").reset_index(drop=True)
|
||
dfs[sym] = sdf
|
||
print(f" {sym}: 拉到 {len(sdf)} 根 K 线")
|
||
except Exception as e:
|
||
print(f" ⚠️ {sym} 拉取失败: {e}")
|
||
|
||
if len(dfs) < 2:
|
||
print(" ⚠️ 可用品种不足 2 个,跳过篮子回测")
|
||
return None
|
||
|
||
min_len = min(len(d) for d in dfs.values())
|
||
instruments = []
|
||
instrument_configs = {}
|
||
per_capital = 100000.0 / len(dfs)
|
||
|
||
for i, (sym, sdf) in enumerate(dfs.items()):
|
||
c = sdf["close"].values.astype(np.float64)[:min_len]
|
||
o = sdf["open"].values.astype(np.float64)[:min_len]
|
||
h = sdf["high"].values.astype(np.float64)[:min_len]
|
||
l = sdf["low"].values.astype(np.float64)[:min_len]
|
||
v = sdf["tick_volume"].values.astype(np.float64)[:min_len]
|
||
ts = sdf["time"].values.astype("int64")[:min_len]
|
||
|
||
sma_f = raptorbt.sma(c, period=10)
|
||
sma_s = raptorbt.sma(c, period=20)
|
||
ent = ((sma_f > sma_s) & np.roll(sma_f <= sma_s, 1)).astype(bool)
|
||
ext = ((sma_f < sma_s) & np.roll(sma_f >= sma_s, 1)).astype(bool)
|
||
ent[:20] = False
|
||
ext[:20] = False
|
||
|
||
instruments.append((ts, o, h, l, c, v, ent, ext, 1, 1.0 / len(dfs), sym))
|
||
inst_cfg = raptorbt.PyInstrumentConfig(lot_size=0.01, alloted_capital=per_capital)
|
||
instrument_configs[sym] = inst_cfg
|
||
|
||
config = raptorbt.PyBacktestConfig(
|
||
initial_capital=100000.0,
|
||
fees=0.001,
|
||
slippage=0.0005,
|
||
)
|
||
config.set_trailing_stop(0.03)
|
||
|
||
for mode in ["any", "all"]:
|
||
result = raptorbt.run_basket_backtest(
|
||
instruments=instruments,
|
||
config=config,
|
||
sync_mode=mode,
|
||
instrument_configs=instrument_configs,
|
||
)
|
||
m = result.metrics
|
||
print(f" sync_mode={mode:<8} → 收益: {m.total_return_pct:>7.2f}% "
|
||
f"夏普: {m.sharpe_ratio:>6.2f} 回撤: {m.max_drawdown_pct:>6.2f}% "
|
||
f"交易: {m.total_trades:>3d} 胜率: {m.win_rate_pct:>5.1f}%")
|
||
return result
|
||
|
||
|
||
def run_pairs_backtest(df):
|
||
print("\n" + "=" * 60)
|
||
print("9. 配对交易 (EURUSD vs GBPUSD)")
|
||
print("=" * 60)
|
||
|
||
pairs = [("EURUSD", "GBPUSD"), ("EURUSD", "USDJPY")]
|
||
for leg1_sym, leg2_sym in pairs:
|
||
try:
|
||
date_to = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||
date_from = (datetime.now(timezone.utc) - timedelta(days=BARS // 24 + 30)).strftime("%Y-%m-%d")
|
||
|
||
data1 = api_get("/rates/from-date", params={
|
||
"symbol": leg1_sym, "timeframe": TIMEFRAME,
|
||
"date_from": date_from, "date_to": date_to,
|
||
})
|
||
data2 = api_get("/rates/from-date", params={
|
||
"symbol": leg2_sym, "timeframe": TIMEFRAME,
|
||
"date_from": date_from, "date_to": date_to,
|
||
})
|
||
|
||
rows1 = data1.get("data", [])
|
||
rows2 = data2.get("data", [])
|
||
if not rows1 or not rows2:
|
||
print(f" ⚠️ {leg1_sym}/{leg2_sym} 数据不足,跳过")
|
||
continue
|
||
|
||
df1 = pd.DataFrame(rows1)
|
||
df2 = pd.DataFrame(rows2)
|
||
df1["time"] = pd.to_datetime(df1["time"])
|
||
df2["time"] = pd.to_datetime(df2["time"])
|
||
df1 = df1.sort_values("time").reset_index(drop=True)
|
||
df2 = df2.sort_values("time").reset_index(drop=True)
|
||
|
||
min_len = min(len(df1), len(df2))
|
||
c1 = df1["close"].values.astype(np.float64)[:min_len]
|
||
c2 = df2["close"].values.astype(np.float64)[:min_len]
|
||
|
||
spread = c1 - c2
|
||
spread_sma = pd.Series(spread).rolling(20).mean().values
|
||
spread_std = pd.Series(spread).rolling(20).std().values
|
||
zscore = (spread - spread_sma) / np.where(spread_std > 0, spread_std, 1e-10)
|
||
|
||
entries = (zscore < -2.0).astype(bool)
|
||
exits = (np.abs(zscore) < 0.5).astype(bool)
|
||
entries[:20] = False
|
||
exits[:20] = False
|
||
|
||
config = raptorbt.PyBacktestConfig(
|
||
initial_capital=100000.0,
|
||
fees=0.001,
|
||
slippage=0.0005,
|
||
)
|
||
config.set_fixed_stop(0.03)
|
||
|
||
result = raptorbt.run_pairs_backtest(
|
||
leg1_timestamps=df1["time"].values.astype("int64")[:min_len],
|
||
leg1_open=df1["open"].values.astype(np.float64)[:min_len],
|
||
leg1_high=df1["high"].values.astype(np.float64)[:min_len],
|
||
leg1_low=df1["low"].values.astype(np.float64)[:min_len],
|
||
leg1_close=c1,
|
||
leg1_volume=df1["tick_volume"].values.astype(np.float64)[:min_len],
|
||
leg2_timestamps=df2["time"].values.astype("int64")[:min_len],
|
||
leg2_open=df2["open"].values.astype(np.float64)[:min_len],
|
||
leg2_high=df2["high"].values.astype(np.float64)[:min_len],
|
||
leg2_low=df2["low"].values.astype(np.float64)[:min_len],
|
||
leg2_close=c2,
|
||
leg2_volume=df2["tick_volume"].values.astype(np.float64)[:min_len],
|
||
entries=entries,
|
||
exits=exits,
|
||
direction=1,
|
||
symbol=f"{leg1_sym}_{leg2_sym}",
|
||
config=config,
|
||
hedge_ratio=1.0,
|
||
)
|
||
|
||
m = result.metrics
|
||
print(f" {leg1_sym}/{leg2_sym} 配对 (Z-score 均值回归):")
|
||
print(f" 收益: {m.total_return_pct:.2f}% 夏普: {m.sharpe_ratio:.2f} "
|
||
f"回撤: {m.max_drawdown_pct:.2f}% 交易: {m.total_trades} 胜率: {m.win_rate_pct:.1f}%")
|
||
except Exception as e:
|
||
print(f" ⚠️ {leg1_sym}/{leg2_sym} 配对失败: {e}")
|
||
return None
|
||
|
||
|
||
def run_monte_carlo(result1, result2):
|
||
print("\n" + "=" * 60)
|
||
print("10. 蒙特卡洛组合模拟")
|
||
print("=" * 60)
|
||
|
||
returns1 = result1.returns()
|
||
returns2 = result2.returns()
|
||
min_len = min(len(returns1), len(returns2))
|
||
|
||
r1 = returns1[:min_len]
|
||
r2 = returns2[:min_len]
|
||
|
||
mask = ~(np.isnan(r1) | np.isnan(r2))
|
||
r1 = r1[mask]
|
||
r2 = r2[mask]
|
||
|
||
if len(r1) < 50:
|
||
print(" ⚠️ 有效收益率数据不足,跳过蒙特卡洛模拟")
|
||
return
|
||
|
||
corr = np.corrcoef(r1, r2)[0, 1]
|
||
correlation_matrix = [
|
||
np.array([1.0, corr]),
|
||
np.array([corr, 1.0]),
|
||
]
|
||
|
||
print(f" 策略 1 (SMA交叉) vs 策略 2 (多策略) 收益率相关系数: {corr:.3f}")
|
||
|
||
for n_sim in [1000, 10000]:
|
||
mc_result = raptorbt.simulate_portfolio_mc(
|
||
returns=[r1, r2],
|
||
weights=np.array([0.6, 0.4]),
|
||
correlation_matrix=correlation_matrix,
|
||
initial_value=100000.0,
|
||
n_simulations=n_sim,
|
||
horizon_days=252,
|
||
seed=42,
|
||
)
|
||
|
||
print(f"\n 模拟次数: {n_sim:,}")
|
||
print(f" {'─' * 40}")
|
||
print(f" 预期收益: {mc_result['expected_return']:>10.2f} %")
|
||
print(f" 亏损概率: {mc_result['probability_of_loss']:>10.2%}")
|
||
print(f" VaR (95%): {mc_result['var_95']:>10.2f} %")
|
||
print(f" CVaR (95%): {mc_result['cvar_95']:>10.2f} %")
|
||
|
||
print(f" 分位数路径终值:")
|
||
for pct, path in mc_result["percentile_paths"]:
|
||
print(f" P{pct:>2.0f}: {path[-1]:>12,.2f}")
|
||
|
||
|
||
def export_results(result, df, strategy_name="sma_cross"):
|
||
print("\n" + "=" * 60)
|
||
print("11. 导出回测结果")
|
||
print("=" * 60)
|
||
|
||
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backtest_output")
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
|
||
trades = result.trades()
|
||
if trades:
|
||
rows = []
|
||
for t in trades:
|
||
rows.append({
|
||
"trade_id": t.id,
|
||
"symbol": t.symbol,
|
||
"direction": "Long" if t.direction == 1 else "Short",
|
||
"entry_idx": t.entry_idx,
|
||
"exit_idx": t.exit_idx,
|
||
"entry_time": df["time"].iloc[t.entry_idx] if t.entry_idx < len(df) else "",
|
||
"exit_time": df["time"].iloc[t.exit_idx] if t.exit_idx < len(df) else "",
|
||
"entry_price": t.entry_price,
|
||
"exit_price": t.exit_price,
|
||
"size": t.size,
|
||
"pnl": t.pnl,
|
||
"return_pct": t.return_pct,
|
||
"fees": t.fees,
|
||
"exit_reason": t.exit_reason,
|
||
})
|
||
trades_df = pd.DataFrame(rows)
|
||
trades_path = os.path.join(output_dir, f"{strategy_name}_trades.csv")
|
||
trades_df.to_csv(trades_path, index=False, encoding="utf-8-sig")
|
||
print(f" 交易记录 → {trades_path} ({len(trades_df)} 笔)")
|
||
|
||
equity = result.equity_curve()
|
||
drawdown = result.drawdown_curve()
|
||
returns = result.returns()
|
||
|
||
curve_df = pd.DataFrame({
|
||
"time": df["time"].values[:len(equity)],
|
||
"equity": equity,
|
||
"drawdown": drawdown,
|
||
"returns": returns,
|
||
})
|
||
curve_path = os.path.join(output_dir, f"{strategy_name}_curves.csv")
|
||
curve_df.to_csv(curve_path, index=False, encoding="utf-8-sig")
|
||
print(f" 权益曲线 → {curve_path} ({len(curve_df)} 根)")
|
||
|
||
m = result.metrics
|
||
metrics_dict = m.to_dict()
|
||
metrics_dict["total_trades"] = m.total_trades
|
||
metrics_dict["total_closed_trades"] = m.total_closed_trades
|
||
metrics_dict["total_open_trades"] = m.total_open_trades
|
||
metrics_dict["winning_trades"] = m.winning_trades
|
||
metrics_dict["losing_trades"] = m.losing_trades
|
||
metrics_dict["max_consecutive_wins"] = m.max_consecutive_wins
|
||
metrics_dict["max_consecutive_losses"] = m.max_consecutive_losses
|
||
metrics_dict["avg_holding_period"] = m.avg_holding_period
|
||
metrics_dict["avg_winning_duration"] = m.avg_winning_duration
|
||
metrics_dict["avg_losing_duration"] = m.avg_losing_duration
|
||
metrics_dict["start_value"] = m.start_value
|
||
metrics_dict["end_value"] = m.end_value
|
||
metrics_dict["total_fees_paid"] = m.total_fees_paid
|
||
metrics_dict["open_trade_pnl"] = m.open_trade_pnl
|
||
metrics_dict["exposure_pct"] = m.exposure_pct
|
||
metrics_dict["payoff_ratio"] = m.payoff_ratio
|
||
metrics_dict["recovery_factor"] = m.recovery_factor
|
||
metrics_dict["omega_ratio"] = m.omega_ratio
|
||
|
||
metrics_df = pd.DataFrame(list(metrics_dict.items()), columns=["metric", "value"])
|
||
metrics_path = os.path.join(output_dir, f"{strategy_name}_metrics.csv")
|
||
metrics_df.to_csv(metrics_path, index=False, encoding="utf-8-sig")
|
||
print(f" 绩效指标 → {metrics_path} ({len(metrics_df)} 项)")
|
||
|
||
print(f"\n 所有文件保存在: {output_dir}")
|
||
|
||
|
||
def test_ferro_indicators(df):
|
||
print("\n" + "=" * 60)
|
||
print("7. ferro-ta 新指标全量测试 (45+ 指标)")
|
||
print("=" * 60)
|
||
|
||
close = df["close"].values.astype(np.float64)
|
||
open_ = df["open"].values.astype(np.float64)
|
||
high = df["high"].values.astype(np.float64)
|
||
low = df["low"].values.astype(np.float64)
|
||
volume = df["tick_volume"].values.astype(np.float64)
|
||
n = len(close)
|
||
errors = []
|
||
tested = 0
|
||
|
||
def check(name, arr, expect_len=None, expect_finite=True, value_range=None, min_valid=1):
|
||
nonlocal tested
|
||
tested += 1
|
||
if expect_len is not None and len(arr) != expect_len:
|
||
errors.append(f"{name}: 长度 {len(arr)} != 期望 {expect_len}")
|
||
return
|
||
valid = arr[~np.isnan(arr)]
|
||
if len(valid) < min_valid:
|
||
errors.append(f"{name}: 有效值仅 {len(valid)} 个 (最少需要 {min_valid})")
|
||
return
|
||
if expect_finite:
|
||
inf_count = np.isinf(valid).sum()
|
||
if inf_count > 0:
|
||
errors.append(f"{name}: 有 {inf_count} 个 inf 值")
|
||
if value_range and len(valid) > 0:
|
||
lo, hi = value_range
|
||
out_of_range = ((valid < lo) | (valid > hi)).sum()
|
||
if out_of_range > 0:
|
||
errors.append(f"{name}: {out_of_range} 个值超出 [{lo}, {hi}]")
|
||
last = arr[-1]
|
||
status = f"{last:.4f}" if not np.isnan(last) else "NaN"
|
||
print(f" {name:<25} 长度={len(arr):>4} 有效值={len(valid):>4} 最新={status}")
|
||
|
||
# ── Overlap 均线类 ──
|
||
print("\n ── Overlap 均线类 ──")
|
||
check("WMA(20)", raptorbt.wma(close, period=20), n, min_valid=10)
|
||
check("DEMA(20)", raptorbt.dema(close, period=20), n, min_valid=10)
|
||
check("TEMA(20)", raptorbt.tema(close, period=20), n, min_valid=10)
|
||
check("KAMA(10)", raptorbt.kama(close, period=10), n, min_valid=10)
|
||
check("T3(20)", raptorbt.t3(close, period=20, vfactor=0.7), n, min_valid=10)
|
||
check("TRIMA(20)", raptorbt.trima(close, period=20), n, min_valid=10)
|
||
check("Midpoint(20)", raptorbt.midpoint(close, period=20), n, min_valid=10)
|
||
check("Midprice(20)", raptorbt.midprice(high, low, period=20), n, min_valid=10)
|
||
check("SAR(0.02,0.2)", raptorbt.sar(high, low, acceleration=0.02, maximum=0.2), n, min_valid=10)
|
||
|
||
# ── Momentum 动量类 ──
|
||
print("\n ── Momentum 动量类 ──")
|
||
check("CCI(20)", raptorbt.cci(high, low, close, period=20), n, min_valid=10)
|
||
check("WillR(14)", raptorbt.willr(high, low, close, period=14), n, value_range=(-101, 1), min_valid=10)
|
||
check("ROC(10)", raptorbt.roc(close, period=10), n, min_valid=10)
|
||
check("MOM(10)", raptorbt.mom(close, period=10), n, min_valid=10)
|
||
check("CMO(14)", raptorbt.cmo(close, period=14), n, min_valid=10)
|
||
check("TRIX(12)", raptorbt.trix(close, period=12), n, min_valid=10)
|
||
check("UltOSC(7,14,28)", raptorbt.ultosc(high, low, close, period1=7, period2=14, period3=28), n, value_range=(-1, 101), min_valid=10)
|
||
check("AroonOsc(14)", raptorbt.aroonosc(high, low, period=14), n, value_range=(-101, 101), min_valid=10)
|
||
check("BOP", raptorbt.bop(open_, high, low, close), n, value_range=(-1.01, 1.01), min_valid=10)
|
||
check("Plus_DI(14)", raptorbt.plus_di(high, low, close, period=14), n, value_range=(-1, 101), min_valid=10)
|
||
check("Minus_DI(14)", raptorbt.minus_di(high, low, close, period=14), n, value_range=(-1, 101), min_valid=10)
|
||
|
||
# ── 多输出动量指标 ──
|
||
print("\n ── 多输出动量指标 ──")
|
||
try:
|
||
stochrsi_k, stochrsi_d = raptorbt.stochrsi(close, timeperiod=14, fastk_period=5, fastd_period=3)
|
||
check("StochRSI_K", stochrsi_k, n, value_range=(-5, 105), min_valid=10)
|
||
check("StochRSI_D", stochrsi_d, n, value_range=(-5, 105), min_valid=10)
|
||
except Exception as e:
|
||
errors.append(f"StochRSI: {e}")
|
||
tested += 2
|
||
|
||
try:
|
||
adx_val, plus_di, minus_di = raptorbt.adx_all(high, low, close, period=14)
|
||
check("ADX_all(14)", adx_val, n, value_range=(-1, 101), min_valid=10)
|
||
check("ADX_all +DI", plus_di, n, value_range=(-1, 101), min_valid=10)
|
||
check("ADX_all -DI", minus_di, n, value_range=(-1, 101), min_valid=10)
|
||
except Exception as e:
|
||
errors.append(f"ADX_all: {e}")
|
||
tested += 3
|
||
|
||
try:
|
||
aroon_up, aroon_down = raptorbt.aroon(high, low, period=14)
|
||
check("Aroon_Up", aroon_up, n, value_range=(-1, 101), min_valid=10)
|
||
check("Aroon_Down", aroon_down, n, value_range=(-1, 101), min_valid=10)
|
||
except Exception as e:
|
||
errors.append(f"Aroon: {e}")
|
||
tested += 2
|
||
|
||
try:
|
||
ppo_line, ppo_signal, ppo_hist = raptorbt.ppo(close, fastperiod=12, slowperiod=26, signalperiod=9)
|
||
check("PPO_Line", ppo_line, n, min_valid=10)
|
||
check("PPO_Signal", ppo_signal, n, min_valid=10)
|
||
check("PPO_Hist", ppo_hist, n, min_valid=10)
|
||
except Exception as e:
|
||
errors.append(f"PPO: {e}")
|
||
tested += 3
|
||
|
||
# ── Volatility 波动率类 ──
|
||
print("\n ── Volatility 波动率类 ──")
|
||
check("NATR(14)", raptorbt.natr(high, low, close, period=14), n, min_valid=10)
|
||
check("TRange", raptorbt.trange(high, low, close), n, min_valid=10)
|
||
check("StdDev(20)", raptorbt.stddev(close, period=20, nbdev=1.0), n, min_valid=10)
|
||
check("VAR(20)", raptorbt.var(close, period=20, nbdev=1.0), n, min_valid=10)
|
||
|
||
# ── Volume 成交量类 ──
|
||
print("\n ── Volume 成交量类 ──")
|
||
check("AD", raptorbt.ad(high, low, close, volume), n, min_valid=10)
|
||
check("ADOSC(3,10)", raptorbt.adosc(high, low, close, volume, fastperiod=3, slowperiod=10), n, min_valid=10)
|
||
check("OBV", raptorbt.obv(close, volume), n, min_valid=10)
|
||
check("MFI(14)", raptorbt.mfi(high, low, close, volume, period=14), n, value_range=(-1, 101), min_valid=10)
|
||
|
||
# ── Price Transform 价格变换类 ──
|
||
print("\n ── Price Transform 价格变换类 ──")
|
||
check("TypPrice", raptorbt.typprice(high, low, close), n, min_valid=10)
|
||
check("MedPrice", raptorbt.medprice(high, low), n, min_valid=10)
|
||
check("AvgPrice", raptorbt.avgprice(open_, high, low, close), n, min_valid=10)
|
||
check("WclPrice", raptorbt.wclprice(high, low, close), n, min_valid=10)
|
||
|
||
# ── Statistic 统计类 ──
|
||
print("\n ── Statistic 统计类 ──")
|
||
check("LinearReg(20)", raptorbt.linearreg(close, period=20), n, min_valid=10)
|
||
check("LinReg_Slope(20)", raptorbt.linearreg_slope(close, period=20), n, min_valid=10)
|
||
check("LinReg_Angle(20)", raptorbt.linearreg_angle(close, period=20), n, min_valid=10)
|
||
check("LinReg_Intercept(20)", raptorbt.linearreg_intercept(close, period=20), n, min_valid=10)
|
||
check("TSF(20)", raptorbt.tsf(close, period=20), n, min_valid=10)
|
||
check("Beta(20)", raptorbt.beta(close, close, period=20), n, min_valid=10)
|
||
check("Correl(20)", raptorbt.correl(close, close, period=20), n, min_valid=10)
|
||
|
||
# ── ADXr ──
|
||
print("\n ── ADXr ──")
|
||
check("ADXr(14)", raptorbt.adxr(high, low, close, period=14), n, value_range=(-1, 101), min_valid=10)
|
||
|
||
# ── APO ──
|
||
print("\n ── APO ──")
|
||
check("APO(12,26)", raptorbt.apo(close, fastperiod=12, slowperiod=26), n, min_valid=10)
|
||
|
||
# ── 汇总 ──
|
||
print("\n" + "─" * 60)
|
||
if errors:
|
||
print(f" ❌ 测试 {tested} 个指标,发现 {len(errors)} 个错误:")
|
||
for e in errors:
|
||
print(f" - {e}")
|
||
else:
|
||
print(f" ✅ 全部 {tested} 个指标测试通过,无错误!")
|
||
print("─" * 60)
|
||
return len(errors) == 0
|
||
|
||
|
||
def run_ferro_strategy_backtest(df):
|
||
print("\n" + "=" * 60)
|
||
print("8. ferro-ta 指标策略回测 (SAR + ADX + CCI 组合)")
|
||
print("=" * 60)
|
||
|
||
close = df["close"].values.astype(np.float64)
|
||
open_ = df["open"].values.astype(np.float64)
|
||
high = df["high"].values.astype(np.float64)
|
||
low = df["low"].values.astype(np.float64)
|
||
volume = df["tick_volume"].values.astype(np.float64)
|
||
timestamps = df["time"].values.astype("int64")
|
||
|
||
sar_val = raptorbt.sar(high, low, acceleration=0.02, maximum=0.2)
|
||
adx_val, plus_di, minus_di = raptorbt.adx_all(high, low, close, period=14)
|
||
cci_val = raptorbt.cci(high, low, close, period=20)
|
||
|
||
sar_above = sar_val > close
|
||
sar_below = sar_val < close
|
||
|
||
adx_strong = adx_val > 25
|
||
bullish_di = plus_di > minus_di
|
||
bearish_di = minus_di > plus_di
|
||
|
||
cci_oversold = cci_val < -100
|
||
cci_overbought = cci_val > 100
|
||
|
||
entries_long = sar_below & adx_strong & bullish_di & cci_oversold
|
||
entries_short = sar_above & adx_strong & bearish_di & cci_overbought
|
||
entries = entries_long | entries_short
|
||
|
||
exits = (sar_below & bearish_di & adx_strong) | (sar_above & bullish_di & adx_strong)
|
||
|
||
warmup = 40
|
||
entries[:warmup] = False
|
||
exits[:warmup] = False
|
||
|
||
n_long = int(entries_long.sum())
|
||
n_short = int(entries_short.sum())
|
||
n_exits = int(exits.sum())
|
||
print(f" SAR+ADX+CCI 组合信号: {n_long} 次做多, {n_short} 次做空, {n_exits} 次出场")
|
||
|
||
config = raptorbt.PyBacktestConfig(
|
||
initial_capital=100000.0,
|
||
fees=0.001,
|
||
slippage=0.0005,
|
||
)
|
||
config.set_atr_stop(multiplier=2.5, period=14)
|
||
config.set_fixed_target(0.04)
|
||
|
||
result = raptorbt.run_single_backtest(
|
||
timestamps=timestamps,
|
||
open=open_, high=high, low=low, close=close,
|
||
volume=volume,
|
||
entries=entries, exits=exits,
|
||
direction=1, weight=1.0, symbol=SYMBOL,
|
||
config=config,
|
||
)
|
||
|
||
m = result.metrics
|
||
print(f"\n {'─' * 40}")
|
||
print(f" SAR+ADX+CCI 策略回测结果")
|
||
print(f" {'─' * 40}")
|
||
print(f" 总收益率: {m.total_return_pct:>10.2f} %")
|
||
print(f" 夏普比率: {m.sharpe_ratio:>10.2f}")
|
||
print(f" 最大回撤: {m.max_drawdown_pct:>10.2f} %")
|
||
print(f" 总交易数: {m.total_trades:>10d}")
|
||
print(f" 胜率: {m.win_rate_pct:>10.1f} %")
|
||
print(f" 盈利因子: {m.profit_factor:>10.2f}")
|
||
|
||
trades = result.trades()
|
||
if trades:
|
||
exit_reasons = {}
|
||
for t in trades:
|
||
r = t.exit_reason
|
||
exit_reasons[r] = exit_reasons.get(r, 0) + 1
|
||
print(f" 出场原因分布: {exit_reasons}")
|
||
|
||
return result
|
||
|
||
|
||
def test_extended_indicators(df):
|
||
"""P0 批 + Hilbert + 市场状态 + 投资组合工具 全量测试"""
|
||
print("\n" + "=" * 60)
|
||
print("12. P0 扩展指标全量测试")
|
||
print("=" * 60)
|
||
|
||
close = df["close"].values.astype(np.float64)
|
||
open_ = df["open"].values.astype(np.float64)
|
||
high = df["high"].values.astype(np.float64)
|
||
low = df["low"].values.astype(np.float64)
|
||
volume = df["tick_volume"].values.astype(np.float64)
|
||
n = len(close)
|
||
errors = []
|
||
|
||
def check(name, arr, expect_len=None, value_range=None, min_valid=1, dtype=None):
|
||
if expect_len is not None and len(arr) != expect_len:
|
||
errors.append(f"{name}: 长度 {len(arr)} != 期望 {expect_len}")
|
||
return
|
||
valid = arr[~np.isnan(arr)]
|
||
if len(valid) < min_valid:
|
||
errors.append(f"{name}: 有效值仅 {len(valid)} 个 (最少需要 {min_valid})")
|
||
return
|
||
if value_range and len(valid) > 0:
|
||
lo, hi = value_range
|
||
out = ((valid < lo) | (valid > hi)).sum()
|
||
if out > 0:
|
||
errors.append(f"{name}: {out} 个值超出 [{lo}, {hi}]")
|
||
if dtype is not None and not isinstance(arr, dtype):
|
||
errors.append(f"{name}: dtype 应为 {dtype.__name__} 而非 {type(arr).__name__}")
|
||
last = arr[-1]
|
||
status = f"{last:.4f}" if not np.isnan(last) else "NaN"
|
||
print(f" {name:<30} 长度={len(arr):>4} 有效值={len(valid):>4} 最新={status}")
|
||
|
||
def check_tuple(name, arrs, expect_len=None, value_ranges=None, min_valids=None):
|
||
"""测试多返回值的指标"""
|
||
if not min_valids:
|
||
min_valids = [1] * len(arrs)
|
||
if not value_ranges:
|
||
value_ranges = [None] * len(arrs)
|
||
for i, (a, vmin, vr) in enumerate(zip(arrs, min_valids, value_ranges)):
|
||
sub = check(f"{name}[{i}]", a, expect_len, vr, vmin)
|
||
|
||
# ── P0 扩展指标 ──
|
||
print("\n ── P0 扩展指标 ──")
|
||
check("VWMA(20)", raptorbt.vwma(close, volume, period=20), n, min_valid=10)
|
||
donch = raptorbt.donchian(high, low, period=20)
|
||
check("Donchian(20) upper", donch[0], n, min_valid=10)
|
||
check("Donchian(20) middle", donch[1], n, min_valid=10)
|
||
check("Donchian(20) lower", donch[2], n, min_valid=10)
|
||
# Donchian 的上下边界应当包含收盘价
|
||
donch_upper = donch[0]
|
||
donch_lower = donch[2]
|
||
valid_idx = ~np.isnan(donch_upper) & ~np.isnan(donch_lower)
|
||
if valid_idx.sum() > 0:
|
||
bad = ((donch_upper[valid_idx] < close[valid_idx]) | (donch_lower[valid_idx] > close[valid_idx])).sum()
|
||
if bad > 0:
|
||
errors.append(f"Donchian: {bad} 根 K 线收盘价落在通道之外")
|
||
|
||
ci_val = raptorbt.choppiness_index(high, low, close, period=14)
|
||
check("Choppiness(14)", ci_val, n, value_range=(-1, 101), min_valid=10)
|
||
|
||
check("HullMA(14)", raptorbt.hull_ma(close, period=14), n, min_valid=10)
|
||
|
||
cl, cs = raptorbt.chandelier_exit(high, low, close, period=22, multiplier=3.0)
|
||
check("Chandelier long_exit", cl, n, min_valid=10)
|
||
check("Chandelier short_exit", cs, n, min_valid=10)
|
||
# Note: long_exit can be < short_exit in sharp trending markets — this is valid.
|
||
# The real check is that both are finite and reasonably close to price.
|
||
|
||
tenkan, kijun, senkou_a, senkou_b, chikou = raptorbt.ichimoku(
|
||
high, low, close, tenkan_period=9, kijun_period=26, senkou_b_period=52, displacement=26)
|
||
check("Ichimoku tenkan", tenkan, n, min_valid=10)
|
||
check("Ichimoku kijun", kijun, n, min_valid=10)
|
||
check("Ichimoku senkou_a", senkou_a, n, min_valid=10)
|
||
check("Ichimoku senkou_b", senkou_b, n, min_valid=10)
|
||
check("Ichimoku chikou", chikou, n, min_valid=10)
|
||
|
||
pivot, r1, s1, r2, s2 = raptorbt.pivot_points(high, low, close, method="classic")
|
||
check("Pivot pivot", pivot, n, min_valid=5)
|
||
check("Pivot R1", r1, n, min_valid=5)
|
||
check("Pivot S1", s1, n, min_valid=5)
|
||
check("Pivot R2", r2, n, min_valid=5)
|
||
check("Pivot S2", s2, n, min_valid=5)
|
||
# R2 > R1 > pivot > S1 > S2 对于有效值
|
||
valid_pp = ~np.isnan(pivot) & ~np.isnan(r1) & ~np.isnan(s1) & ~np.isnan(r2) & ~np.isnan(s2)
|
||
if valid_pp.sum() > 0:
|
||
order = (r2[valid_pp] > r1[valid_pp]) & (r1[valid_pp] > pivot[valid_pp]) & \
|
||
(pivot[valid_pp] > s1[valid_pp]) & (s1[valid_pp] > s2[valid_pp])
|
||
bad = (~order).sum()
|
||
if bad > 0:
|
||
errors.append(f"Pivot: {bad} 处 R2>R1>PIVOT>S1>S2 排序异常")
|
||
|
||
# ── Hilbert Transform ──
|
||
print("\n ── Hilbert Transform (周期变换) ──")
|
||
check("HT_Trendline", raptorbt.ht_trendline(close), n, min_valid=10)
|
||
check("HT_DCPeriod", raptorbt.ht_dcperiod(close), n, value_range=(-1, 100), min_valid=10)
|
||
check("HT_DCPhase", raptorbt.ht_dcphase(close), n, value_range=(-361, 361), min_valid=10)
|
||
ip, quad = raptorbt.ht_phasor(close)
|
||
check("HT_Phasor in_phase", ip, n, min_valid=10)
|
||
check("HT_Phasor quadrature", quad, n, min_valid=10)
|
||
sin, lead = raptorbt.ht_sine(close)
|
||
check("HT_Sine", sin, n, value_range=(-1.05, 1.05), min_valid=10)
|
||
check("HT_LeadSine", lead, n, value_range=(-1.05, 1.05), min_valid=10)
|
||
mode = raptorbt.ht_trendmode(close)
|
||
# mode 是 i32 数组
|
||
check("HT_TrendMode", mode, n, value_range=(-2, 2), min_valid=10)
|
||
# 验证 TrendMode 只有 0/1
|
||
mode_valid = mode[~np.isnan(mode)] if mode.dtype.kind == 'f' else mode
|
||
unique = np.unique(mode_valid)
|
||
bad = np.isin(unique, [0, 1]).all() == False and len(unique) > 0
|
||
if len(unique) > 0 and not set(unique) <= {0, 1}:
|
||
errors.append(f"HT_TrendMode: 含有非 0/1 值: {unique}")
|
||
|
||
# ── 市场状态检测 ──
|
||
print("\n ── 市场状态检测 ──")
|
||
adx_raw = raptorbt.adx(high, low, close, period=14)
|
||
r_adx = raptorbt.regime_adx(adx_raw, threshold=25.0)
|
||
check("Regime_ADX", r_adx, n, value_range=(-2, 2), min_valid=5)
|
||
# 验证只有 -1/0/1
|
||
unique_r = np.unique(r_adx)
|
||
if not set(unique_r) <= {-1, 0, 1}:
|
||
errors.append(f"Regime_ADX: 含有非 -1/0/1 值: {unique_r}")
|
||
|
||
atr_raw = raptorbt.atr(high, low, close, period=14)
|
||
r_combo = raptorbt.regime_combined(adx_raw, atr_raw, close, 25.0, 2.0)
|
||
check("Regime_Combined", r_combo, n, value_range=(-2, 2), min_valid=5)
|
||
unique_c = np.unique(r_combo)
|
||
if not set(unique_c) <= {-1, 0, 1}:
|
||
errors.append(f"Regime_Combined: 含有非 -1/0/1 值: {unique_c}")
|
||
|
||
breaks = raptorbt.detect_breaks_cusum(close, window=30, threshold=1.5, slack=0.5)
|
||
check("Breaks_CUSUM", breaks, n, value_range=(-2, 2), min_valid=5)
|
||
unique_b = np.unique(breaks)
|
||
if not set(unique_b) <= {-1, 0, 1}:
|
||
errors.append(f"Breaks_CUSUM: 含有非 -1/0/1 值: {unique_b}")
|
||
|
||
vol_breaks = raptorbt.rolling_variance_break(close, short_window=10, long_window=30, threshold=2.0)
|
||
check("Breaks_VarRatio", vol_breaks, n, value_range=(-2, 2), min_valid=5)
|
||
unique_v = np.unique(vol_breaks)
|
||
if not set(unique_v) <= {-1, 0, 1}:
|
||
errors.append(f"Breaks_VarRatio: 含有非 -1/0/1 值: {unique_v}")
|
||
|
||
# ── 投资组合工具 ──
|
||
print("\n ── 投资组合工具 ──")
|
||
# 创建第二组数据(随机偏移模拟)
|
||
close2 = close * (1 + np.random.uniform(-0.01, 0.01, n))
|
||
rb = raptorbt.rolling_beta(close, close2, window=20)
|
||
check("Rolling_Beta", rb, n, min_valid=10)
|
||
|
||
dd_series, max_dd = raptorbt.drawdown_series(close)
|
||
check("DrawdownSeries", dd_series, n, value_range=(-1.01, 0.01), min_valid=5)
|
||
if max_dd > 0:
|
||
errors.append(f"DrawdownSeries max_dd 应为非正值: {max_dd}")
|
||
|
||
check("ZScore(20)", raptorbt.zscore_series(close, window=20), n, min_valid=10)
|
||
|
||
returns = np.diff(close) / close[:-1]
|
||
returns2 = np.diff(close2) / close2[:-1]
|
||
rs = raptorbt.relative_strength(returns, returns2)
|
||
check("RelativeStrength", rs, None, min_valid=10)
|
||
|
||
hedge = 1.0
|
||
spr = raptorbt.spread(close, close2, hedge)
|
||
check("Spread", spr, n, min_valid=10)
|
||
# 验证 spread ≈ close - close2
|
||
valid_sp = ~np.isnan(spr) & ~np.isnan(close - close2)
|
||
if valid_sp.sum() > 0:
|
||
diff = np.abs(spr[valid_sp] - (close[valid_sp] - close2[valid_sp]))
|
||
bad = (diff > 1e-6).sum()
|
||
if bad > 0:
|
||
errors.append(f"Spread: {bad} 处与 close - close2 偏差超过 1e-6")
|
||
|
||
r = raptorbt.ratio(close, close2)
|
||
check("Ratio", r, n, min_valid=10)
|
||
# 验证 ratio ≈ close / close2
|
||
valid_r = ~np.isnan(r) & ~np.isnan(close / close2)
|
||
if valid_r.sum() > 0:
|
||
diff_r = np.abs(r[valid_r] - close[valid_r] / close2[valid_r])
|
||
bad_r = (diff_r > 1e-6).sum()
|
||
if bad_r > 0:
|
||
errors.append(f"Ratio: {bad_r} 处与 close / close2 偏差超过 1e-6")
|
||
|
||
# ── 汇总 ──
|
||
print("\n" + "─" * 60)
|
||
if errors:
|
||
print(f" ❌ 测试发现 {len(errors)} 个错误:")
|
||
for e in errors:
|
||
print(f" - {e}")
|
||
else:
|
||
print(f" ✅ 全部扩展指标测试通过,无错误!")
|
||
print("─" * 60)
|
||
return len(errors) == 0
|
||
|
||
|
||
def main():
|
||
print("╔══════════════════════════════════════════════════════════╗")
|
||
print("║ RaptorBT + Mt5Bridge 集成测试 ║")
|
||
print("║ 从 MT5 拉取真实数据 → RaptorBT 回测 ║")
|
||
print("╚══════════════════════════════════════════════════════════╝\n")
|
||
|
||
check_health()
|
||
fetch_account()
|
||
|
||
df = fetch_klines()
|
||
|
||
result1 = run_backtest(df)
|
||
result2 = run_multi_indicator_backtest(df)
|
||
show_indicators(df)
|
||
test_ferro_indicators(df)
|
||
test_extended_indicators(df)
|
||
result_ferro = run_ferro_strategy_backtest(df)
|
||
result3 = run_atr_stop_backtest(df)
|
||
run_basket_backtest(df)
|
||
run_pairs_backtest(df)
|
||
run_monte_carlo(result1, result2)
|
||
|
||
export_results(result1, df, strategy_name="sma_cross")
|
||
export_results(result2, df, strategy_name="multi_strategy")
|
||
if result3:
|
||
export_results(result3, df, strategy_name="atr_stop_rr")
|
||
if result_ferro:
|
||
export_results(result_ferro, df, strategy_name="ferro_sar_adx_cci")
|
||
|
||
print("\n" + "=" * 60)
|
||
print("✅ 全部测试完成!")
|
||
print("=" * 60)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |