diff --git a/.gitignore b/.gitignore index 7c0ce6e..407ec17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .venv/ __pycache__/ +*.pyc *.log .extract.log html_cache/ diff --git a/AGENTS.md b/AGENTS.md index 858c57a..e7ba09e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ MQL5 Agent Skills project. Creates and publishes Agent Skills conforming to the ``` mql5-skills/ ├── AGENTS.md # This file — project conventions +├── LICENSE # MIT license ├── README.md # Public readme ├── pyproject.toml # uv project config ├── sitemaps/ # Source sitemaps from mql5.com @@ -28,6 +29,8 @@ mql5-skills/ ├── skills/ │ └── mql5/ # The MQL5 development skill │ ├── SKILL.md # Skill definition (agentskills.io spec) +│ ├── scripts/ +│ │ └── verify_sl_tp_formulas.py # SL/TP risk formula verification │ └── references/ │ ├── book/ # Programming book markdown (from sitemap_book_en.xml) │ │ ├── 0000-book.md @@ -35,16 +38,20 @@ mql5-skills/ │ │ │ ├── 0001-intro.md │ │ │ └── pics/ │ │ └── ... -│ └── docs/ # API reference markdown (from sitemap_docs_en.xml) -│ ├── 0000-docs.md -│ ├── 01-basis/ -│ │ ├── 0001-basis.md -│ │ └── pics/ -│ └── ... +│ ├── docs/ # API reference markdown (from sitemap_docs_en.xml) +│ │ ├── 0000-docs.md +│ │ ├── 01-basis/ +│ │ │ ├── 0001-basis.md +│ │ │ └── pics/ +│ │ └── ... +│ └── symbol-spec/ # Broker symbol specifications (CSV) +│ ├── specs-XAUUSD.csv +│ └── specs-USDJPY.csv └── docs-dev/ # Development documentation ├── extraction.md # Extraction workflow and script design ├── naming.md # Folder/file naming conventions - └── skill-design.md # SKILL.md content plan + ├── skill-design.md # SKILL.md content plan + └── symbol-spec.md # Symbol spec workflow ``` ## SKILL.md Convention (skills/mql5/) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..650b76d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 chikui + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e69de29..743a29b 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,115 @@ +# mql5-skills + +MQL5 Agent Skill for MetaTrader 5 development. Conforms to the +[AgentSkills.io Specification](https://agentskills.io/specification). + +## What's Inside + +An AI agent skill (`skills/mql5/SKILL.md`) that gives an LLM agent working +knowledge of MQL5 development — positions, orders, indicators, risk management, +backtesting — backed by the full MQL5 programming book and API reference +extracted to Markdown. + +``` +skills/mql5/ +├── SKILL.md # The agent skill (agentskills.io spec) +├── scripts/ +│ ├── verify_sl_tp_formulas.py # Python SL/TP risk formula verification +│ └── ... +└── references/ + ├── book/ # MQL5 Programming Book (581 pages) + ├── docs/ # MQL5 API Reference (4135 pages) + └── symbol-spec/ # Broker symbol specifications (CSV) +``` + +## Quick Start + +### Use the Skill + +Point your AI agent at `skills/mql5/SKILL.md`. The skill covers: + +- **Trading operations**: CTrade class, OrderSend, position management +- **Indicators**: built-in handles, multi-timeframe, custom indicators +- **Risk management**: PointValue-based SL/TP calculation, lot sizing, + currency conversion for cross-pair profit currencies +- **Backtesting**: Strategy Tester, OnTester custom criteria, optimization +- **Pitfalls**: 16 documented pitfalls including SL/TP-specific issues + +### Extract References (Development) + +The `references/book/` and `references/docs/` directories are generated from +mql5.com sitemaps via a two-phase pipeline: + +```bash +# Phase 1: Download HTML (needs network) +uv run scripts/extract.py download --all + +# Phase 2: Convert to Markdown (offline) +uv run scripts/extract.py convert --all +``` + +See `docs-dev/extraction.md` for the full pipeline design. + +### Verify Risk Formulas + +```bash +python skills/mql5/scripts/verify_sl_tp_formulas.py +``` + +Tests SL/TP calculation against real symbol specs (XAUUSD, USDJPY) with +verified PointValue-based formulas. + +## Symbol Specs + +Broker-specific symbol specifications live in `skills/mql5/references/symbol-spec/`: + +| File | Symbol | Calc Mode | Contract Size | Digits | +|------|--------|-----------|---------------|--------| +| `specs-XAUUSD.csv` | XAUUSD | CFD Leverage | 100 | 2 | +| `specs-USDJPY.csv` | USDJPY | Forex | 100,000 | 3 | + +These are used by the verification script and can be extended for additional +symbols. + +## Project Structure + +``` +mql5-skills/ +├── AGENTS.md # Project conventions for AI agents +├── LICENSE # MIT +├── README.md # This file +├── pyproject.toml # uv project config (Python 3.14) +├── sitemaps/ # Source sitemaps from mql5.com +│ ├── sitemap_book_en.xml # 581 URLs → programming book +│ └── sitemap_docs_en.xml # 4135 URLs → API reference docs +├── scripts/ # Extraction pipeline +│ └── extract.py # XML → HTML → Markdown +├── skills/mql5/ # The MQL5 agent skill +│ ├── SKILL.md +│ ├── scripts/ +│ └── references/ +├── docs-dev/ # Development documentation +│ ├── extraction.md # Extraction pipeline design +│ ├── naming.md # File/folder naming conventions +│ ├── skill-design.md # SKILL.md content plan +│ └── symbol-spec.md # Symbol spec workflow +└── html_cache/ # Downloaded HTML (gitignored) +``` + +## Development + +```bash +# Setup +uv sync + +# Run extraction +uv run scripts/extract.py download --all +uv run scripts/extract.py convert --all + +# Verify formulas +python skills/mql5/scripts/verify_sl_tp_formulas.py +``` + +## License + +MIT diff --git a/docs-dev/skill-design.md b/docs-dev/skill-design.md index b24fcac..1abcd83 100644 --- a/docs-dev/skill-design.md +++ b/docs-dev/skill-design.md @@ -9,106 +9,93 @@ conforming to the [AgentSkills.io Specification](https://agentskills.io/specific --- name: mql5 description: > - MQL5 development skill for MetaTrader 5. Covers Expert Advisors, Indicators, - Scripts, and Services. Focus on positions, orders, indicators, ticks, and bars. - Includes programming book and API reference documentation. + MQL5 development skill for MetaTrader 5 Expert Advisors, Indicators, Scripts, + and Services. Focus on positions, orders, indicators, ticks, bars, risk + management, backtesting, and multi-instance MT5 operations. Includes + programming book and API reference documentation. version: "0.1" license: MIT compatibility: > Target: MetaTrader 5 platform. Language: MQL5 (C++-like syntax). File extensions: *.mq5 (source), *.mqh (headers). - References: mql5.com/en/book (programming), mql5.com/en/docs (API). + Run time: Windows native, Linux via Wine, macOS via Wine. metadata: project-version: "0.1.0" - sources: - book: sitemaps/sitemap_book_en.xml (581 URLs) - docs: sitemaps/sitemap_docs_en.xml (4135 URLs) focus-areas: - positions - orders - indicators - ticks - bars + - risk-management + - backtesting --- ``` ## Body Content Structure -The body should be structured as follows: +### 1. MQL5 Fundamentals +- Language and file types (.mq5, .mqh, .ex5) +- Program types: EA, Indicator, Script, Service +- MQL5 directory structure (Windows, Linux/Wine) +- Multi-instance MT5 operations -### 1. Overview +### 2. Trading Operations +- Core concepts: Order, Deal, Position +- CTrade class usage patterns +- Position queries (Hedging vs Netting) +- Order execution pattern with error handling -Brief description of MQL5 and MetaTrader 5: -- MQL5 is the programming language for MetaTrader 5 -- Syntax similar to C++ -- File types: `.mq5` (source), `.mqh` (headers) -- Program types: Expert Advisors, Indicators, Scripts, Services +### 3. Indicators and Multi-Timeframe +- Built-in indicator handles (iMA, iRSI, iMACD, iBands) +- Reading indicator values via CopyBuffer +- Multi-timeframe analysis pattern +- New bar detection -### 2. Quick Reference — Key Operations +### 4. Ticks and Bars +- Timeseries access (MqlRates, ArraySetAsSeries) +- Tick data (MqlTick, SymbolInfoTick) +- Key functions table -Focus areas with concise API patterns: +### 5. Risk Management and Lot Sizing +- **PointValue concept**: profit-currency per 1-point move for 1 lot + - Forex/CFD: `point × ContractSize` + - Futures: `point × TickValue / TickSize` +- **Direction A**: SL distance points → SL price +- **Direction B**: Risk% + fixed lots → SL price (with currency conversion) +- **Direction C**: SL price + risk% → lot size +- **Profit verification**: OrderCalcProfit + manual formula +- Risk-to-Reward ratio +- Position sizing rules (7 rules) -#### Positions -- `CTrade` class for position management -- `PositionGetSymbol()`, `PositionSelect()`, `PositionGetDouble()` -- `CTrade::PositionOpen()`, `CTrade::PositionClose()` +### 6. Backtesting and Optimization +- Strategy Tester concepts +- OnTester custom optimization criterion +- Key statistics table +- Backtesting workflow -#### Orders -- `CTrade::OrderSend()` for pending orders -- `ORDER_TYPE_BUY_LIMIT`, `ORDER_TYPE_SELL_LIMIT`, etc. -- `OrderGetTicket()`, `OrderSelect()` +### 7. Event Handlers Reference +- Handler table (OnInit through OnTesterPass) -#### Indicators -- `iMA()`, `iRSI()`, `iMACD()`, `iBands()` — built-in indicators -- `CopyBuffer()` to read indicator values -- `IndicatorCreate()` for custom indicators +### 8. Common Pitfalls +- **General** (10 items): ResultRetcode, MagicNumber, NormalizeDouble, etc. +- **SL/TP and Risk Calculation** (6 items): PointValue vs TICK_VALUE, + TickSize vs Point, currency conversion, NormalizeDouble rounding, + lot step quantization, STOPS_LEVEL check -#### Ticks -- `SymbolInfoTick()` — current tick data -- `MqlTick` structure: `bid`, `ask`, `last`, `volume`, `time` -- `OnTick()` handler for Expert Advisors +### 9. Quick Reference — EA Skeleton +- Complete EA template with inline risk functions: + - `PointValue()`, `FindFXRate()`, `CalcSLFromRisk()`, `CalcLotsFromSL()` + - OnTick example with SL calculation and loss verification -#### Bars -- `Bars()`, `BarsCalculated()` — bar count -- `CopyOpen()`, `CopyHigh()`, `CopyLow()`, `CopyClose()`, `CopyVolume()` -- `CopyRates()`, `CopyTime()` -- `CTerminalInfo`, `CSymbolInfo` for symbol/bar info - -### 3. Program Types - -| Type | Purpose | Key Handler | -|------|---------|-------------| -| Expert Advisor | Automated trading | `OnTick()`, `OnInit()`, `OnDeinit()` | -| Indicator | Technical analysis | `OnCalculate()` | -| Script | One-shot execution | `OnStart()` | -| Service | Background task | `OnStart()`, `OnTimer()` | - -### 4. Common Patterns - -- Trade execution with error handling -- Indicator buffer management -- Timer-based operations -- Chart object manipulation -- File I/O for logging/data - -### 5. References - -Point to the extracted documentation: -- `references/book/` — Programming book (learning path) -- `references/docs/` — API reference (function/type lookup) - -### 6. Pitfalls & Best Practices - -- `RefreshRates()` before trading operations -- `NormalizeDouble()` for price comparisons -- Check `Retcode()` after trade operations -- Use `CTrade` class over raw `OrderSend()` -- Handle `OnTimer()` for periodic operations -- Test with Strategy Tester before live deployment +### 10. References +- In-skill references (book/, docs/, symbol-spec/, scripts/) +- External links (MQL5 Reference, MQL5 Book, Strategy Tester Guide) ## Implementation Notes -- The SKILL.md should be concise (< 1024 chars for description, body can be longer) - Body is loaded as context by AI agents — prioritize actionable patterns - Reference files provide depth; SKILL.md provides the "what to do" +- Risk formulas verified against real symbol specs (XAUUSD, USDJPY) + via `scripts/verify_sl_tp_formulas.py` - Version 0.1: initial content, will expand as extraction completes diff --git a/docs-dev/symbol-spec.md b/docs-dev/symbol-spec.md new file mode 100644 index 0000000..376757c --- /dev/null +++ b/docs-dev/symbol-spec.md @@ -0,0 +1,83 @@ +# Symbol Specification Workflow + +Broker-specific symbol specifications stored as CSV files, used by the +SL/TP risk formula verification script. + +## Location + +``` +skills/mql5/references/symbol-spec/ +├── specs-XAUUSD.csv +├── specs-USDJPY.csv +└── ... +``` + +## CSV Format + +Each file is a two-column CSV (`Property,Value`) exported from the MT5 +Symbol Properties dialog. Key fields: + +| Field | Example (XAUUSD) | Example (USDJPY) | Used By | +|-------|-------------------|-------------------|---------| +| Digits | 2 | 3 | NormalizeDouble, Point | +| Contract size | 100 | 100000 | PointValue, profit formula | +| Calculation | CFD Leverage | Forex | Formula selection | +| Tick size | 0.01 | 0.001 | Futures formula | +| Tick value | 1.0 | 0.618839 | Futures formula | +| Stops level | 0 | 0 | Min SL distance check | +| Profit currency | USD | JPY | Currency conversion | +| Minimal volume | 0.01 | 0.01 | Lot normalization | +| Maximal volume | 80.00 | 80.00 | Lot normalization | +| Volume step | 0.01 | 0.01 | Lot normalization | + +## How to Export from MT5 + +1. In MT5: Tools → Symbols (or press Ctrl+U) +2. Select the symbol → Properties tab +3. Right-click → "Copy" or manually record values +4. Create `specs-{SYMBOL}.csv` with the `Property,Value` format + +## How to Add a New Symbol + +1. Export specs from MT5 (see above) +2. Save as `skills/mql5/references/symbol-spec/specs-{SYMBOL}.csv` +3. Add the symbol to `verify_sl_tp_formulas.py`: + ```python + specs = { + "XAUUSD": SymbolSpec.from_csv(SPEC_DIR / "specs-XAUUSD.csv"), + "USDJPY": SymbolSpec.from_csv(SPEC_DIR / "specs-USDJPY.csv"), + "EURUSD": SymbolSpec.from_csv(SPEC_DIR / "specs-EURUSD.csv"), # new + } + bids = {"XAUUSD": 4121.28, "USDJPY": 161.561, "EURUSD": 1.0850} + fx_rates = {"XAUUSD": 1.0, "USDJPY": 161.561, "EURUSD": 1.0} + ``` +4. Run `python skills/mql5/scripts/verify_sl_tp_formulas.py` to verify + +## Key Distinctions by Calc Mode + +### Forex (SYMBOL_CALC_MODE_FOREX) +- Profit = `(close - open) × ContractSize × Lots` +- PointValue = `point × ContractSize` +- Example: USDJPY — ContractSize=100000, PointValue=100 JPY/pt/lot + +### CFD Leverage (SYMBOL_CALC_MODE_CFDLEVERAGE) +- Profit = `(close - open) × ContractSize × Lots` +- PointValue = `point × ContractSize` +- Example: XAUUSD — ContractSize=100, PointValue=1.0 USD/pt/lot + +### Futures (SYMBOL_CALC_MODE_FUTURES) +- Profit = `(close - open) × TickValue / TickSize × Lots` +- PointValue = `point × TickValue / TickSize` +- Uses broker-supplied TickValue instead of ContractSize + +## Currency Conversion + +When `SYMBOL_CURRENCY_PROFIT ≠ ACCOUNT_CURRENCY`: + +``` +risk_profcy = risk_account_cy × fx_rate +``` + +The verification script uses the `FindFXRate` helper to locate a Forex pair +in Market Watch that converts between the two currencies. In MQL5 code, +this is implemented in `SKILL.md` Section 5 (Direction B/C). diff --git a/pyproject.toml b/pyproject.toml index d91d8f4..c1ecc7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,9 @@ [project] name = "mql5-skills" version = "0.1.0" -description = "Add your description here" +description = "MQL5 Agent Skill for MetaTrader 5 development — extraction pipeline and risk formula verification" readme = "README.md" +license = "MIT" requires-python = ">=3.14" dependencies = [ "beautifulsoup4>=4.15.0", diff --git a/skills/mql5/SKILL.md b/skills/mql5/SKILL.md index bb6fe41..3abcea3 100644 --- a/skills/mql5/SKILL.md +++ b/skills/mql5/SKILL.md @@ -276,35 +276,182 @@ SymbolInfoTick(_Symbol, tick); ## 5. Risk Management and Lot Sizing -### Fixed Percentage Risk +### Core Concept: PointValue + +`PointValue` = profit/loss in profit-currency for a 1-point price move on 1 lot. +This is the foundation for all risk calculations. ```mql5 -double CalculateLotSize(double riskPercent, double slPoints) { - double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); - double riskAmount = accountBalance * riskPercent / 100.0; +double PointValue(string symbol) { + double point = SymbolInfoDouble(symbol, SYMBOL_POINT); + double contract = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE); + ENUM_SYMBOL_CALC_MODE mode = + (ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE); - double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); - double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); - double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); + switch (mode) { + case SYMBOL_CALC_MODE_FOREX: + case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE: + case SYMBOL_CALC_MODE_CFD: + case SYMBOL_CALC_MODE_CFDINDEX: + case SYMBOL_CALC_MODE_CFDLEVERAGE: + case SYMBOL_CALC_MODE_EXCH_STOCKS: + case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX: + return point * contract; - if (tickValue == 0 || tickSize == 0 || slPoints == 0) return 0; + case SYMBOL_CALC_MODE_FUTURES: + case SYMBOL_CALC_MODE_EXCH_FUTURES: + case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS: + return point * SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE) + / SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); + } + return 0; +} +``` - double slMoneyPerLot = (slPoints * point / tickSize) * tickValue; - double lot = riskAmount / slMoneyPerLot; +Key distinction: +- `SYMBOL_TRADE_TICK_VALUE` = profit-currency per tick for **1 lot** (broker-supplied) +- `PointValue` = profit-currency per **1 point** for **1 lot** (computed) +- `loss = points × PointValue × Lots` + +### Direction A: SL Distance Points → SL Price + +Given a stop loss distance in points, compute the SL price level. + +```mql5 +double CalcSLFromPoints(string symbol, double openPrice, int slPoints, + bool isBuy) { + double point = SymbolInfoDouble(symbol, SYMBOL_POINT); + double slDistPrice = slPoints * point; + + if (isBuy) + return NormalizeDouble(openPrice - slDistPrice, + (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)); + else + return NormalizeDouble(openPrice + slDistPrice, + (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)); +} +``` + +### Direction B: Risk % → SL Price (fixed lot size) + +Given account balance, risk %, and lot size, compute where SL must be placed. + +**CRITICAL**: When profit_currency ≠ account_currency, convert risk amount first. + +```mql5 +double CalcSLFromRisk(string symbol, double balance, double riskPct, + double lots, double openPrice, bool isBuy) { + double pv = PointValue(symbol); + if (pv == 0 || lots == 0) return 0; + + double riskAmount = balance * riskPct / 100.0; + + // If profit currency differs from account currency, convert. + // Example: USDJPY → profit=JPY, account=USD → multiply by USDJPY bid + string profCy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT); + string accCy = SymbolInfoString(ACCOUNT_CURRENCY); + if (profCy != accCy) { + // Find exchange rate pair: look for a Forex symbol with + // base=accCy, profit=profCy (or reverse) + string rateSym = ""; + int dir = FindFXRate(accCy, profCy, rateSym); + if (dir == 0) { Print("Cannot convert ", profCy, "→", accCy); return 0; } + MqlTick tick; + SymbolInfoTick(rateSym, tick); + double rate = (dir > 0) ? tick.bid : 1.0 / tick.ask; + riskAmount *= rate; // risk in profit currency + } + + double points = riskAmount / (pv * lots); + double slPrice = points * SymbolInfoDouble(symbol, SYMBOL_POINT); + + if (isBuy) + return NormalizeDouble(openPrice - slPrice, + (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)); + else + return NormalizeDouble(openPrice + slPrice, + (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)); +} + +// Helper: find a Forex pair that converts from→to +// Returns +1 if pair is from/to, -1 if to/from, 0 if not found +int FindFXRate(string from, string to, string &result) { + for (int i = 0; i < SymbolsTotal(true); i++) { + string sym = SymbolName(i, true); + ENUM_SYMBOL_CALC_MODE m = + (ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(sym, SYMBOL_TRADE_CALC_MODE); + if (m != SYMBOL_CALC_MODE_FOREX && + m != SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE) continue; + string base = SymbolInfoString(sym, SYMBOL_CURRENCY_BASE); + string profit = SymbolInfoString(sym, SYMBOL_CURRENCY_PROFIT); + if (base == from && profit == to) { result = sym; return +1; } + if (base == to && profit == from) { result = sym; return -1; } + } + return 0; +} +``` + +### Direction C: SL Price → Lot Size (risk-based sizing) + +Given a fixed SL price, compute the lot size so loss matches the risk budget. + +```mql5 +double CalcLotsFromSL(string symbol, double balance, double riskPct, + double openPrice, double slPrice) { + double pv = PointValue(symbol); + if (pv == 0) return 0; + + double riskAmount = balance * riskPct / 100.0; + + // Currency conversion (same as Direction B above) + string profCy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT); + string accCy = SymbolInfoString(ACCOUNT_CURRENCY); + if (profCy != accCy) { + string rateSym = ""; + int dir = FindFXRate(accCy, profCy, rateSym); + if (dir == 0) return 0; + MqlTick tick; + SymbolInfoTick(rateSym, tick); + double rate = (dir > 0) ? tick.bid : 1.0 / tick.ask; + riskAmount *= rate; + } + + double slDistPrice = MathAbs(openPrice - slPrice); + if (slDistPrice == 0) return 0; + + double point = SymbolInfoDouble(symbol, SYMBOL_POINT); + double points = slDistPrice / point; + double rawLots = riskAmount / (pv * points); // Normalize to broker constraints - double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); - double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); - double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); + double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); + double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); - lot = MathFloor(lot / lotStep) * lotStep; + double lot = MathFloor(rawLots / lotStep) * lotStep; lot = MathMax(lot, minLot); lot = MathMin(lot, maxLot); - return NormalizeDouble(lot, 2); } ``` +### Profit Verification + +Use `OrderCalcProfit` (EA/scripts only) or manual formula to verify: + +```mql5 +// Using OrderCalcProfit +double profit; +OrderCalcProfit(ORDER_TYPE_BUY, symbol, lots, openPrice, closePrice, profit); +// profit is in profit currency + +// Manual formula (Forex/CFD) +double profit = (closePrice - openPrice) * ContractSize * Lots; + +// Manual formula (Futures) +double profit = (closePrice - openPrice) * TickValue / TickSize * Lots; +``` + ### Risk-to-Reward Ratio ```mql5 @@ -317,9 +464,13 @@ double tp = (orderType == ORDER_TYPE_BUY) ? price + tpDistance : price - tpDista ### Position Sizing Rules 1. Never risk more than 1-2% per trade -2. Calculate lot size from risk amount and SL distance -3. Normalize to broker's lot step and min/max constraints -4. Account for spread when calculating SL distance +2. Calculate SL price from risk% and lot size (Direction B), OR + calculate lot size from SL price and risk% (Direction C) +3. Always verify with `OrderCalcProfit` or manual formula +4. Normalize SL with `NormalizeDouble(price, SYMBOL_DIGITS)` +5. Check SL distance ≥ `SYMBOL_TRADE_STOPS_LEVEL × Point` +6. Normalize lots to `SYMBOL_VOLUME_STEP`, clamp to `[VOLUME_MIN, VOLUME_MAX]` +7. When profit_currency ≠ account_currency, convert risk amount via FX rate ## 6. Backtesting and Optimization @@ -407,9 +558,11 @@ EA Development Cycle: ## 8. Common Pitfalls +### General + 1. **Always check `ResultRetcode()`** after `PositionOpen()` — success != execution 2. **Use `SetExpertMagicNumber()`** to distinguish your EA's trades -3. **Normalize prices** with `SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)` +3. **Normalize prices** with `NormalizeDouble(price, SYMBOL_DIGITS)` 4. **Check `Bars() > N`** before trading to ensure enough history 5. **Use `ArraySetAsSeries(true)`** for timeseries arrays (index 0 = latest) 6. **Release indicator handles** in `OnDeinit()` with `IndicatorRelease()` @@ -418,6 +571,15 @@ EA Development Cycle: 9. **Spread varies**: use `SymbolInfoInteger(_Symbol, SYMBOL_SPREAD)` for live spread 10. **Timer in tester**: use `EventSetTimer()` in `OnInit()`, not hardcoded delays +### SL/TP and Risk Calculation + +11. **PointValue ≠ TICK_VALUE**: `SYMBOL_TRADE_TICK_VALUE` is per tick (broker-defined step), `PointValue = point × ContractSize` is per point (smallest price unit). For most Forex: TickSize = Point, so they coincide; for futures/metals they may differ. +12. **TickSize ≠ Point**: Always use the correct formula for the symbol's `SYMBOL_TRADE_CALC_MODE`. Forex/CFD: `loss = delta_price × ContractSize × Lots`. Futures: `loss = delta_price × TickValue / TickSize × Lots`. +13. **Profit currency ≠ Account currency**: USDJPY profit is JPY, not USD. Risk amount must be converted: `risk_JPY = risk_USD × USDJPY_bid`. Failing this makes risk 100×+ too small. +14. **NormalizeDouble introduces rounding**: SL price rounded to `SYMBOL_DIGITS` causes ~0.01-0.02% deviation from target loss. Acceptable; verify with `OrderCalcProfit`. +15. **Lot step quantization**: `MathFloor(rawLots / lotStep) * lotStep` can leave residual risk unmet. For large lot_step or small risk budgets, actual loss may differ from target by up to one lot_step worth of loss. +16. **STOPS_LEVEL check**: SL must be ≥ `SYMBOL_TRADE_STOPS_LEVEL × Point` from current price. If stops_level ≤ 0, use a safety margin (e.g. 150 points). + ## 9. Quick Reference — EA Skeleton ```mql5 @@ -440,6 +602,100 @@ CTrade trade; bool IsHedging; datetime lastBarTime = 0; +//+------------------------------------------------------------------+ +//| PointValue: profit-currency per 1-point move for 1 lot | +//+------------------------------------------------------------------+ +double PointValue(string symbol) { + double point = SymbolInfoDouble(symbol, SYMBOL_POINT); + double contract = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE); + ENUM_SYMBOL_CALC_MODE mode = + (ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE); + if (mode == SYMBOL_CALC_MODE_FUTURES || + mode == SYMBOL_CALC_MODE_EXCH_FUTURES || + mode == SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS) + return point * SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE) + / SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); + return point * contract; // Forex, CFD, Stocks +} + +//+------------------------------------------------------------------+ +//| FindFXRate: locate a Forex pair for currency conversion | +//+------------------------------------------------------------------+ +int FindFXRate(string from, string to, string &result) { + for (int i = 0; i < SymbolsTotal(true); i++) { + string sym = SymbolName(i, true); + ENUM_SYMBOL_CALC_MODE m = + (ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(sym, SYMBOL_TRADE_CALC_MODE); + if (m != SYMBOL_CALC_MODE_FOREX && + m != SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE) continue; + string base = SymbolInfoString(sym, SYMBOL_CURRENCY_BASE); + string profit = SymbolInfoString(sym, SYMBOL_CURRENCY_PROFIT); + if (base == from && profit == to) { result = sym; return +1; } + if (base == to && profit == from) { result = sym; return -1; } + } + return 0; +} + +//+------------------------------------------------------------------+ +//| CalcSLFromRisk: risk% + lots → SL price | +//+------------------------------------------------------------------+ +double CalcSLFromRisk(string symbol, double balance, double riskPct, + double lots, double openPrice, bool isBuy) { + double pv = PointValue(symbol); + if (pv == 0 || lots == 0) return 0; + double riskAmount = balance * riskPct / 100.0; + + // Currency conversion if needed + string profCy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT); + string accCy = SymbolInfoString(ACCOUNT_CURRENCY); + if (profCy != accCy) { + string rateSym = ""; + int dir = FindFXRate(accCy, profCy, rateSym); + if (dir == 0) return 0; + MqlTick tick; SymbolInfoTick(rateSym, tick); + riskAmount *= (dir > 0) ? tick.bid : 1.0 / tick.ask; + } + + double points = riskAmount / (pv * lots); + double slPrice = points * SymbolInfoDouble(symbol, SYMBOL_POINT); + int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); + return isBuy ? NormalizeDouble(openPrice - slPrice, digits) + : NormalizeDouble(openPrice + slPrice, digits); +} + +//+------------------------------------------------------------------+ +//| CalcLotsFromSL: SL price + risk% → lot size | +//+------------------------------------------------------------------+ +double CalcLotsFromSL(string symbol, double balance, double riskPct, + double openPrice, double slPrice) { + double pv = PointValue(symbol); + if (pv == 0) return 0; + double riskAmount = balance * riskPct / 100.0; + + string profCy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT); + string accCy = SymbolInfoString(ACCOUNT_CURRENCY); + if (profCy != accCy) { + string rateSym = ""; + int dir = FindFXRate(accCy, profCy, rateSym); + if (dir == 0) return 0; + MqlTick tick; SymbolInfoTick(rateSym, tick); + riskAmount *= (dir > 0) ? tick.bid : 1.0 / tick.ask; + } + + double slDist = MathAbs(openPrice - slPrice); + if (slDist == 0) return 0; + double points = slDist / SymbolInfoDouble(symbol, SYMBOL_POINT); + double rawLots = riskAmount / (pv * points); + + double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); + double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); + double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); + double lot = MathFloor(rawLots / lotStep) * lotStep; + lot = MathMax(lot, minLot); + lot = MathMin(lot, maxLot); + return NormalizeDouble(lot, 2); +} + //+------------------------------------------------------------------+ int OnInit() { IsHedging = ((ENUM_ACCOUNT_MARGIN_MODE) @@ -465,13 +721,28 @@ void OnTick() { if (barTime == lastBarTime) return; lastBarTime = barTime; - // Analysis and trading logic here - // ... + // Example: buy with 1% risk, SL at 500 points + double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + int slPts = 500; + double sl = CalcSLFromPoints(_Symbol, bid, slPts, true); + // Or: double sl = CalcSLFromRisk(_Symbol, + // AccountInfoDouble(ACCOUNT_BALANCE), RiskPercent, + // 0.10, bid, true); + + double lots = CalcLotsFromSL(_Symbol, + AccountInfoDouble(ACCOUNT_BALANCE), RiskPercent, bid, sl); + + // Verify loss matches risk budget + double profit; + OrderCalcProfit(ORDER_TYPE_BUY, _Symbol, lots, bid, sl, profit); + PrintFormat("SL=%.5f lots=%.2f expected_loss=%.2f", + sl, lots, profit); + + // trade.Buy(lots, _Symbol, 0, sl, 0, "EA Signal"); } //+------------------------------------------------------------------+ double OnTester() { - // Custom optimization criterion double trades = TesterStatistics(STAT_TRADES); if (trades < 30) return 0; return TesterStatistics(STAT_PROFIT_FACTOR); @@ -497,6 +768,11 @@ double OnTester() { - `24-customind/` — Custom indicator creation - `13-event-handlers/` — Event handlers (OnTick, OnTester, etc.) - `34-standardlibrary/` — Standard library (CTrade, CPositionInfo, etc.) + - `01-constants/` — Enums and structures (MqlTradeRequest, ENUM_SYMBOL_CALC_MODE) +- `references/symbol-spec/` — Symbol specification CSVs (broker-specific) + - `specs-XAUUSD.csv` — XAUUSD: CFD Leverage, ContractSize=100, Digits=2 + - `specs-USDJPY.csv` — USDJPY: Forex, ContractSize=100000, Digits=3 +- `scripts/verify_sl_tp_formulas.py` — Python verification of SL/TP risk formulas ### External diff --git a/skills/mql5/references/symbol-spec/specs-USDJPY.csv b/skills/mql5/references/symbol-spec/specs-USDJPY.csv new file mode 100644 index 0000000..e7cd7cc --- /dev/null +++ b/skills/mql5/references/symbol-spec/specs-USDJPY.csv @@ -0,0 +1,39 @@ +Property,Value +Sector,SECTOR_CURRENCY +Industry,INDUSTRY_UNDEFINED +Digits,3 +Contract size,100000 +Spread,3 (floating) +Stops level,0 +Margin currency,USD +Profit currency,JPY +Calculation,Forex +Tick size,0.00100000 +Tick value,0.618839 +Hedged margin,0.00 +Chart mode,By bid price +Trade,Full access +Execution,Market +GTC mode,Good till cancelled +Filling,Immediate or Cancel +Expiration,All +Orders,"Market, Limit, Stop, Stop Limit, Stop Loss, Take Profit" +Minimal volume,0.01 +Maximal volume,80.00 +Volume step,0.01 +Swap type,In points +Swap long,5.96 +Swap short,-16.95 +Swap rates Monday,1 +Swap rates Tuesday,1 +Swap rates Wednesday,3 +Swap rates Thursday,1 +Swap rates Friday,1 +Margin rates Market buy Initial,1.0000000 +Margin rates Market buy usd / lot,~0.00 +Margin rates Market buy Maintenance,1.0000000 +Margin rates Market buy usd / lot,~0.00 +Margin rates Market sell Initial,1.0000000 +Margin rates Market sell usd / lot,~0.00 +Margin rates Market sell Maintenance,1.0000000 +Margin rates Market sell usd / lot,~0.00 diff --git a/skills/mql5/references/symbol-spec/specs-XAUUSD.csv b/skills/mql5/references/symbol-spec/specs-XAUUSD.csv new file mode 100644 index 0000000..e435d32 --- /dev/null +++ b/skills/mql5/references/symbol-spec/specs-XAUUSD.csv @@ -0,0 +1,39 @@ +Property,Value +Sector,SECTOR_COMMODITIES +Industry,INDUSTRY_COMMODITIES_PRECIOUS +Digits,2 +Contract size,100 +Spread,12 (floating) +Stops level,0 +Margin currency,USD +Profit currency,USD +Calculation,CFD Leverage +Tick size,0.01000000 +Tick value,1.000000 +Hedged margin,25.00 +Chart mode,By bid price +Trade,Full access +Execution,Market +GTC mode,Good till cancelled +Filling,Immediate or Cancel +Expiration,All +Orders,"Market, Limit, Stop, Stop Limit, Stop Loss, Take Profit" +Minimal volume,0.01 +Maximal volume,80.00 +Volume step,0.01 +Swap type,In points +Swap long,-63.88 +Swap short,40.23 +Swap rates Monday,1 +Swap rates Tuesday,1 +Swap rates Wednesday,3 +Swap rates Thursday,1 +Swap rates Friday,1 +Margin rates Market buy Initial,1.0000000 +Margin rates Market buy usd / lot,~0.00 +Margin rates Market buy Maintenance,1.0000000 +Margin rates Market buy usd / lot,~0.00 +Margin rates Market sell Initial,1.0000000 +Margin rates Market sell usd / lot,~0.00 +Margin rates Market sell Maintenance,1.0000000 +Margin rates Market sell usd / lot,~0.00 diff --git a/skills/mql5/scripts/verify_sl_tp_formulas.py b/skills/mql5/scripts/verify_sl_tp_formulas.py new file mode 100644 index 0000000..68b82c3 --- /dev/null +++ b/skills/mql5/scripts/verify_sl_tp_formulas.py @@ -0,0 +1,400 @@ +#!/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()