Files
gavindiazandClaude Code ae47b4e30f feat: EURUSD 策略 + MT5 盈亏/仓位折算率修复 (tick_value 语义)
- mtlib: PnL/仓位统一走 报价货币->账户货币 折算率 (tick_value 语义),
  USDJPY 已用 demo 实盘+导出记录逐分验证关闭 (偏差 <0.01 USD)
- run_backtest/position_sizing 由注册商品自动推导折算率 (USD 报价对子恒 1.0)
- 新增 eurusd_scalp 策略 (M1 均值回归, MAGIC=2026091501) + 50k 数据
- 协议入口 AGENTS.md / CLAUDE.md / .mcp.json; 策略登记 MAGIC/DATA_FILE
- 验证记录 ReportHistory-52845377.html (12 笔 deal 交叉核对, smoke 47 PASS)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 20:08:53 +08:00

123 lines
5.2 KiB
Python

"""商品事实注册表 - 与具体策略无关的商品级信息。
规则:
- 这里只放"事实" (实盘点差/合约大小/交易时段/盈利换算), 不放"策略选择"(TP倍数等)
- 点差必须实测后写入, 严禁拍脑袋; spread_max_pressure 用于压力测试
- 新增商品: 加一个 dataclass 字段/注册行即可, 不需要动任何策略代码
"""
from dataclasses import dataclass, field
# 账户货币 (实测 ICMarketsSC-Demo: USD)。盈利/风险一律以账户货币计。
ACCOUNT_CURRENCY = "USD"
@dataclass(frozen=True)
class Instrument:
symbol: str # MT5 里的交易品种名
timeframe: str # 默认周期 (M1)
contract_size: float # 1 手对应多少基础单位 (XAUUSD: 100 盎司)
min_volume: float # 最小手数
price_digits: int # 报价小数位
measured_spread: float # 实测点差, 必须实测后填 (账户货币单位)
spread_max_pressure: float # 压力测试用点差 (通常 实测 x 1.4 左右)
session_utc: frozenset # 允许交易的时段 (UTC 小时集合, 离散)
profit_currency: str = "USD" # 报价货币 (PnL 原币). USDJPY 报价=日元, 需折算账户货币
def __str__(self):
hours = ",".join(str(h) for h in sorted(self.session_utc))
return (f"{self.symbol}[{self.timeframe}] "
f"contract={self.contract_size} min_vol={self.min_volume} "
f"spread={self.measured_spread} (pressure={self.spread_max_pressure}) "
f"session_utc={{{hours}}}")
XAUUSD = Instrument(
symbol="XAUUSD",
timeframe="M1",
contract_size=100,
min_volume=0.01,
price_digits=2,
measured_spread=0.16, # 2026-09 实测黄金 bid/ask 差 ~16 points
spread_max_pressure=0.22, # 压力测试档: 实测点差 x1.4 左右
session_utc=frozenset({7, 8, 9, 10, 16, 17, 18, 20}),
)
EURUSD = Instrument(
symbol="EURUSD",
timeframe="M1",
contract_size=100000,
min_volume=0.01,
price_digits=5,
measured_spread=0.00001, # 2026-09 微结构实测可成交点差中位数 1 point (240min/11257 ticks, p95=1)
spread_max_pressure=0.00002, # 压力测试档: 覆盖 p99=2 points, 实测值 x2 保守档
session_utc=frozenset({7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), # 伦敦开 ~ 纽约尾盘
)
USDJPY = Instrument(
symbol="USDJPY",
timeframe="M1",
contract_size=100000,
min_volume=0.01,
price_digits=3,
measured_spread=0.009, # 2026-09 实盘点差: 157.786/157.795 = 9 pips (多日实测 8~11 pips)
spread_max_pressure=0.014, # 压力测试档: 覆盖 14 pips (伦敦/纽约重叠时点差放大)
session_utc=frozenset({0, 1, 2, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), # 东京+伦敦+纽约
profit_currency="JPY", # 报价货币=日元, PnL 原币非账户货币, 需折算
)
# 注册表: 通过 symbol 名查商品
_INSTRUMENTS = {inst.symbol: inst for inst in [XAUUSD, EURUSD, USDJPY]}
def quote_to_account_series(inst, df):
"""回测用逐K线折算率序列 (与 df 对齐, 直达 run_backtest)。
- USD 报价对子: 全 1.0 (与旧公式恒等)
- 直接 JPY 对子 (USDJPY): 1/close, 每根K线历史精确
"""
if inst.profit_currency == ACCOUNT_CURRENCY:
return None
if not inst.symbol.upper().startswith(ACCOUNT_CURRENCY):
raise NotImplementedError(
f"{inst.symbol} 为交叉对子, 折算需额外直盘汇率, 未实现")
close = df['close'].to_numpy(dtype=float)
return [1.0 / c if c and c > 0 else 1.0 for c in close]
def quote_to_account_rate(inst, price=None):
"""1 单位报价货币 = 多少账户货币 (PnL 原币→账户货币折算率)。
依据 MT5 tick_value 语义: tick_value = tick_size * contract_size
* quote_to_account_rate, 使之与实盘成交记录逐分对齐。
- 报价货币 == 账户货币 (XAUUSD/EURUSD): 恒为 1.0 (与旧公式恒等)
- 直接对子 (USDJPY, 基准货币==账户货币): 1 JPY = 1/price USD,
price 即该对子自身报价 (回测传每根K线收盘价, 实盘传最新收盘价)
- 交叉对子 (如 EURJPY): 需账户/报价直盘汇率, 暂未实现 (抛错防误用)
"""
if inst.profit_currency == ACCOUNT_CURRENCY:
return 1.0
if price is None or price <= 0:
raise ValueError(
f"{inst.symbol} 报价货币 {inst.profit_currency} != 账户货币 "
f"{ACCOUNT_CURRENCY}, 必须传入该对子当时报价价以做折算")
if inst.symbol.upper().startswith(ACCOUNT_CURRENCY):
# 直接对子: 基准货币==账户货币, 1 报价单位 = 1/price 账户货币
return 1.0 / price
raise NotImplementedError(
f"{inst.symbol} 为交叉对子 (基准货币非 {ACCOUNT_CURRENCY}), "
f"折算需要额外直盘汇率, 未实现; 请先让实盘记录确认后再加入")
def get_instrument(symbol):
"""按 MT5 symbol 取商品事实, 未注册时报错(防止拼写静默错误)。"""
try:
return _INSTRUMENTS[symbol.upper()]
except KeyError:
raise KeyError(
f"商品 {symbol} 未注册, 请先在 mtlib/instruments.py 中添加 "
f"(含实测点差与交易时段)")
def all_instruments():
return list(_INSTRUMENTS.values())