Files
2026-07-09 05:08:16 +08:00

1090 lines
42 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()