Files
mql5-skills/skills/mql5/scripts/verify_sl_tp_formulas.py
T
ZhijuCen cb0f520623 feat: SL/TP risk formulas, project docs, symbol specs
SKILL.md Section 5 rewritten with PointValue-based formulas:
- Direction A: SL points → SL price
- Direction B: Risk% + lots → SL price (with currency conversion)
- Direction C: SL price + risk% → lot size
- FindFXRate helper for profit_currency ≠ account_currency
- OrderCalcProfit verification pattern

Section 8 expanded with 6 SL/TP pitfalls (PointValue vs TICK_VALUE,
TickSize ≠ Point, currency conversion, NormalizeDouble rounding,
lot step quantization, STOPS_LEVEL check).

Section 9 EA skeleton now includes inline PointValue, FindFXRate,
CalcSLFromRisk, CalcLotsFromSL functions.

New files:
- LICENSE (MIT)
- README.md
- docs-dev/symbol-spec.md (symbol spec workflow)
- skills/mql5/references/symbol-spec/specs-{XAUUSD,USDJPY}.csv
- skills/mql5/scripts/verify_sl_tp_formulas.py (Python verification)

Updated: pyproject.toml, AGENTS.md, .gitignore, docs-dev/skill-design.md
2026-06-24 03:32:28 +08:00

