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
This commit is contained in:
ZhijuCen
2026-06-24 03:32:28 +08:00
parent d17f68e979
commit cb0f520623
11 changed files with 1069 additions and 100 deletions
+1
View File
@@ -1,6 +1,7 @@
.venv/
__pycache__/
*.pyc
*.log
.extract.log
html_cache/
+14 -7
View File
@@ -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/)
+21
View File
@@ -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.
+115
View File
@@ -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
+57 -70
View File
@@ -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
+83
View File
@@ -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).
+2 -1
View File
@@ -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",
+298 -22
View File
@@ -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
@@ -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
1 Property Value
2 Sector SECTOR_CURRENCY
3 Industry INDUSTRY_UNDEFINED
4 Digits 3
5 Contract size 100000
6 Spread 3 (floating)
7 Stops level 0
8 Margin currency USD
9 Profit currency JPY
10 Calculation Forex
11 Tick size 0.00100000
12 Tick value 0.618839
13 Hedged margin 0.00
14 Chart mode By bid price
15 Trade Full access
16 Execution Market
17 GTC mode Good till cancelled
18 Filling Immediate or Cancel
19 Expiration All
20 Orders Market, Limit, Stop, Stop Limit, Stop Loss, Take Profit
21 Minimal volume 0.01
22 Maximal volume 80.00
23 Volume step 0.01
24 Swap type In points
25 Swap long 5.96
26 Swap short -16.95
27 Swap rates Monday 1
28 Swap rates Tuesday 1
29 Swap rates Wednesday 3
30 Swap rates Thursday 1
31 Swap rates Friday 1
32 Margin rates Market buy Initial 1.0000000
33 Margin rates Market buy usd / lot ~0.00
34 Margin rates Market buy Maintenance 1.0000000
35 Margin rates Market buy usd / lot ~0.00
36 Margin rates Market sell Initial 1.0000000
37 Margin rates Market sell usd / lot ~0.00
38 Margin rates Market sell Maintenance 1.0000000
39 Margin rates Market sell usd / lot ~0.00
@@ -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
1 Property Value
2 Sector SECTOR_COMMODITIES
3 Industry INDUSTRY_COMMODITIES_PRECIOUS
4 Digits 2
5 Contract size 100
6 Spread 12 (floating)
7 Stops level 0
8 Margin currency USD
9 Profit currency USD
10 Calculation CFD Leverage
11 Tick size 0.01000000
12 Tick value 1.000000
13 Hedged margin 25.00
14 Chart mode By bid price
15 Trade Full access
16 Execution Market
17 GTC mode Good till cancelled
18 Filling Immediate or Cancel
19 Expiration All
20 Orders Market, Limit, Stop, Stop Limit, Stop Loss, Take Profit
21 Minimal volume 0.01
22 Maximal volume 80.00
23 Volume step 0.01
24 Swap type In points
25 Swap long -63.88
26 Swap short 40.23
27 Swap rates Monday 1
28 Swap rates Tuesday 1
29 Swap rates Wednesday 3
30 Swap rates Thursday 1
31 Swap rates Friday 1
32 Margin rates Market buy Initial 1.0000000
33 Margin rates Market buy usd / lot ~0.00
34 Margin rates Market buy Maintenance 1.0000000
35 Margin rates Market buy usd / lot ~0.00
36 Margin rates Market sell Initial 1.0000000
37 Margin rates Market sell usd / lot ~0.00
38 Margin rates Market sell Maintenance 1.0000000
39 Margin rates Market sell usd / lot ~0.00
@@ -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()