Files

1090 lines
42 KiB
Python
Raw Permalink Normal View History

2026-07-09 05:08:16 +08:00
"""
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()