401 lines
16 KiB
Python
Raw 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.
#!/usr/bin/env python3
"""
Verify SL/TP calculation formulas from MQL5 documentation.
Two symbol types:
- XAUUSD: CFD Leverage mode → Profit = (close-open) * ContractSize * Lots
- USDJPY: Forex mode → Profit = (close-open) * ContractSize * Lots
(but profit currency = JPY, so dollar-equivalent needs conversion)
Bid prices used: XAUUSD=4121.28, USDJPY=161.561
"""
from __future__ import annotations
import csv
from dataclasses import dataclass
from pathlib import Path
# ── Spec loader ──────────────────────────────────────────────────────
SPEC_DIR = Path(__file__).resolve().parent.parent / "references" / "symbol-spec"
@dataclass
class SymbolSpec:
name: str
digits: int
contract_size: float
calc_mode: str
tick_size: float
tick_value: float
stops_level: int
profit_currency: str
lot_min: float
lot_max: float
lot_step: float
@property
def point(self) -> float:
"""SYMBOL_POINT = 10^(-digits)"""
return 10 ** (-self.digits)
@classmethod
def from_csv(cls, path: Path) -> "SymbolSpec":
rows: dict[str, str] = {}
with open(path, newline="") as f:
for row in csv.reader(f):
if len(row) >= 2:
rows[row[0].strip()] = row[1].strip()
return cls(
name=path.stem.replace("specs-", ""),
digits=int(rows["Digits"]),
contract_size=float(rows["Contract size"]),
calc_mode=rows["Calculation"],
tick_size=float(rows["Tick size"]),
tick_value=float(rows["Tick value"]),
stops_level=int(rows["Stops level"]),
profit_currency=rows["Profit currency"],
lot_min=float(rows["Minimal volume"]),
lot_max=float(rows["Maximal volume"]),
lot_step=float(rows["Volume step"]),
)
# ── Core formulas ────────────────────────────────────────────────────
#
# Key distinction:
# PointValue = value of 1 POINT move for 1 lot (in profit currency)
# loss = number_of_points * PointValue * Lots
#
# Therefore:
# number_of_points = loss / (PointValue * Lots)
# sl_distance_price = number_of_points * point
#
# When profit_currency != account_currency, we must convert:
# max_loss_profcy = balance * risk_pct / 100 * fx_rate_to_profcy
#
def point_value(spec: SymbolSpec) -> float:
"""
MPM::PointValue: value of 1 POINT move for 1 lot, in profit currency.
Forex/CFD: point * ContractSize (since Profit = delta * ContractSize * Lots)
Futures: point * TickValue / TickSize
"""
mode = spec.calc_mode.lower()
if "forex" in mode or "cfd" in mode or "stock" in mode:
return spec.point * spec.contract_size
elif "future" in mode:
return spec.point * spec.tick_value / spec.tick_size
raise ValueError(f"Unsupported calc mode: {spec.calc_mode}")
def risk_amount(
balance_usd: float,
risk_pct: float,
spec: SymbolSpec,
fx_rate: float,
) -> float:
"""
Convert risk from account currency (USD) to profit currency.
fx_rate: how many units of profit_currency per 1 USD
e.g. USDJPY=161.561 → fx_rate=161.561
XAUUSD (USD=USD) → fx_rate=1.0
"""
return balance_usd * risk_pct / 100.0 * fx_rate
def calc_sl_from_risk(
spec: SymbolSpec,
max_loss_profcy: float, # risk budget in profit currency
lots: float,
open_price: float,
direction: str, # "BUY" or "SELL"
) -> float:
"""
Given risk budget (in profit currency) and lot size, compute SL price.
points = max_loss / (PointValue * Lots) (number of points)
sl_price = open_price ± points * point
BUY: SL = open - points * point
SELL: SL = open + points * point
"""
pv = point_value(spec)
points = max_loss_profcy / (pv * lots)
sl_distance_price = points * spec.point
if direction == "BUY":
sl = open_price - sl_distance_price
else:
sl = open_price + sl_distance_price
return round(sl, spec.digits)
def calc_lots_from_sl(
spec: SymbolSpec,
max_loss_profcy: float, # risk budget in profit currency
open_price: float,
sl_price: float,
) -> float:
"""
Given a fixed SL price, compute lot size so that loss == max_loss_profcy.
sl_distance_price = abs(open - sl)
points = sl_distance_price / point
Lots = max_loss / (PointValue * points)
Then normalize to lot_step, clamp to [lot_min, lot_max].
"""
sl_distance_price = abs(open_price - sl_price)
if sl_distance_price == 0:
return 0.0
pv = point_value(spec)
points = sl_distance_price / spec.point
raw_lots = max_loss_profcy / (pv * points)
# Normalize to lot_step
normed = int(raw_lots / spec.lot_step) * spec.lot_step
normed = max(normed, spec.lot_min)
normed = min(normed, spec.lot_max)
return round(normed, 8)
def calc_profit(
spec: SymbolSpec, lots: float, open_price: float, close_price: float
) -> float:
"""
Profit in profit currency (from OrderCalcProfit formulas).
"""
mode = spec.calc_mode.lower()
if "forex" in mode or "cfd" in mode or "stock" in mode:
return (close_price - open_price) * spec.contract_size * lots
elif "future" in mode:
return (close_price - open_price) * spec.tick_value / spec.tick_size * lots
raise ValueError(f"Unsupported: {spec.calc_mode}")
# ── Display helpers ──────────────────────────────────────────────────
SEP = "─" * 72
PCY = " " # profit currency suffix
def fmt(v: float, d: int) -> str:
return f"{v:,.{d}f}"
def run_tests(
spec: SymbolSpec, bid: float, account_balance: float, fx_rate: float
):
pc = spec.profit_currency # e.g. "JPY" or "USD"
print(f"\n{SEP}")
print(f" Symbol: {spec.name} | Calc Mode: {spec.calc_mode}")
print(f" Digits={spec.digits} ContractSize={spec.contract_size}")
print(f" Point={spec.point} TickSize={spec.tick_size} TickValue={spec.tick_value}")
print(f" Profit currency: {pc} | FX rate: {fx_rate} {pc}/USD")
print(SEP)
pv = point_value(spec)
print(f" PointValue = {pv} ({pc} per 1-point move, 1 lot)")
print()
# ─────────────────────────────────────────────────────────────────
# Test 1: SL → Profit round-trip (verify formula correctness)
# ─────────────────────────────────────────────────────────────────
print(" TEST 1: SL→Profit round-trip (fixed lots=0.10)")
lots = 0.10
for sl_distance_pts in [100, 500, 1000, 2000]:
sl_dist_price = sl_distance_pts * spec.point
sl_buy = round(bid - sl_dist_price, spec.digits)
loss_buy = calc_profit(spec, lots, bid, sl_buy)
print(f" SL距离={sl_distance_pts:>5} pts "
f"→ 价格距离={sl_dist_price} "
f"BUY SL={fmt(sl_buy, spec.digits)} "
f"亏损={fmt(loss_buy, spec.digits)} {pc}")
# Also verify with Ask = Bid + spread
spread_pts = 12 if spec.name == "XAUUSD" else 3
ask = round(bid + spread_pts * spec.point, spec.digits)
print(f" (Ask={fmt(ask, spec.digits)}, spread={spread_pts} pts)")
print()
# ─────────────────────────────────────────────────────────────────
# Test 2: Risk% → SL price (forward direction)
# ─────────────────────────────────────────────────────────────────
print(f" TEST 2: Risk%→SL (Balance={fmt(account_balance, 2)} USD, Lots=0.10)")
for risk_pct in [0.5, 1.0, 2.0, 5.0]:
ml = risk_amount(account_balance, risk_pct, spec, fx_rate)
for direction in ["BUY", "SELL"]:
price = bid if direction == "BUY" else ask
sl = calc_sl_from_risk(spec, ml, lots, price, direction)
# Verify: compute actual loss at this SL
actual_loss = calc_profit(spec, lots, price, sl)
print(f" Risk={risk_pct}% {direction:4s} "
f"SL={fmt(sl, spec.digits)} "
f"目标亏损={fmt(ml, 2)} {pc} "
f"实际亏损={fmt(actual_loss, 2)} {pc} "
f"差={fmt(abs(actual_loss) - ml, 6)}")
print()
# ─────────────────────────────────────────────────────────────────
# Test 3: Fixed SL price → Lot size (reverse direction)
# ─────────────────────────────────────────────────────────────────
risk_pct = 1.0
ml = risk_amount(account_balance, risk_pct, spec, fx_rate)
print(f" TEST 3: Fixed SL→Lots (Balance={fmt(account_balance, 2)} USD, Risk={risk_pct}%)")
print(f" risk budget = {fmt(ml, 2)} {pc}")
for sl_distance_pts in [100, 500, 1000, 2000]:
sl_dist_price = sl_distance_pts * spec.point
sl_buy = round(bid - sl_dist_price, spec.digits)
lots_calc = calc_lots_from_sl(spec, ml, bid, sl_buy)
if lots_calc > 0:
actual_loss = calc_profit(spec, lots_calc, bid, sl_buy)
else:
actual_loss = 0.0
print(f" SL距离={sl_distance_pts:>5} pts "
f"SL={fmt(sl_buy, spec.digits)} "
f"计算手数={lots_calc:.4f} "
f"实际亏损={fmt(actual_loss, 2)} {pc} "
f"差={fmt(abs(actual_loss) - ml, 6)}")
print()
# ─────────────────────────────────────────────────────────────────
# Test 4: Cross-verify — forward vs reverse should match
# ─────────────────────────────────────────────────────────────────
print(" TEST 4: Cross-verify forward↔reverse")
test_cases = [
(0.5, 500),
(1.0, 1000),
(2.0, 1500),
]
for risk_pct, sl_pts in test_cases:
ml = risk_amount(account_balance, risk_pct, spec, fx_rate)
sl_dist_price = sl_pts * spec.point
sl_buy = round(bid - sl_dist_price, spec.digits)
# Forward: risk% → SL (with lots=0.10)
lots_fwd = 0.10
sl_fwd = calc_sl_from_risk(spec, ml, lots_fwd, bid, "BUY")
# Reverse: SL → lots
lots_rev = calc_lots_from_sl(spec, ml, bid, sl_buy)
# Forward loss check
loss_fwd = calc_profit(spec, lots_fwd, bid, sl_fwd)
# Reverse loss check
loss_rev = calc_profit(spec, lots_rev, bid, sl_buy)
print(f" Risk={risk_pct}% SL距离={sl_pts}pts budget={fmt(ml, 2)} {pc}")
print(f" Forward: SL={fmt(sl_fwd, spec.digits)} lots={lots_fwd:.2f} "
f"loss={fmt(loss_fwd, 2)} {pc} (budget={fmt(ml, 2)})")
print(f" Reverse: lots={lots_rev:.4f} "
f"loss={fmt(loss_rev, 2)} {pc} (budget={fmt(ml, 2)})")
print(f" Δloss = {fmt(abs(loss_fwd) - ml, 6)} "
f"(forward vs budget)")
print()
# ── Main ─────────────────────────────────────────────────────────────
def main():
BALANCE = 10000.0 # USD demo account
specs = {
"XAUUSD": SymbolSpec.from_csv(SPEC_DIR / "specs-XAUUSD.csv"),
"USDJPY": SymbolSpec.from_csv(SPEC_DIR / "specs-USDJPY.csv"),
}
bids = {"XAUUSD": 4121.28, "USDJPY": 161.561}
# FX rates: profit_currency per 1 USD
# XAUUSD: profit=USD → rate=1.0
# USDJPY: profit=JPY → rate=USDJPY_bid
fx_rates = {"XAUUSD": 1.0, "USDJPY": bids["USDJPY"]}
print("=" * 72)
print(" MQL5 SL/TP Formula Verification")
print(f" Account Balance: {BALANCE:,.2f} USD")
print("=" * 72)
for name in ["XAUUSD", "USDJPY"]:
run_tests(specs[name], bids[name], BALANCE, fx_rates[name])
# ─────────────────────────────────────────────────────────────────
# Special: USDJPY — currency conversion walkthrough
# ─────────────────────────────────────────────────────────────────
print(SEP)
print(" USDJPY: Currency Conversion Walkthrough")
print(SEP)
jpy_spec = specs["USDJPY"]
jpy_rate = bids["USDJPY"]
lots = 0.10
risk_pct = 1.0
# Step 1-2: risk budget in profit currency
target_usd = BALANCE * risk_pct / 100.0 # = 100.00 USD
target_jpy = target_usd * jpy_rate # = 16,156.10 JPY
# Step 3: formula → SL
ml_jpy = risk_amount(BALANCE, risk_pct, jpy_spec, jpy_rate)
sl_fwd = calc_sl_from_risk(jpy_spec, ml_jpy, lots, bids["USDJPY"], "BUY")
# Step 4-5: verify
loss_jpy = calc_profit(jpy_spec, lots, bids["USDJPY"], sl_fwd)
loss_usd = loss_jpy / jpy_rate
print(f" Risk={risk_pct}%, Lots={lots}, Balance={fmt(BALANCE, 2)} USD")
print()
print(f" Step 1: risk budget (USD) = {fmt(BALANCE, 2)} × {risk_pct}% = {fmt(target_usd, 2)} USD")
print(f" Step 2: convert to JPY = {fmt(target_usd, 2)} × {jpy_rate} = {fmt(target_jpy, 2)} JPY")
print(f" Step 3: points = budget / (PointValue × Lots)")
print(f" = {fmt(target_jpy, 2)} / ({point_value(jpy_spec):.1f} × {lots}) = {target_jpy / (point_value(jpy_spec) * lots):.1f} pts")
print(f" SL距离 = {target_jpy / (point_value(jpy_spec) * lots):.1f} × {jpy_spec.point} = "
f"{target_jpy / (point_value(jpy_spec) * lots) * jpy_spec.point:.4f} price")
print(f" SL = {bids['USDJPY']} - {target_jpy / (point_value(jpy_spec) * lots) * jpy_spec.point:.4f} = "
f"{fmt(sl_fwd, jpy_spec.digits)}")
print()
print(f" Step 4: actual loss = {fmt(loss_jpy, 2)} JPY")
print(f" Step 5: loss in USD = {fmt(loss_jpy, 2)} / {jpy_rate} = {fmt(loss_usd, 2)} USD")
print()
print(f" Result: target {fmt(target_usd, 2)} USD ≈ actual {fmt(loss_usd, 2)} USD "
f"(差={fmt(abs(loss_usd) - target_usd, 4)} USD, "
f"来自 NormalizeDouble 四舍五入)")
print()
# ─────────────────────────────────────────────────────────────────
# Special: XAUUSD lots sensitivity for 1% risk
# ─────────────────────────────────────────────────────────────────
print(SEP)
print(" XAUUSD: Lots vs SL distance for 1% risk ($100 target loss)")
print(SEP)
xau = specs["XAUUSD"]
ml_usd = risk_amount(BALANCE, 1.0, xau, 1.0)
for lots in [0.01, 0.05, 0.10, 0.50, 1.00, 2.00]:
pv = point_value(xau)
points = ml_usd / (pv * lots)
sl_dist_price = points * xau.point
sl_dist_pts = int(points)
sl = round(bids["XAUUSD"] - sl_dist_price, xau.digits)
# Verify
actual_loss = calc_profit(xau, lots, bids["XAUUSD"], sl)
print(f" Lots={lots:>5.2f} "
f"SL距离={sl_dist_pts:>6} pts ({sl_dist_price:.2f} price) "
f"SL={fmt(sl, xau.digits)} "
f"亏损={fmt(actual_loss, 2)} USD "
f"差={fmt(abs(actual_loss) - ml_usd, 6)}")
if __name__ == "__main__":
main()