- 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>
123 lines
5.2 KiB
Python
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()) |