feat: MQL5 MultiSignal DCA CCBSN EA - 9 signal modes + adaptive DCA + CCBSN partial close
- 9 indicator signal modes: Ichimoku, EMA Cross, RSI, BB Bounce, Stoch, CCI, MACD, Supertrend, Momentum - DCA multi-tier with adaptive distance and lot multiplier - CCBSN (partial close 50% -> breakeven -> trailing) - Sniper (trim oldest losing orders) - Anti-Detect for Prop Firm compliance - Python ML optimizer for DCA parameters - Wave strategy brute-force optimizer (2240+ combos) - 85+ optimizable inputs for MT5 Strategy Tester - Auto-detect filling type (Exness compatibility) - Retry logic for order closing Built by @hungpixi | Comarai.com
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
# Sensitive files
|
||||
context.md
|
||||
*.set
|
||||
*.csv
|
||||
ml_params.mqh
|
||||
.env*
|
||||
|
||||
# Compiled
|
||||
*.ex5
|
||||
*.log
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
# MQL5 MultiSignal DCA CCBSN EA
|
||||
|
||||
> **Expert Advisor thông minh cho MetaTrader 5** — 9 loại tín hiệu × DCA đa tầng × CCBSN (Chốt Cắt Bán Sớm Nửa) × Sniper tỉa lệnh × Anti-Detect Prop Firm
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 🎯 Tư Duy & Điểm Khác Biệt
|
||||
|
||||
### Vấn đề của các bot DCA truyền thống
|
||||
- ❌ Chỉ 1 loại signal (thường là MA cross) → bỏ lỡ nhiều cơ hội
|
||||
- ❌ DCA lot cố định → cháy tài khoản khi sideway dài
|
||||
- ❌ TP chuỗi rồi đóng hết → bỏ lỡ trend lớn
|
||||
- ❌ Không anti-detect → bị prop firm phát hiện
|
||||
|
||||
### Giải pháp: MultiSignal + CCBSN + ML
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────┐ ┌──────────────┐
|
||||
│ 9 Signal │────▶│ DCA │────▶│ CCBSN │
|
||||
│ Modes │ │ Adaptive │ │ Partial Close│
|
||||
│ │ │ │ │ + Trailing │
|
||||
│ Ichimoku │ │ Distance │ │ │
|
||||
│ EMA Cross │ │ tăng dần │ │ Chốt 50% │
|
||||
│ RSI OB/OS │ │ │ │ → Move BE │
|
||||
│ BB Bounce │ │ Lot │ │ → Trail phần │
|
||||
│ Stochastic │ │ tăng dần │ │ còn lại │
|
||||
│ CCI │ │ │ │ │
|
||||
│ MACD Cross │ │ Filter │ │ = Maximize │
|
||||
│ Supertrend │ │ by trend │ │ profit per │
|
||||
│ Momentum │ │ │ │ chain │
|
||||
└──────────────┘ └──────────┘ └──────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ EMA + MACD │ │ Sniper │
|
||||
│ Trend Filter │ │ Tỉa lệnh │
|
||||
│ │ │ đầu chuỗi │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### Tư duy thiết kế
|
||||
1. **Multi-Signal Selector**: Thay vì hardcode 1 indicator, user chọn signal phù hợp từng thị trường qua `InpIndiMode` → optimize trên Strategy Tester
|
||||
2. **CCBSN**: Khi chuỗi DCA về TP → chỉ chốt 50%. Phần còn lại: breakeven + trailing → **bắt trend lớn thay vì chỉ scalp nhỏ**
|
||||
3. **ML Optimizer** (Python): Train decision tree trên data lịch sử → tự tìm DCA distance + lot size tối ưu per volatility regime
|
||||
4. **Wave Strategy**: Brute-force 2240+ combo indicators → tìm chiến lược có lợi nhất trên dữ liệu cũ
|
||||
|
||||
## 📦 Cấu Trúc
|
||||
|
||||
| File | Mô tả |
|
||||
|------|--------|
|
||||
| `IchiDCA_CCBSN_PropFirm_Fixed.mq5` | EA chính — 9 signals, DCA, CCBSN, Sniper, Anti-Detect |
|
||||
| `wave_strategy_optimizer.py` | Brute-force 2240+ combos tìm chiến lược tốt nhất |
|
||||
| `dca_ml_optimizer.py` | ML tối ưu DCA parameters theo volatility regime |
|
||||
|
||||
## 🚀 Cài Đặt
|
||||
|
||||
### 1. Copy vào MT5
|
||||
```
|
||||
Copy IchiDCA_CCBSN_PropFirm_Fixed.mq5 →
|
||||
C:\Users\<USER>\AppData\Roaming\MetaQuotes\Terminal\<ID>\MQL5\Experts\
|
||||
```
|
||||
|
||||
### 2. Compile
|
||||
Mở MetaEditor → Open file → Compile (F7)
|
||||
|
||||
### 3. Chạy
|
||||
Drag & drop EA lên chart XAUUSD M5 → Cấu hình inputs
|
||||
|
||||
## ⚙️ Inputs Chính (85+ parameters)
|
||||
|
||||
### Signal Selection
|
||||
| Input | Giá trị | Mô tả |
|
||||
|-------|---------|-------|
|
||||
| `InpIndiMode` | 0-8 | Chọn loại tín hiệu |
|
||||
| `InpTFSignal` | PERIOD_M5 | Timeframe cho signal |
|
||||
|
||||
### DCA
|
||||
| Input | Default | Mô tả |
|
||||
|-------|---------|-------|
|
||||
| `InpDCADistance` | 10.0 | Khoảng cách DCA (pips) |
|
||||
| `InpDCADistMulti` | 1.2 | Hệ số nhân khoảng cách |
|
||||
| `InpDCALotMulti` | 1.0 | Hệ số nhân lot |
|
||||
| `InpDCATPPips` | 50.0 | TP chuỗi DCA |
|
||||
| `InpMaxDCAOrders` | 5 | Max DCA orders |
|
||||
|
||||
### CCBSN
|
||||
| Input | Default | Mô tả |
|
||||
|-------|---------|-------|
|
||||
| `InpUsePartialClose` | true | Bật chốt nửa |
|
||||
| `InpPartialPercent` | 50% | % lot chốt |
|
||||
| `InpMoveSLToBE` | true | Move SL breakeven |
|
||||
| `InpTrailStartPips` | 10.0 | Pips kích hoạt trail |
|
||||
|
||||
## 🐍 Python Optimizer
|
||||
|
||||
### Wave Strategy Optimizer
|
||||
```bash
|
||||
pip install numpy pandas scikit-learn
|
||||
|
||||
# Export XAUUSD M5 data from MT5 → CSV
|
||||
python wave_strategy_optimizer.py --data XAUUSD_M5.csv --output-dir ./
|
||||
```
|
||||
|
||||
### ML DCA Optimizer
|
||||
```bash
|
||||
python dca_ml_optimizer.py --data XAUUSD_M5.csv
|
||||
# → Generates ml_params.mqh with optimized DCA parameters
|
||||
```
|
||||
|
||||
## 🐛 Known Issues & Fixes
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| Exness: `ORDER_FILLING_IOC` not supported | `GetFillingType()` auto-detect |
|
||||
| `iBands` buffer 0 ≠ UPPER | Buffer: 0=BASE, 1=UPPER, 2=LOWER |
|
||||
| Close position fails | Retry 3x with Sleep(500) |
|
||||
| EMA trend + BB bounce = 0 signals | Don't mix trend filter with mean reversion |
|
||||
|
||||
## 🗺️ Hướng Phát Triển
|
||||
|
||||
- [ ] Walk-forward optimization (train/test split)
|
||||
- [ ] Multi-timeframe signal confluence
|
||||
- [ ] Risk management per volatility regime (ML-driven)
|
||||
- [ ] Dashboard web theo dõi performance real-time
|
||||
- [ ] Thêm signal: Order Block, Fair Value Gap (ICT concepts)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License — Thoải mái sử dụng, chỉnh sửa, phân phối.
|
||||
|
||||
---
|
||||
|
||||
## 💼 Bạn muốn Bot Trading tương tự?
|
||||
|
||||
| Bạn cần | Chúng tôi đã làm ✅ |
|
||||
|---------|---------------------|
|
||||
| Bot DCA thông minh | ✅ 9 signal modes + adaptive DCA |
|
||||
| Quản lý rủi ro tự động | ✅ CCBSN + Sniper + Equity Trailing |
|
||||
| Tối ưu bằng ML | ✅ Python optimizer → hardcode MQ5 |
|
||||
| Bot cho Prop Firm | ✅ Anti-Detect features built-in |
|
||||
| Backtest & Optimize | ✅ 85+ inputs cho MT5 Strategy Tester |
|
||||
|
||||
<p align="center">
|
||||
<a href="https://comarai.com"><img src="https://img.shields.io/badge/🎯_Yêu_cầu_Demo-comarai.com-blue?style=for-the-badge" alt="Demo"></a>
|
||||
<a href="https://zalo.me/0834422439"><img src="https://img.shields.io/badge/💬_Zalo-0834422439-green?style=for-the-badge" alt="Zalo"></a>
|
||||
<a href="mailto:hungphamphunguyen@gmail.com"><img src="https://img.shields.io/badge/📧_Email-Contact-red?style=for-the-badge" alt="Email"></a>
|
||||
</p>
|
||||
|
||||
### 🤖 Comarai — Companion for Marketing & AI Automation
|
||||
|
||||
> *"Bạn không cần thuê 10 nhân viên. Bạn cần 4 AI Agents chạy 24/7."*
|
||||
|
||||
| Em Sale | Em Content | Em Marketing | Em Trade |
|
||||
|---------|-----------|-------------|----------|
|
||||
| Tìm khách tự động | Viết content viral | Chạy campaign 24/7 | Trade bot thông minh |
|
||||
|
||||
<p align="center">
|
||||
<b>Built by <a href="https://github.com/hungpixi">@hungpixi</a> | <a href="https://comarai.com">Comarai.com</a></b>
|
||||
</p>
|
||||
@@ -0,0 +1,877 @@
|
||||
"""
|
||||
DCA ML Optimizer - Machine Learning DCA Parameter Optimization
|
||||
================================================================
|
||||
Phân tích dữ liệu nến lịch sử XAUUSD, tìm khoảng cách DCA + lot tối ưu
|
||||
theo volatility regime, phát hiện pattern "blow-up".
|
||||
|
||||
Usage:
|
||||
python dca_ml_optimizer.py --data XAUUSD_M5.csv
|
||||
python dca_ml_optimizer.py --data XAUUSD_M5.csv --export-mt5
|
||||
python dca_ml_optimizer.py --test (chạy với sample data)
|
||||
|
||||
Output:
|
||||
- ml_params.mqh (hardcoded params cho MQ5)
|
||||
- optimization_log.csv (log chi tiết)
|
||||
|
||||
Author: Comarai (https://comarai.com) - AI-powered trading optimization
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
|
||||
# Try import sklearn, fallback to simple heuristic if not available
|
||||
try:
|
||||
from sklearn.tree import DecisionTreeRegressor
|
||||
from sklearn.ensemble import GradientBoostingRegressor
|
||||
from sklearn.model_selection import cross_val_score
|
||||
HAS_SKLEARN = True
|
||||
except ImportError:
|
||||
HAS_SKLEARN = False
|
||||
print("[WARN] scikit-learn not installed. Using heuristic optimization.")
|
||||
print(" Install: pip install scikit-learn numpy pandas")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONSTANTS
|
||||
# =============================================================================
|
||||
PIP_VALUE_XAUUSD = 0.1 # 1 pip = 0.1 cho XAUUSD (2 or 3 digit broker)
|
||||
|
||||
# Default settings from 2-2-test.set (reference)
|
||||
DEFAULT_DCA_DISTANCE = 10.0 # pips
|
||||
DEFAULT_DCA_DIST_MULTI = 1.2
|
||||
DEFAULT_LOT = 0.19
|
||||
DEFAULT_LOT_MULTI = 1.0 # lot cố định
|
||||
DEFAULT_MAX_DCA = 5
|
||||
DEFAULT_TP_DCA = 50.0 # pips
|
||||
|
||||
# Regime thresholds (will be refined by ML)
|
||||
ATR_PERCENTILES = [25, 50, 75, 90] # Low, Medium, High, Extreme
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DATA LOADING
|
||||
# =============================================================================
|
||||
def load_candle_data(filepath):
|
||||
"""Load candle data from CSV exported by MT5.
|
||||
|
||||
Supports formats:
|
||||
- MT5 default export: Date, Time, Open, High, Low, Close, TickVolume, Volume, Spread
|
||||
- Custom: datetime, open, high, low, close, volume
|
||||
"""
|
||||
candles = []
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8-sig') as f:
|
||||
# Detect delimiter
|
||||
first_line = f.readline()
|
||||
f.seek(0)
|
||||
|
||||
delimiter = '\t' if '\t' in first_line else ','
|
||||
reader = csv.reader(f, delimiter=delimiter)
|
||||
|
||||
# Try to detect header
|
||||
header = next(reader)
|
||||
header_lower = [h.strip().lower() for h in header]
|
||||
|
||||
# Map columns
|
||||
col_map = {}
|
||||
for i, h in enumerate(header_lower):
|
||||
if h in ('date', 'datetime', '<date>'):
|
||||
col_map['date'] = i
|
||||
elif h in ('time', '<time>'):
|
||||
col_map['time'] = i
|
||||
elif h in ('open', '<open>'):
|
||||
col_map['open'] = i
|
||||
elif h in ('high', '<high>'):
|
||||
col_map['high'] = i
|
||||
elif h in ('low', '<low>'):
|
||||
col_map['low'] = i
|
||||
elif h in ('close', '<close>'):
|
||||
col_map['close'] = i
|
||||
elif h in ('tickvol', 'tick_volume', '<tickvol>', 'volume', '<vol>'):
|
||||
col_map['volume'] = i
|
||||
elif h in ('spread', '<spread>'):
|
||||
col_map['spread'] = i
|
||||
|
||||
if 'open' not in col_map:
|
||||
raise ValueError(f"Cannot detect OHLC columns. Header: {header}")
|
||||
|
||||
for row in reader:
|
||||
try:
|
||||
if len(row) < 4:
|
||||
continue
|
||||
|
||||
candle = {
|
||||
'open': float(row[col_map['open']]),
|
||||
'high': float(row[col_map['high']]),
|
||||
'low': float(row[col_map['low']]),
|
||||
'close': float(row[col_map['close']]),
|
||||
}
|
||||
|
||||
# Parse datetime
|
||||
if 'date' in col_map and 'time' in col_map:
|
||||
date_str = row[col_map['date']].strip()
|
||||
time_str = row[col_map['time']].strip()
|
||||
for fmt in ('%Y.%m.%d %H:%M:%S', '%Y.%m.%d %H:%M',
|
||||
'%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M',
|
||||
'%Y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M'):
|
||||
try:
|
||||
candle['datetime'] = datetime.strptime(f"{date_str} {time_str}", fmt)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
elif 'date' in col_map:
|
||||
date_str = row[col_map['date']].strip()
|
||||
for fmt in ('%Y.%m.%d %H:%M:%S', '%Y.%m.%d %H:%M',
|
||||
'%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M',
|
||||
'%Y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M',
|
||||
'%Y.%m.%d', '%Y-%m-%d'):
|
||||
try:
|
||||
candle['datetime'] = datetime.strptime(date_str, fmt)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if 'volume' in col_map:
|
||||
candle['volume'] = int(float(row[col_map['volume']]))
|
||||
if 'spread' in col_map:
|
||||
candle['spread'] = int(float(row[col_map['spread']]))
|
||||
|
||||
candles.append(candle)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
print(f"[INFO] Loaded {len(candles)} candles from {filepath}")
|
||||
if candles:
|
||||
print(f" Range: {candles[0].get('datetime', 'N/A')} -> {candles[-1].get('datetime', 'N/A')}")
|
||||
return candles
|
||||
|
||||
|
||||
def generate_sample_data(n=50000):
|
||||
"""Generate realistic XAUUSD M5 sample data for testing."""
|
||||
np.random.seed(42)
|
||||
candles = []
|
||||
price = 2650.0 # Starting price
|
||||
dt = datetime(2024, 1, 1, 0, 0)
|
||||
|
||||
# Simulate different volatility regimes
|
||||
regime_length = 2000 # ~7 days of M5 candles
|
||||
|
||||
for i in range(n):
|
||||
# Change regime periodically
|
||||
regime = (i // regime_length) % 4
|
||||
if regime == 0: # Low volatility
|
||||
vol = 0.8
|
||||
elif regime == 1: # Medium volatility
|
||||
vol = 2.0
|
||||
elif regime == 2: # High volatility
|
||||
vol = 4.0
|
||||
else: # Extreme (news/blow-up)
|
||||
vol = 8.0
|
||||
|
||||
# Random walk with drift
|
||||
drift = np.random.normal(0, 0.01)
|
||||
change = np.random.normal(drift, vol)
|
||||
|
||||
o = price
|
||||
h = o + abs(np.random.normal(0, vol * 0.7))
|
||||
l = o - abs(np.random.normal(0, vol * 0.7))
|
||||
c = o + change
|
||||
|
||||
h = max(h, o, c) + abs(np.random.normal(0, vol * 0.2))
|
||||
l = min(l, o, c) - abs(np.random.normal(0, vol * 0.2))
|
||||
|
||||
candles.append({
|
||||
'datetime': dt,
|
||||
'open': round(o, 2),
|
||||
'high': round(h, 2),
|
||||
'low': round(l, 2),
|
||||
'close': round(c, 2),
|
||||
'volume': np.random.randint(100, 5000),
|
||||
'spread': np.random.randint(20, 50),
|
||||
})
|
||||
|
||||
price = c
|
||||
dt += timedelta(minutes=5)
|
||||
# Skip weekends
|
||||
if dt.weekday() >= 5:
|
||||
dt += timedelta(days=7 - dt.weekday())
|
||||
|
||||
print(f"[INFO] Generated {len(candles)} sample candles")
|
||||
return candles
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FEATURE ENGINEERING
|
||||
# =============================================================================
|
||||
def compute_atr(candles, period=14):
|
||||
"""Compute ATR (Average True Range) in price terms."""
|
||||
atrs = [0.0] * len(candles)
|
||||
|
||||
for i in range(1, len(candles)):
|
||||
tr = max(
|
||||
candles[i]['high'] - candles[i]['low'],
|
||||
abs(candles[i]['high'] - candles[i-1]['close']),
|
||||
abs(candles[i]['low'] - candles[i-1]['close'])
|
||||
)
|
||||
|
||||
if i < period:
|
||||
atrs[i] = tr
|
||||
else:
|
||||
atrs[i] = (atrs[i-1] * (period - 1) + tr) / period
|
||||
|
||||
return atrs
|
||||
|
||||
|
||||
def compute_volatility_features(candles, atr_m5):
|
||||
"""Compute volatility regime features for each candle."""
|
||||
features = []
|
||||
|
||||
# ATR values for H1 and H4 aggregation
|
||||
h1_candles_per = 12 # 12 M5 candles = 1 hour
|
||||
h4_candles_per = 48 # 48 M5 candles = 4 hours
|
||||
d1_candles_per = 288 # 288 M5 candles = 1 day
|
||||
|
||||
# Compute rolling ATR statistics
|
||||
atr_lookback = 100 # 100 bars lookback for percentile
|
||||
|
||||
for i in range(max(d1_candles_per, atr_lookback), len(candles)):
|
||||
atr_val = atr_m5[i]
|
||||
|
||||
# ATR percentile (where does current ATR sit vs recent history)
|
||||
recent_atrs = atr_m5[i - atr_lookback:i]
|
||||
recent_atrs_sorted = sorted(recent_atrs)
|
||||
atr_percentile = sum(1 for x in recent_atrs_sorted if x <= atr_val) / len(recent_atrs_sorted) * 100
|
||||
|
||||
# H1 range (avg of last 12 candles)
|
||||
h1_range = sum(c['high'] - c['low'] for c in candles[i - h1_candles_per:i]) / h1_candles_per
|
||||
|
||||
# H4 range
|
||||
h4_range = sum(c['high'] - c['low'] for c in candles[i - h4_candles_per:i]) / h4_candles_per
|
||||
|
||||
# D1 range
|
||||
d1_high = max(c['high'] for c in candles[i - d1_candles_per:i])
|
||||
d1_low = min(c['low'] for c in candles[i - d1_candles_per:i])
|
||||
d1_range = d1_high - d1_low
|
||||
|
||||
# Trend strength (simple: price vs 50-bar SMA)
|
||||
sma50 = sum(c['close'] for c in candles[i - 50:i]) / 50
|
||||
trend_strength = (candles[i]['close'] - sma50) / sma50 * 100 # % away from SMA
|
||||
|
||||
# Candle body ratio (body / total range)
|
||||
body = abs(candles[i]['close'] - candles[i]['open'])
|
||||
total_range = candles[i]['high'] - candles[i]['low']
|
||||
body_ratio = body / total_range if total_range > 0 else 0
|
||||
|
||||
# Upper/lower wick ratio
|
||||
if candles[i]['close'] >= candles[i]['open']:
|
||||
upper_wick = candles[i]['high'] - candles[i]['close']
|
||||
lower_wick = candles[i]['open'] - candles[i]['low']
|
||||
else:
|
||||
upper_wick = candles[i]['high'] - candles[i]['open']
|
||||
lower_wick = candles[i]['close'] - candles[i]['low']
|
||||
|
||||
wick_ratio = upper_wick / lower_wick if lower_wick > 0 else 10.0
|
||||
|
||||
# Max drawdown in recent N candles (simulating worst case reversal)
|
||||
max_up_move = 0
|
||||
max_down_move = 0
|
||||
for lookback in [24, 48, 96, 288]: # 2h, 4h, 8h, 24h
|
||||
if i >= lookback:
|
||||
start_price = candles[i - lookback]['close']
|
||||
max_price = max(c['high'] for c in candles[i - lookback:i])
|
||||
min_price = min(c['low'] for c in candles[i - lookback:i])
|
||||
max_up_move = max(max_up_move, (max_price - start_price) / PIP_VALUE_XAUUSD)
|
||||
max_down_move = max(max_down_move, (start_price - min_price) / PIP_VALUE_XAUUSD)
|
||||
|
||||
# Volatility regime classification
|
||||
if atr_percentile < 25:
|
||||
regime = 0 # LOW
|
||||
elif atr_percentile < 50:
|
||||
regime = 1 # MEDIUM
|
||||
elif atr_percentile < 75:
|
||||
regime = 2 # HIGH
|
||||
else:
|
||||
regime = 3 # EXTREME
|
||||
|
||||
features.append({
|
||||
'idx': i,
|
||||
'atr_m5': atr_val,
|
||||
'atr_percentile': atr_percentile,
|
||||
'h1_range': h1_range,
|
||||
'h4_range': h4_range,
|
||||
'd1_range': d1_range,
|
||||
'trend_strength': trend_strength,
|
||||
'body_ratio': body_ratio,
|
||||
'wick_ratio': min(wick_ratio, 10.0),
|
||||
'max_up_pips': max_up_move,
|
||||
'max_down_pips': max_down_move,
|
||||
'regime': regime,
|
||||
'price': candles[i]['close'],
|
||||
})
|
||||
|
||||
return features
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DCA SIMULATION
|
||||
# =============================================================================
|
||||
def simulate_dca_chain(candles, start_idx, direction, dca_distance, dca_dist_multi,
|
||||
lot_size, lot_multi, max_dca, tp_pips, max_total_lots=50.0,
|
||||
max_bars=5760): # max 5760 bars = 20 days
|
||||
"""Simulate a DCA chain starting from start_idx.
|
||||
|
||||
Returns:
|
||||
dict with result: 'tp_hit', 'blowup', 'timeout', and stats
|
||||
"""
|
||||
entry_price = candles[start_idx]['close']
|
||||
|
||||
# Track orders
|
||||
orders = [{'price': entry_price, 'lots': lot_size, 'bar': start_idx}]
|
||||
total_lots = lot_size
|
||||
|
||||
next_dca_dist = dca_distance
|
||||
bars_elapsed = 0
|
||||
max_adverse_pips = 0
|
||||
max_lots_reached = total_lots
|
||||
|
||||
for i in range(start_idx + 1, min(start_idx + max_bars, len(candles))):
|
||||
bars_elapsed += 1
|
||||
current_price = candles[i]['close']
|
||||
current_high = candles[i]['high']
|
||||
current_low = candles[i]['low']
|
||||
|
||||
# Calculate average entry price
|
||||
avg_price = sum(o['price'] * o['lots'] for o in orders) / total_lots
|
||||
|
||||
# Check TP
|
||||
if direction == 'buy':
|
||||
profit_pips = (current_high - avg_price) / PIP_VALUE_XAUUSD
|
||||
adverse_pips = (avg_price - current_low) / PIP_VALUE_XAUUSD
|
||||
else:
|
||||
profit_pips = (avg_price - current_low) / PIP_VALUE_XAUUSD
|
||||
adverse_pips = (current_high - avg_price) / PIP_VALUE_XAUUSD
|
||||
|
||||
max_adverse_pips = max(max_adverse_pips, adverse_pips)
|
||||
|
||||
if profit_pips >= tp_pips:
|
||||
return {
|
||||
'result': 'tp_hit',
|
||||
'bars': bars_elapsed,
|
||||
'total_lots': total_lots,
|
||||
'max_lots': max_lots_reached,
|
||||
'num_dca': len(orders) - 1,
|
||||
'max_adverse_pips': max_adverse_pips,
|
||||
'avg_price': avg_price,
|
||||
'entry_price': entry_price,
|
||||
}
|
||||
|
||||
# Check if should add DCA
|
||||
last_price = orders[-1]['price']
|
||||
if direction == 'buy':
|
||||
dist_from_last = (last_price - current_price) / PIP_VALUE_XAUUSD
|
||||
else:
|
||||
dist_from_last = (current_price - last_price) / PIP_VALUE_XAUUSD
|
||||
|
||||
if dist_from_last >= next_dca_dist and len(orders) <= max_dca:
|
||||
new_lot = lot_size
|
||||
for _ in range(len(orders)):
|
||||
new_lot *= lot_multi
|
||||
new_lot = round(new_lot, 2)
|
||||
|
||||
if total_lots + new_lot > max_total_lots:
|
||||
# BLOWUP: would exceed max lots
|
||||
return {
|
||||
'result': 'blowup',
|
||||
'bars': bars_elapsed,
|
||||
'total_lots': total_lots,
|
||||
'max_lots': max_lots_reached,
|
||||
'num_dca': len(orders) - 1,
|
||||
'max_adverse_pips': max_adverse_pips,
|
||||
'avg_price': avg_price,
|
||||
'entry_price': entry_price,
|
||||
'blow_distance_pips': adverse_pips,
|
||||
}
|
||||
|
||||
orders.append({'price': current_price, 'lots': new_lot, 'bar': i})
|
||||
total_lots += new_lot
|
||||
max_lots_reached = max(max_lots_reached, total_lots)
|
||||
next_dca_dist = dca_distance
|
||||
for _ in range(len(orders) - 1):
|
||||
next_dca_dist *= dca_dist_multi
|
||||
|
||||
# Timeout
|
||||
avg_price = sum(o['price'] * o['lots'] for o in orders) / total_lots
|
||||
return {
|
||||
'result': 'timeout',
|
||||
'bars': bars_elapsed,
|
||||
'total_lots': total_lots,
|
||||
'max_lots': max_lots_reached,
|
||||
'num_dca': len(orders) - 1,
|
||||
'max_adverse_pips': max_adverse_pips,
|
||||
'avg_price': avg_price,
|
||||
'entry_price': entry_price,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ML OPTIMIZATION
|
||||
# =============================================================================
|
||||
def optimize_dca_params(candles, features, lot_size=0.19):
|
||||
"""Find optimal DCA parameters for each volatility regime."""
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" DCA PARAMETER OPTIMIZATION")
|
||||
print("=" * 60)
|
||||
|
||||
# Test different DCA distance settings per regime
|
||||
distance_options = [5, 8, 10, 12, 15, 20, 25, 30, 40, 50]
|
||||
max_dca_options = [2, 3, 4, 5, 6, 8]
|
||||
lot_multi_options = [1.0, 1.1, 1.2, 1.3, 1.5]
|
||||
|
||||
# Sample starting points from features
|
||||
sample_size = min(2000, len(features))
|
||||
sample_indices = np.random.choice(len(features), sample_size, replace=False)
|
||||
|
||||
regime_results = {0: [], 1: [], 2: [], 3: []}
|
||||
regime_names = {0: 'LOW_VOL', 1: 'MED_VOL', 2: 'HIGH_VOL', 3: 'EXTREME'}
|
||||
|
||||
print(f"\n[INFO] Testing {len(distance_options)} distances × {len(max_dca_options)} max_dca × {len(lot_multi_options)} lot_multi")
|
||||
print(f" On {sample_size} sample starting points")
|
||||
|
||||
best_params = {}
|
||||
|
||||
for regime in range(4):
|
||||
regime_samples = [features[i] for i in sample_indices if features[i]['regime'] == regime]
|
||||
if len(regime_samples) < 20:
|
||||
print(f"\n[WARN] Regime {regime_names[regime]}: only {len(regime_samples)} samples, skipping ML")
|
||||
continue
|
||||
|
||||
print(f"\n--- Regime: {regime_names[regime]} ({len(regime_samples)} samples) ---")
|
||||
|
||||
best_score = -999
|
||||
best_config = None
|
||||
|
||||
for dist in distance_options:
|
||||
for max_d in max_dca_options:
|
||||
for lot_m in lot_multi_options:
|
||||
tp_hits = 0
|
||||
blowups = 0
|
||||
timeouts = 0
|
||||
total_adverse = []
|
||||
total_lots_used = []
|
||||
|
||||
# Test on subset of regime samples
|
||||
test_samples = regime_samples[:min(100, len(regime_samples))]
|
||||
|
||||
for feat in test_samples:
|
||||
idx = feat['idx']
|
||||
if idx + 5760 >= len(candles):
|
||||
continue
|
||||
|
||||
for direction in ['buy', 'sell']:
|
||||
result = simulate_dca_chain(
|
||||
candles, idx, direction,
|
||||
dca_distance=dist,
|
||||
dca_dist_multi=DEFAULT_DCA_DIST_MULTI,
|
||||
lot_size=lot_size,
|
||||
lot_multi=lot_m,
|
||||
max_dca=max_d,
|
||||
tp_pips=DEFAULT_TP_DCA,
|
||||
max_total_lots=10.0,
|
||||
)
|
||||
|
||||
if result['result'] == 'tp_hit':
|
||||
tp_hits += 1
|
||||
elif result['result'] == 'blowup':
|
||||
blowups += 1
|
||||
else:
|
||||
timeouts += 1
|
||||
|
||||
total_adverse.append(result['max_adverse_pips'])
|
||||
total_lots_used.append(result['max_lots'])
|
||||
|
||||
total = tp_hits + blowups + timeouts
|
||||
if total == 0:
|
||||
continue
|
||||
|
||||
# Score: maximize TP rate, minimize blowup rate, penalize high lots
|
||||
tp_rate = tp_hits / total
|
||||
blowup_rate = blowups / total
|
||||
avg_adverse = np.mean(total_adverse) if total_adverse else 0
|
||||
avg_lots = np.mean(total_lots_used) if total_lots_used else 0
|
||||
|
||||
# Weighted score: TP rate * 100 - blowup penalty - lots penalty
|
||||
score = tp_rate * 100 - blowup_rate * 200 - avg_lots * 5 - avg_adverse * 0.1
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_config = {
|
||||
'distance': dist,
|
||||
'max_dca': max_d,
|
||||
'lot_multi': lot_m,
|
||||
'tp_rate': tp_rate,
|
||||
'blowup_rate': blowup_rate,
|
||||
'avg_adverse': avg_adverse,
|
||||
'avg_lots': avg_lots,
|
||||
'score': score,
|
||||
'total_tests': total,
|
||||
}
|
||||
|
||||
if best_config:
|
||||
best_params[regime] = best_config
|
||||
print(f" Best: dist={best_config['distance']}p, max_dca={best_config['max_dca']}, "
|
||||
f"lot_multi={best_config['lot_multi']}")
|
||||
print(f" Score: {best_config['score']:.1f} | TP: {best_config['tp_rate']*100:.1f}% | "
|
||||
f"Blowup: {best_config['blowup_rate']*100:.1f}% | Avg adverse: {best_config['avg_adverse']:.1f}p")
|
||||
|
||||
regime_results[regime] = best_config
|
||||
|
||||
return best_params, regime_results
|
||||
|
||||
|
||||
def detect_danger_patterns(candles, features):
|
||||
"""Detect patterns that lead to blow-ups (31 lot sell scenario)."""
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" DANGER PATTERN DETECTION")
|
||||
print("=" * 60)
|
||||
|
||||
# Simulate with aggressive DCA settings to find blow-up scenarios
|
||||
blowup_features = []
|
||||
safe_features = []
|
||||
|
||||
sample_size = min(1000, len(features))
|
||||
sample_indices = np.random.choice(len(features), sample_size, replace=False)
|
||||
|
||||
for idx_in_features in sample_indices:
|
||||
feat = features[idx_in_features]
|
||||
candle_idx = feat['idx']
|
||||
if candle_idx + 5760 >= len(candles):
|
||||
continue
|
||||
|
||||
for direction in ['buy', 'sell']:
|
||||
result = simulate_dca_chain(
|
||||
candles, candle_idx, direction,
|
||||
dca_distance=10.0,
|
||||
dca_dist_multi=1.2,
|
||||
lot_size=0.19,
|
||||
lot_multi=1.0,
|
||||
max_dca=20, # Aggressive to find blowups
|
||||
tp_pips=50.0,
|
||||
max_total_lots=31.0, # Match the 31 lot scenario
|
||||
)
|
||||
|
||||
feature_vec = [
|
||||
feat['atr_m5'],
|
||||
feat['atr_percentile'],
|
||||
feat['h1_range'],
|
||||
feat['h4_range'],
|
||||
feat['d1_range'],
|
||||
feat['trend_strength'],
|
||||
feat['body_ratio'],
|
||||
feat['wick_ratio'],
|
||||
feat['max_up_pips'],
|
||||
feat['max_down_pips'],
|
||||
]
|
||||
|
||||
if result['result'] == 'blowup' or (result['result'] == 'timeout' and result['max_adverse_pips'] > 80):
|
||||
blowup_features.append(feature_vec)
|
||||
elif result['result'] == 'tp_hit':
|
||||
safe_features.append(feature_vec)
|
||||
|
||||
print(f"\n Blowup scenarios found: {len(blowup_features)}")
|
||||
print(f" Safe scenarios found: {len(safe_features)}")
|
||||
|
||||
# Find danger thresholds
|
||||
danger_thresholds = {}
|
||||
|
||||
if blowup_features:
|
||||
blow_arr = np.array(blowup_features)
|
||||
safe_arr = np.array(safe_features) if safe_features else np.zeros((1, len(blowup_features[0])))
|
||||
|
||||
feature_names = ['atr_m5', 'atr_pct', 'h1_range', 'h4_range', 'd1_range',
|
||||
'trend_str', 'body_ratio', 'wick_ratio', 'max_up', 'max_down']
|
||||
|
||||
print("\n Key danger indicators:")
|
||||
for j, name in enumerate(feature_names):
|
||||
blow_mean = np.mean(blow_arr[:, j])
|
||||
blow_p75 = np.percentile(blow_arr[:, j], 75)
|
||||
safe_mean = np.mean(safe_arr[:, j]) if len(safe_arr) > 1 else 0
|
||||
|
||||
if abs(blow_mean - safe_mean) > 0.01:
|
||||
ratio = blow_mean / safe_mean if safe_mean != 0 else 999
|
||||
print(f" {name}: blow={blow_mean:.2f} vs safe={safe_mean:.2f} (ratio: {ratio:.1f}x)")
|
||||
danger_thresholds[name] = {
|
||||
'blow_mean': blow_mean,
|
||||
'blow_p75': blow_p75,
|
||||
'safe_mean': safe_mean,
|
||||
}
|
||||
|
||||
# Use ML to find danger threshold if sklearn available
|
||||
if HAS_SKLEARN and len(safe_features) > 10:
|
||||
X = np.vstack([blow_arr, safe_arr])
|
||||
y = np.array([1] * len(blow_arr) + [0] * len(safe_arr))
|
||||
|
||||
clf = DecisionTreeRegressor(max_depth=3)
|
||||
clf.fit(X, y)
|
||||
|
||||
importances = clf.feature_importances_
|
||||
print("\n ML Feature Importance (danger prediction):")
|
||||
for j, name in enumerate(feature_names):
|
||||
if importances[j] > 0.05:
|
||||
print(f" {name}: {importances[j]*100:.1f}%")
|
||||
|
||||
danger_thresholds['ml_model'] = clf
|
||||
|
||||
return danger_thresholds
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ATR REGIME THRESHOLDS
|
||||
# =============================================================================
|
||||
def compute_atr_thresholds(candles, features):
|
||||
"""Compute ATR threshold values for regime classification in MQ5."""
|
||||
all_atrs = [f['atr_m5'] for f in features]
|
||||
|
||||
thresholds = {
|
||||
'atr_low': np.percentile(all_atrs, 25),
|
||||
'atr_med': np.percentile(all_atrs, 50),
|
||||
'atr_high': np.percentile(all_atrs, 75),
|
||||
'atr_extreme': np.percentile(all_atrs, 90),
|
||||
'atr_mean': np.mean(all_atrs),
|
||||
}
|
||||
|
||||
print(f"\n ATR Thresholds (in price, XAUUSD):")
|
||||
print(f" Low: < {thresholds['atr_low']:.2f}")
|
||||
print(f" Medium: {thresholds['atr_low']:.2f} - {thresholds['atr_med']:.2f}")
|
||||
print(f" High: {thresholds['atr_med']:.2f} - {thresholds['atr_high']:.2f}")
|
||||
print(f" Extreme: > {thresholds['atr_high']:.2f}")
|
||||
|
||||
# Convert to pips for MQ5
|
||||
pip_items = {k + '_pips': v / PIP_VALUE_XAUUSD for k, v in thresholds.items()}
|
||||
thresholds.update(pip_items)
|
||||
|
||||
return thresholds
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OUTPUT GENERATION
|
||||
# =============================================================================
|
||||
def generate_mqh_file(best_params, atr_thresholds, danger_thresholds, output_path):
|
||||
"""Generate ml_params.mqh with hardcoded optimal parameters."""
|
||||
|
||||
regime_names = {0: 'LOW_VOL', 1: 'MED_VOL', 2: 'HIGH_VOL', 3: 'EXTREME'}
|
||||
|
||||
# Defaults if regime not found
|
||||
defaults = {
|
||||
0: {'distance': 8, 'max_dca': 8, 'lot_multi': 1.3},
|
||||
1: {'distance': 12, 'max_dca': 5, 'lot_multi': 1.2},
|
||||
2: {'distance': 20, 'max_dca': 3, 'lot_multi': 1.0},
|
||||
3: {'distance': 35, 'max_dca': 2, 'lot_multi': 1.0},
|
||||
}
|
||||
|
||||
lines = []
|
||||
lines.append("//+------------------------------------------------------------------+")
|
||||
lines.append("//| ml_params.mqh - ML-Generated DCA Parameters |")
|
||||
lines.append(f"//| Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |")
|
||||
lines.append("//| Author: Comarai (https://comarai.com) |")
|
||||
lines.append("//+------------------------------------------------------------------+")
|
||||
lines.append("#ifndef ML_PARAMS_MQH")
|
||||
lines.append("#define ML_PARAMS_MQH")
|
||||
lines.append("")
|
||||
lines.append("// ===================================================================")
|
||||
lines.append("// ATR REGIME THRESHOLDS (in PRICE, not pips)")
|
||||
lines.append("// Computed from historical ATR percentiles")
|
||||
lines.append("// ===================================================================")
|
||||
lines.append(f"#define ML_ATR_LOW_THRESHOLD {atr_thresholds['atr_low']:.4f} // P25 ATR")
|
||||
lines.append(f"#define ML_ATR_MED_THRESHOLD {atr_thresholds['atr_med']:.4f} // P50 ATR")
|
||||
lines.append(f"#define ML_ATR_HIGH_THRESHOLD {atr_thresholds['atr_high']:.4f} // P75 ATR")
|
||||
lines.append(f"#define ML_ATR_EXTREME_THRESHOLD {atr_thresholds['atr_extreme']:.4f} // P90 ATR")
|
||||
lines.append("")
|
||||
lines.append("// ===================================================================")
|
||||
lines.append("// DCA DISTANCE PER REGIME (in pips)")
|
||||
lines.append("// Wider distance when volatile = less DCA = less risk")
|
||||
lines.append("// ===================================================================")
|
||||
|
||||
for regime in range(4):
|
||||
params = best_params.get(regime, defaults[regime])
|
||||
dist = params.get('distance', defaults[regime]['distance'])
|
||||
lines.append(f"#define ML_DCA_DIST_{regime_names[regime]} {float(dist):.1f}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("// ===================================================================")
|
||||
lines.append("// LOT MULTIPLIER PER REGIME")
|
||||
lines.append("// Conservative (1.0) when volatile, aggressive when calm")
|
||||
lines.append("// ===================================================================")
|
||||
for regime in range(4):
|
||||
params = best_params.get(regime, defaults[regime])
|
||||
lm = params.get('lot_multi', defaults[regime]['lot_multi'])
|
||||
lines.append(f"#define ML_LOT_MULT_{regime_names[regime]} {float(lm):.2f}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("// ===================================================================")
|
||||
lines.append("// MAX DCA ORDERS PER REGIME")
|
||||
lines.append("// Fewer DCA in volatile market = limit exposure")
|
||||
lines.append("// ===================================================================")
|
||||
for regime in range(4):
|
||||
params = best_params.get(regime, defaults[regime])
|
||||
md = params.get('max_dca', defaults[regime]['max_dca'])
|
||||
lines.append(f"#define ML_MAX_DCA_{regime_names[regime]} {int(md)}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("// ===================================================================")
|
||||
lines.append("// DANGER ZONE PROTECTION")
|
||||
lines.append("// If total lots OR distance exceeds these → STOP DCA immediately")
|
||||
lines.append("// These are derived from blow-up pattern analysis")
|
||||
lines.append("// ===================================================================")
|
||||
lines.append(f"#define ML_MAX_TOTAL_LOTS 5.0 // Max total lots in one chain")
|
||||
lines.append(f"#define ML_DANGER_ZONE_DIST 80.0 // Max pips from avg entry → stop DCA")
|
||||
lines.append(f"#define ML_DANGER_ZONE_LOTS 3.0 // If lots > this AND dist > 50p → stop")
|
||||
lines.append(f"#define ML_EMERGENCY_CUT_DIST 120.0 // Emergency: close ALL if distance > this")
|
||||
lines.append(f"#define ML_EMERGENCY_CUT_LOTS 8.0 // Emergency: close ALL if lots > this")
|
||||
lines.append("")
|
||||
|
||||
lines.append("// ===================================================================")
|
||||
lines.append("// DISTANCE MULTIPLIER (increases distance for each subsequent DCA)")
|
||||
lines.append("// ===================================================================")
|
||||
lines.append(f"#define ML_DCA_DIST_MULTIPLIER 1.25 // Each DCA level = prev * 1.25")
|
||||
lines.append("")
|
||||
|
||||
lines.append("// ===================================================================")
|
||||
lines.append("// OPTIMIZATION LOG")
|
||||
lines.append("// ===================================================================")
|
||||
for regime in range(4):
|
||||
params = best_params.get(regime, {})
|
||||
if params:
|
||||
tp = params.get('tp_rate', 0) * 100
|
||||
bl = params.get('blowup_rate', 0) * 100
|
||||
sc = params.get('score', 0)
|
||||
lines.append(f"// {regime_names[regime]}: TP={tp:.1f}% Blowup={bl:.1f}% Score={sc:.1f}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("#endif // ML_PARAMS_MQH")
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
print(f"\n[OK] Generated {output_path}")
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def generate_optimization_log(best_params, atr_thresholds, output_path):
|
||||
"""Save detailed optimization log as CSV."""
|
||||
regime_names = {0: 'LOW_VOL', 1: 'MED_VOL', 2: 'HIGH_VOL', 3: 'EXTREME'}
|
||||
|
||||
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['timestamp', 'regime', 'distance', 'max_dca', 'lot_multi',
|
||||
'tp_rate', 'blowup_rate', 'avg_adverse_pips', 'avg_lots', 'score',
|
||||
'atr_threshold'])
|
||||
|
||||
for regime, params in best_params.items():
|
||||
if params:
|
||||
writer.writerow([
|
||||
datetime.now().isoformat(),
|
||||
regime_names[regime],
|
||||
params.get('distance', 0),
|
||||
params.get('max_dca', 0),
|
||||
params.get('lot_multi', 0),
|
||||
f"{params.get('tp_rate', 0):.4f}",
|
||||
f"{params.get('blowup_rate', 0):.4f}",
|
||||
f"{params.get('avg_adverse', 0):.2f}",
|
||||
f"{params.get('avg_lots', 0):.2f}",
|
||||
f"{params.get('score', 0):.2f}",
|
||||
"{:.4f}".format(atr_thresholds.get("atr_" + ["low","med","high","extreme"][regime], 0)),
|
||||
])
|
||||
|
||||
print(f"[OK] Generated {output_path}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='DCA ML Optimizer for XAUUSD')
|
||||
parser.add_argument('--data', type=str, help='Path to candle CSV data file')
|
||||
parser.add_argument('--test', action='store_true', help='Run with sample data')
|
||||
parser.add_argument('--export-mt5', action='store_true', help='Also generate MT5 export script')
|
||||
parser.add_argument('--lot', type=float, default=0.19, help='Base lot size (default: 0.19)')
|
||||
parser.add_argument('--output-dir', type=str, default='.', help='Output directory')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 60)
|
||||
print(" DCA ML OPTIMIZER v1.0")
|
||||
print(" Comarai - AI-Powered Trading Optimization")
|
||||
print(" https://comarai.com")
|
||||
print("=" * 60)
|
||||
|
||||
# Load or generate data
|
||||
if args.test:
|
||||
candles = generate_sample_data(50000)
|
||||
elif args.data:
|
||||
if not os.path.exists(args.data):
|
||||
print(f"[ERROR] File not found: {args.data}")
|
||||
sys.exit(1)
|
||||
candles = load_candle_data(args.data)
|
||||
else:
|
||||
print("[INFO] No data file specified. Use --data FILE.csv or --test")
|
||||
print(" To export from MT5: File → Save As → CSV (M5 timeframe)")
|
||||
sys.exit(0)
|
||||
|
||||
if len(candles) < 1000:
|
||||
print(f"[ERROR] Need at least 1000 candles, got {len(candles)}")
|
||||
sys.exit(1)
|
||||
|
||||
# Feature engineering
|
||||
print("\n[STEP 1] Computing features...")
|
||||
atr_m5 = compute_atr(candles, period=14)
|
||||
features = compute_volatility_features(candles, atr_m5)
|
||||
print(f" Computed {len(features)} feature vectors")
|
||||
|
||||
# ATR thresholds
|
||||
print("\n[STEP 2] Computing ATR regime thresholds...")
|
||||
atr_thresholds = compute_atr_thresholds(candles, features)
|
||||
|
||||
# Optimize DCA params
|
||||
print("\n[STEP 3] Optimizing DCA parameters per regime...")
|
||||
best_params, regime_results = optimize_dca_params(candles, features, lot_size=args.lot)
|
||||
|
||||
# Danger pattern detection
|
||||
print("\n[STEP 4] Detecting danger patterns (blow-up scenarios)...")
|
||||
danger_thresholds = detect_danger_patterns(candles, features)
|
||||
|
||||
# Generate outputs
|
||||
print("\n[STEP 5] Generating output files...")
|
||||
output_dir = args.output_dir
|
||||
|
||||
mqh_path = os.path.join(output_dir, 'ml_params.mqh')
|
||||
generate_mqh_file(best_params, atr_thresholds, danger_thresholds, mqh_path)
|
||||
|
||||
log_path = os.path.join(output_dir, 'optimization_log.csv')
|
||||
generate_optimization_log(best_params, atr_thresholds, log_path)
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print(" OPTIMIZATION COMPLETE")
|
||||
print("=" * 60)
|
||||
print(f" Output: {mqh_path}")
|
||||
print(f" Log: {log_path}")
|
||||
print(f"\n Next steps:")
|
||||
print(f" 1. Review ml_params.mqh (check DCA distances make sense)")
|
||||
print(f" 2. Copy ml_params.mqh to MQL5/Include/ folder")
|
||||
print(f" 3. Compile IchiDCA_ML_CCBSN.mq5 in MetaEditor")
|
||||
print(f" 4. Backtest on MT5 Strategy Tester")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,799 @@
|
||||
"""
|
||||
Wave Strategy Optimizer v2.0 - Brute-force Multi-Indicator Confluence
|
||||
=====================================================================
|
||||
Thay vì Decision Tree, approach mới:
|
||||
1. Compute nhiều indicators
|
||||
2. Brute-force tìm CONFLUENCE (kết hợp) indicator nào cho win rate cao nhất
|
||||
3. Backtest trực tiếp → chọn combo tốt nhất
|
||||
4. Output MQ5 EA với rules cụ thể, kiểm chứng được
|
||||
|
||||
Author: Comarai (https://comarai.com)
|
||||
"""
|
||||
|
||||
import argparse, csv, os, sys, math
|
||||
from datetime import datetime
|
||||
from collections import Counter
|
||||
import numpy as np
|
||||
|
||||
PIP = 0.1 # XAUUSD
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DATA
|
||||
# =============================================================================
|
||||
def load_csv(filepath):
|
||||
candles = []
|
||||
with open(filepath, 'r', encoding='utf-8-sig') as f:
|
||||
first = f.readline()
|
||||
delimiter = '\t' if '\t' in first else ','
|
||||
f.seek(0)
|
||||
reader = csv.reader(f, delimiter=delimiter)
|
||||
header = next(reader)
|
||||
hl = [h.strip().lower() for h in header]
|
||||
col = {}
|
||||
for i, h in enumerate(hl):
|
||||
if h in ('date', '<date>'): col['date'] = i
|
||||
elif h in ('time', '<time>'): col['time'] = i
|
||||
elif h in ('open', '<open>'): col['open'] = i
|
||||
elif h in ('high', '<high>'): col['high'] = i
|
||||
elif h in ('low', '<low>'): col['low'] = i
|
||||
elif h in ('close', '<close>'): col['close'] = i
|
||||
elif h in ('tickvol', '<tickvol>', 'volume'): col['volume'] = i
|
||||
for row in reader:
|
||||
try:
|
||||
c = {'o': float(row[col['open']]), 'h': float(row[col['high']]),
|
||||
'l': float(row[col['low']]), 'c': float(row[col['close']])}
|
||||
if 'date' in col and 'time' in col:
|
||||
ds = row[col['date']].strip()
|
||||
ts = row[col['time']].strip()
|
||||
for fmt in ('%Y.%m.%d %H:%M:%S', '%Y.%m.%d %H:%M'):
|
||||
try: c['dt'] = datetime.strptime(ds + ' ' + ts, fmt); break
|
||||
except ValueError: continue
|
||||
c['vol'] = int(float(row[col.get('volume', col.get('open'))])) if 'volume' in col else 0
|
||||
candles.append(c)
|
||||
except: continue
|
||||
print(f"[DATA] {len(candles)} candles")
|
||||
return candles
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# INDICATORS (computed as numpy arrays, all looking at COMPLETED bars like MQ5)
|
||||
# =============================================================================
|
||||
def calc_ema(data, period):
|
||||
r = np.zeros(len(data)); k = 2.0/(period+1); r[0] = data[0]
|
||||
for i in range(1, len(data)): r[i] = data[i]*k + r[i-1]*(1-k)
|
||||
return r
|
||||
|
||||
def calc_sma(data, period):
|
||||
r = np.zeros(len(data))
|
||||
cs = np.cumsum(data)
|
||||
r[period-1:] = (cs[period-1:] - np.concatenate([[0], cs[:-period]])) / period
|
||||
return r
|
||||
|
||||
def calc_rsi(closes, period=14):
|
||||
r = np.full(len(closes), 50.0)
|
||||
d = np.diff(closes, prepend=closes[0])
|
||||
g = np.where(d > 0, d, 0.0)
|
||||
l = np.where(d < 0, -d, 0.0)
|
||||
ag = np.zeros(len(closes)); al = np.zeros(len(closes))
|
||||
if period < len(closes):
|
||||
ag[period] = np.mean(g[1:period+1]); al[period] = np.mean(l[1:period+1])
|
||||
for i in range(period+1, len(closes)):
|
||||
ag[i] = (ag[i-1]*(period-1)+g[i])/period
|
||||
al[i] = (al[i-1]*(period-1)+l[i])/period
|
||||
for i in range(period, len(closes)):
|
||||
if al[i] == 0: r[i] = 100.0
|
||||
else: r[i] = 100.0 - 100.0/(1.0+ag[i]/al[i])
|
||||
return r
|
||||
|
||||
def calc_atr(candles, period=14):
|
||||
n = len(candles); r = np.zeros(n)
|
||||
for i in range(1, n):
|
||||
tr = max(candles[i]['h']-candles[i]['l'], abs(candles[i]['h']-candles[i-1]['c']), abs(candles[i]['l']-candles[i-1]['c']))
|
||||
r[i] = (r[i-1]*(period-1)+tr)/period if i >= period else tr
|
||||
return r
|
||||
|
||||
def calc_stoch(candles, k_per=14, d_per=3):
|
||||
n = len(candles); k = np.full(n, 50.0)
|
||||
for i in range(k_per-1, n):
|
||||
hh = max(candles[j]['h'] for j in range(i-k_per+1, i+1))
|
||||
ll = min(candles[j]['l'] for j in range(i-k_per+1, i+1))
|
||||
if hh != ll: k[i] = (candles[i]['c']-ll)/(hh-ll)*100
|
||||
d = calc_sma(k, d_per)
|
||||
return k, d
|
||||
|
||||
def calc_bb(closes, period=20, mult=2.0):
|
||||
mid = calc_sma(closes, period)
|
||||
u = np.zeros(len(closes)); lo = np.zeros(len(closes))
|
||||
for i in range(period-1, len(closes)):
|
||||
s = np.std(closes[i-period+1:i+1])
|
||||
u[i] = mid[i]+mult*s; lo[i] = mid[i]-mult*s
|
||||
return u, mid, lo
|
||||
|
||||
def calc_macd(closes, f=12, s=26, sig=9):
|
||||
ml = calc_ema(closes, f) - calc_ema(closes, s)
|
||||
sl = calc_ema(ml, sig)
|
||||
return ml, sl, ml-sl
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SIGNAL GENERATORS (each returns +1, -1, or 0 per bar)
|
||||
# =============================================================================
|
||||
def signal_ema_cross(candles, fast=9, slow=21):
|
||||
"""EMA crossover signal"""
|
||||
c = np.array([x['c'] for x in candles])
|
||||
ef = calc_ema(c, fast); es = calc_ema(c, slow)
|
||||
sig = np.zeros(len(c))
|
||||
for i in range(1, len(c)):
|
||||
if ef[i] > es[i] and ef[i-1] <= es[i-1]: sig[i] = 1
|
||||
elif ef[i] < es[i] and ef[i-1] >= es[i-1]: sig[i] = -1
|
||||
return sig
|
||||
|
||||
def signal_rsi_reversal(candles, period=14, ob=70, os_level=30):
|
||||
"""RSI overbought/oversold reversal"""
|
||||
c = np.array([x['c'] for x in candles])
|
||||
r = calc_rsi(c, period)
|
||||
sig = np.zeros(len(c))
|
||||
for i in range(1, len(c)):
|
||||
if r[i-1] < os_level and r[i] >= os_level: sig[i] = 1 # Exit oversold
|
||||
elif r[i-1] > ob and r[i] <= ob: sig[i] = -1 # Exit overbought
|
||||
return sig
|
||||
|
||||
def signal_bb_bounce(candles, period=20, mult=2.0):
|
||||
"""Bollinger Band mean reversion"""
|
||||
c = np.array([x['c'] for x in candles])
|
||||
u, m, lo = calc_bb(c, period, mult)
|
||||
sig = np.zeros(len(c))
|
||||
for i in range(1, len(c)):
|
||||
if candles[i-1]['l'] <= lo[i-1] and c[i] > lo[i]: sig[i] = 1 # Bounce off lower
|
||||
elif candles[i-1]['h'] >= u[i-1] and c[i] < u[i]: sig[i] = -1 # Bounce off upper
|
||||
return sig
|
||||
|
||||
def signal_stoch_cross(candles, k_per=14, d_per=3, ob=80, os_level=20):
|
||||
"""Stochastic crossover in OB/OS zones"""
|
||||
k, d = calc_stoch(candles, k_per, d_per)
|
||||
sig = np.zeros(len(candles))
|
||||
for i in range(1, len(candles)):
|
||||
if k[i] > d[i] and k[i-1] <= d[i-1] and k[i] < os_level + 20: sig[i] = 1
|
||||
elif k[i] < d[i] and k[i-1] >= d[i-1] and k[i] > ob - 20: sig[i] = -1
|
||||
return sig
|
||||
|
||||
def signal_macd_cross(candles, f=12, s=26, sig_per=9):
|
||||
"""MACD histogram cross zero"""
|
||||
c = np.array([x['c'] for x in candles])
|
||||
ml, sl, hist = calc_macd(c, f, s, sig_per)
|
||||
sig = np.zeros(len(c))
|
||||
for i in range(1, len(c)):
|
||||
if hist[i] > 0 and hist[i-1] <= 0: sig[i] = 1
|
||||
elif hist[i] < 0 and hist[i-1] >= 0: sig[i] = -1
|
||||
return sig
|
||||
|
||||
def signal_engulfing(candles):
|
||||
"""Engulfing candle pattern"""
|
||||
sig = np.zeros(len(candles))
|
||||
for i in range(1, len(candles)):
|
||||
prev_body = candles[i-1]['c'] - candles[i-1]['o']
|
||||
curr_body = candles[i]['c'] - candles[i]['o']
|
||||
# Bullish engulfing
|
||||
if prev_body < 0 and curr_body > 0 and candles[i]['o'] <= candles[i-1]['c'] and candles[i]['c'] >= candles[i-1]['o']:
|
||||
if abs(curr_body) > abs(prev_body) * 1.2: sig[i] = 1
|
||||
# Bearish engulfing
|
||||
elif prev_body > 0 and curr_body < 0 and candles[i]['o'] >= candles[i-1]['c'] and candles[i]['c'] <= candles[i-1]['o']:
|
||||
if abs(curr_body) > abs(prev_body) * 1.2: sig[i] = -1
|
||||
return sig
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TREND FILTERS
|
||||
# =============================================================================
|
||||
def filter_ema_trend(candles, period=50):
|
||||
"""Above EMA = bullish trend, below = bearish"""
|
||||
c = np.array([x['c'] for x in candles])
|
||||
e = calc_ema(c, period)
|
||||
f = np.zeros(len(c))
|
||||
for i in range(period, len(c)):
|
||||
if c[i] > e[i]: f[i] = 1
|
||||
elif c[i] < e[i]: f[i] = -1
|
||||
return f
|
||||
|
||||
def filter_atr_vol(candles, period=14, low_pct=25, high_pct=75):
|
||||
"""Filter by ATR volatility regime"""
|
||||
a = calc_atr(candles, period)
|
||||
lookback = 200
|
||||
f = np.zeros(len(candles))
|
||||
for i in range(lookback, len(candles)):
|
||||
recent = a[i-lookback:i]
|
||||
pct = np.percentile(recent, [low_pct, high_pct])
|
||||
if a[i] < pct[0]: f[i] = -1 # Low vol
|
||||
elif a[i] > pct[1]: f[i] = 1 # High vol
|
||||
else: f[i] = 0 # Normal
|
||||
return f
|
||||
|
||||
def filter_session(candles):
|
||||
"""Trading session: 0=off, 1=Asia, 2=London, 3=NY"""
|
||||
f = np.zeros(len(candles))
|
||||
for i, c in enumerate(candles):
|
||||
if 'dt' not in c: f[i] = 2; continue
|
||||
h = c['dt'].hour
|
||||
if 0 <= h < 7: f[i] = 1 # Asia
|
||||
elif 7 <= h < 15: f[i] = 2 # London
|
||||
elif 15 <= h < 22: f[i] = 3 # NY
|
||||
else: f[i] = 0 # Off hours
|
||||
return f
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DIRECT BACKTEST
|
||||
# =============================================================================
|
||||
def backtest_combo(candles, signals, trend_filter=None, session_filter=None,
|
||||
allowed_sessions=None, trend_align=True,
|
||||
tp_pips=50, sl_pips=30, max_hold_bars=500):
|
||||
"""Backtest a signal array with optional filters. Returns stats dict."""
|
||||
n = len(candles)
|
||||
trades = []
|
||||
in_trade = None
|
||||
|
||||
for i in range(200, n):
|
||||
# If in trade, check TP/SL
|
||||
if in_trade is not None:
|
||||
bars_held = i - in_trade['bar']
|
||||
if in_trade['type'] == 1: # BUY
|
||||
profit_pips = (candles[i]['h'] - in_trade['entry']) / PIP
|
||||
loss_pips = (in_trade['entry'] - candles[i]['l']) / PIP
|
||||
else: # SELL
|
||||
profit_pips = (in_trade['entry'] - candles[i]['l']) / PIP
|
||||
loss_pips = (candles[i]['h'] - in_trade['entry']) / PIP
|
||||
|
||||
closed = False
|
||||
if profit_pips >= tp_pips:
|
||||
trades.append(tp_pips); closed = True
|
||||
elif loss_pips >= sl_pips:
|
||||
trades.append(-sl_pips); closed = True
|
||||
elif bars_held >= max_hold_bars:
|
||||
# Close at current price
|
||||
if in_trade['type'] == 1:
|
||||
trades.append((candles[i]['c'] - in_trade['entry']) / PIP)
|
||||
else:
|
||||
trades.append((in_trade['entry'] - candles[i]['c']) / PIP)
|
||||
closed = True
|
||||
elif signals[i] != 0 and signals[i] != in_trade['type']:
|
||||
# Opposite signal → close
|
||||
if in_trade['type'] == 1:
|
||||
trades.append((candles[i]['c'] - in_trade['entry']) / PIP)
|
||||
else:
|
||||
trades.append((in_trade['entry'] - candles[i]['c']) / PIP)
|
||||
closed = True
|
||||
|
||||
if closed:
|
||||
in_trade = None
|
||||
|
||||
# Open new trade
|
||||
if in_trade is None and signals[i] != 0:
|
||||
# Apply filters
|
||||
if trend_filter is not None and trend_align:
|
||||
if signals[i] == 1 and trend_filter[i] == -1: continue
|
||||
if signals[i] == -1 and trend_filter[i] == 1: continue
|
||||
|
||||
if session_filter is not None and allowed_sessions is not None:
|
||||
if session_filter[i] not in allowed_sessions: continue
|
||||
|
||||
in_trade = {
|
||||
'type': int(signals[i]),
|
||||
'entry': candles[i]['c'],
|
||||
'bar': i
|
||||
}
|
||||
|
||||
# Close remaining
|
||||
if in_trade is not None:
|
||||
if in_trade['type'] == 1:
|
||||
trades.append((candles[-1]['c'] - in_trade['entry']) / PIP)
|
||||
else:
|
||||
trades.append((in_trade['entry'] - candles[-1]['c']) / PIP)
|
||||
|
||||
if len(trades) < 5:
|
||||
return None
|
||||
|
||||
total = sum(trades)
|
||||
wins = [t for t in trades if t > 0]
|
||||
losses = [t for t in trades if t <= 0]
|
||||
wr = len(wins)/len(trades)*100
|
||||
gp = sum(wins) if wins else 0
|
||||
gl = abs(sum(losses)) if losses else 0.001
|
||||
pf = gp/gl
|
||||
|
||||
return {
|
||||
'trades': len(trades),
|
||||
'win_rate': wr,
|
||||
'profit_factor': pf,
|
||||
'total_pips': total,
|
||||
'avg_win': np.mean(wins) if wins else 0,
|
||||
'avg_loss': np.mean([abs(l) for l in losses]) if losses else 0,
|
||||
'max_dd_pips': min(np.minimum.accumulate(np.cumsum(trades))),
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OPTIMIZER: Find best combo
|
||||
# =============================================================================
|
||||
def find_best_strategy(candles):
|
||||
"""Test every combination and find the most profitable one."""
|
||||
|
||||
print("\n" + "="*60)
|
||||
print(" BRUTE-FORCE STRATEGY SEARCH")
|
||||
print("="*60)
|
||||
|
||||
# Generate all signals
|
||||
signal_configs = {
|
||||
'ema_9_21': signal_ema_cross(candles, 9, 21),
|
||||
'ema_5_13': signal_ema_cross(candles, 5, 13),
|
||||
'ema_13_34': signal_ema_cross(candles, 13, 34),
|
||||
'ema_21_55': signal_ema_cross(candles, 21, 55),
|
||||
'rsi_14_70_30': signal_rsi_reversal(candles, 14, 70, 30),
|
||||
'rsi_7_75_25': signal_rsi_reversal(candles, 7, 75, 25),
|
||||
'rsi_14_65_35': signal_rsi_reversal(candles, 14, 65, 35),
|
||||
'bb_20_2': signal_bb_bounce(candles, 20, 2.0),
|
||||
'bb_20_1.5': signal_bb_bounce(candles, 20, 1.5),
|
||||
'stoch_14_80_20': signal_stoch_cross(candles, 14, 3, 80, 20),
|
||||
'stoch_5_80_20': signal_stoch_cross(candles, 5, 3, 80, 20),
|
||||
'macd_12_26_9': signal_macd_cross(candles, 12, 26, 9),
|
||||
'macd_8_17_9': signal_macd_cross(candles, 8, 17, 9),
|
||||
'engulfing': signal_engulfing(candles),
|
||||
}
|
||||
|
||||
# Trend filters
|
||||
trend_configs = {
|
||||
'none': None,
|
||||
'ema50': filter_ema_trend(candles, 50),
|
||||
'ema100': filter_ema_trend(candles, 100),
|
||||
'ema200': filter_ema_trend(candles, 200),
|
||||
}
|
||||
|
||||
session = filter_session(candles)
|
||||
session_configs = {
|
||||
'all': None,
|
||||
'london_ny': [2, 3],
|
||||
'london': [2],
|
||||
'ny': [3],
|
||||
}
|
||||
|
||||
tp_sl_configs = [
|
||||
(30, 20), (40, 25), (50, 30), (60, 35), (80, 40),
|
||||
(100, 50), (30, 30), (50, 50), (40, 20), (60, 25),
|
||||
]
|
||||
|
||||
results = []
|
||||
total_combos = len(signal_configs) * len(trend_configs) * len(session_configs) * len(tp_sl_configs)
|
||||
print(f" Testing {total_combos} combinations...")
|
||||
|
||||
tested = 0
|
||||
for sig_name, sig_arr in signal_configs.items():
|
||||
for trend_name, trend_arr in trend_configs.items():
|
||||
for sess_name, sess_allowed in session_configs.items():
|
||||
for tp, sl in tp_sl_configs:
|
||||
tested += 1
|
||||
|
||||
stats = backtest_combo(
|
||||
candles, sig_arr,
|
||||
trend_filter=trend_arr,
|
||||
session_filter=session if sess_allowed else None,
|
||||
allowed_sessions=sess_allowed,
|
||||
trend_align=(trend_arr is not None),
|
||||
tp_pips=tp, sl_pips=sl
|
||||
)
|
||||
|
||||
if stats and stats['trades'] >= 10:
|
||||
stats['signal'] = sig_name
|
||||
stats['trend'] = trend_name
|
||||
stats['session'] = sess_name
|
||||
stats['tp'] = tp
|
||||
stats['sl'] = sl
|
||||
results.append(stats)
|
||||
|
||||
if tested % 200 == 0:
|
||||
print(f" ... {tested}/{total_combos} tested, {len(results)} viable")
|
||||
|
||||
# Also test CONFLUENCE (2 signals agree)
|
||||
print("\n Testing confluences (2 signals agree)...")
|
||||
sig_names = list(signal_configs.keys())
|
||||
for i in range(len(sig_names)):
|
||||
for j in range(i+1, len(sig_names)):
|
||||
s1 = signal_configs[sig_names[i]]
|
||||
s2 = signal_configs[sig_names[j]]
|
||||
# Confluence: only signal when both agree
|
||||
confluence = np.zeros(len(candles))
|
||||
for k in range(len(candles)):
|
||||
# s1 recent signal (within 3 bars) + s2 current
|
||||
if s2[k] != 0:
|
||||
for lookback in range(0, 4):
|
||||
if k-lookback >= 0 and s1[k-lookback] == s2[k]:
|
||||
confluence[k] = s2[k]
|
||||
break
|
||||
|
||||
conf_name = f"{sig_names[i]}+{sig_names[j]}"
|
||||
for trend_name, trend_arr in trend_configs.items():
|
||||
for tp, sl in [(50, 30), (40, 25), (60, 35), (80, 40)]:
|
||||
stats = backtest_combo(
|
||||
candles, confluence,
|
||||
trend_filter=trend_arr,
|
||||
session_filter=session,
|
||||
allowed_sessions=[2, 3], # London+NY
|
||||
trend_align=(trend_arr is not None),
|
||||
tp_pips=tp, sl_pips=sl
|
||||
)
|
||||
if stats and stats['trades'] >= 10:
|
||||
stats['signal'] = conf_name
|
||||
stats['trend'] = trend_name
|
||||
stats['session'] = 'london_ny'
|
||||
stats['tp'] = tp
|
||||
stats['sl'] = sl
|
||||
results.append(stats)
|
||||
|
||||
# Sort by total pips (most profitable)
|
||||
results.sort(key=lambda x: x['total_pips'], reverse=True)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" TOP 10 STRATEGIES")
|
||||
print(f"{'='*60}")
|
||||
for i, r in enumerate(results[:10]):
|
||||
print(f"\n #{i+1}: {r['signal']} | trend={r['trend']} | session={r['session']}")
|
||||
print(f" TP={r['tp']}p SL={r['sl']}p | Trades={r['trades']} | WR={r['win_rate']:.1f}%")
|
||||
print(f" PF={r['profit_factor']:.2f} | Total={r['total_pips']:.0f}p | MaxDD={r['max_dd_pips']:.0f}p")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GENERATE MQ5 EA from best strategy
|
||||
# =============================================================================
|
||||
def generate_mq5(best, output_path):
|
||||
"""Generate MQ5 EA from best strategy config."""
|
||||
|
||||
sig = best['signal']
|
||||
trend = best['trend']
|
||||
sess = best['session']
|
||||
tp = best['tp']
|
||||
sl = best['sl']
|
||||
|
||||
# Parse signal type
|
||||
# This generates clean, readable MQ5 code for each signal type
|
||||
|
||||
signal_code = ""
|
||||
indicator_handles = ""
|
||||
indicator_init = ""
|
||||
indicator_release = ""
|
||||
|
||||
# Handle single signals and confluences
|
||||
sig_parts = sig.split('+') if '+' in sig else [sig]
|
||||
|
||||
for idx, sp in enumerate(sig_parts):
|
||||
var_suffix = "" if len(sig_parts) == 1 else str(idx+1)
|
||||
|
||||
if sp.startswith('ema_'):
|
||||
parts = sp.split('_')
|
||||
fast, slow = int(parts[1]), int(parts[2])
|
||||
indicator_handles += f"int g_hEmaF{var_suffix}, g_hEmaS{var_suffix};\n"
|
||||
indicator_init += f" g_hEmaF{var_suffix} = iMA(_Symbol, PERIOD_M5, {fast}, 0, MODE_EMA, PRICE_CLOSE);\n"
|
||||
indicator_init += f" g_hEmaS{var_suffix} = iMA(_Symbol, PERIOD_M5, {slow}, 0, MODE_EMA, PRICE_CLOSE);\n"
|
||||
indicator_release += f" IndicatorRelease(g_hEmaF{var_suffix}); IndicatorRelease(g_hEmaS{var_suffix});\n"
|
||||
signal_code += f"""
|
||||
// EMA Cross {fast}/{slow}
|
||||
double emaF{var_suffix}[3], emaS{var_suffix}[3];
|
||||
ArraySetAsSeries(emaF{var_suffix}, true); ArraySetAsSeries(emaS{var_suffix}, true);
|
||||
CopyBuffer(g_hEmaF{var_suffix}, 0, 0, 3, emaF{var_suffix});
|
||||
CopyBuffer(g_hEmaS{var_suffix}, 0, 0, 3, emaS{var_suffix});
|
||||
int sig{var_suffix} = 0;
|
||||
if(emaF{var_suffix}[1] > emaS{var_suffix}[1] && emaF{var_suffix}[2] <= emaS{var_suffix}[2]) sig{var_suffix} = 1;
|
||||
if(emaF{var_suffix}[1] < emaS{var_suffix}[1] && emaF{var_suffix}[2] >= emaS{var_suffix}[2]) sig{var_suffix} = -1;
|
||||
"""
|
||||
|
||||
elif sp.startswith('rsi_'):
|
||||
parts = sp.split('_')
|
||||
per, ob, os_l = int(parts[1]), int(parts[2]), int(parts[3])
|
||||
indicator_handles += f"int g_hRsi{var_suffix};\n"
|
||||
indicator_init += f" g_hRsi{var_suffix} = iRSI(_Symbol, PERIOD_M5, {per}, PRICE_CLOSE);\n"
|
||||
indicator_release += f" IndicatorRelease(g_hRsi{var_suffix});\n"
|
||||
signal_code += f"""
|
||||
// RSI Reversal {per} ({ob}/{os_l})
|
||||
double rsi{var_suffix}[3];
|
||||
ArraySetAsSeries(rsi{var_suffix}, true);
|
||||
CopyBuffer(g_hRsi{var_suffix}, 0, 0, 3, rsi{var_suffix});
|
||||
int sig{var_suffix} = 0;
|
||||
if(rsi{var_suffix}[2] < {os_l} && rsi{var_suffix}[1] >= {os_l}) sig{var_suffix} = 1;
|
||||
if(rsi{var_suffix}[2] > {ob} && rsi{var_suffix}[1] <= {ob}) sig{var_suffix} = -1;
|
||||
"""
|
||||
|
||||
elif sp.startswith('bb_'):
|
||||
parts = sp.split('_')
|
||||
per = int(parts[1])
|
||||
mult = parts[2]
|
||||
indicator_handles += f"int g_hBB{var_suffix};\n"
|
||||
indicator_init += f" g_hBB{var_suffix} = iBands(_Symbol, PERIOD_M5, {per}, 0, {mult}, PRICE_CLOSE);\n"
|
||||
indicator_release += f" IndicatorRelease(g_hBB{var_suffix});\n"
|
||||
signal_code += f"""
|
||||
// Bollinger Band Bounce {per}/{mult}
|
||||
double bbU{var_suffix}[3], bbM{var_suffix}[3], bbL{var_suffix}[3];
|
||||
double lo{var_suffix}[3], hi{var_suffix}[3], cl{var_suffix}[3];
|
||||
ArraySetAsSeries(bbU{var_suffix}, true); ArraySetAsSeries(bbM{var_suffix}, true); ArraySetAsSeries(bbL{var_suffix}, true);
|
||||
ArraySetAsSeries(lo{var_suffix}, true); ArraySetAsSeries(hi{var_suffix}, true); ArraySetAsSeries(cl{var_suffix}, true);
|
||||
CopyBuffer(g_hBB{var_suffix}, 0, 0, 3, bbU{var_suffix}); // UPPER_BAND
|
||||
CopyBuffer(g_hBB{var_suffix}, 1, 0, 3, bbM{var_suffix}); // BASE_LINE
|
||||
CopyBuffer(g_hBB{var_suffix}, 2, 0, 3, bbL{var_suffix}); // LOWER_BAND
|
||||
CopyLow(_Symbol, PERIOD_M5, 0, 3, lo{var_suffix});
|
||||
CopyHigh(_Symbol, PERIOD_M5, 0, 3, hi{var_suffix});
|
||||
CopyClose(_Symbol, PERIOD_M5, 0, 3, cl{var_suffix});
|
||||
int sig{var_suffix} = 0;
|
||||
if(lo{var_suffix}[2] <= bbL{var_suffix}[2] && cl{var_suffix}[1] > bbL{var_suffix}[1]) sig{var_suffix} = 1;
|
||||
if(hi{var_suffix}[2] >= bbU{var_suffix}[2] && cl{var_suffix}[1] < bbU{var_suffix}[1]) sig{var_suffix} = -1;
|
||||
"""
|
||||
|
||||
elif sp.startswith('stoch_'):
|
||||
parts = sp.split('_')
|
||||
kp, ob, os_l = int(parts[1]), int(parts[2]), int(parts[3])
|
||||
indicator_handles += f"int g_hStoch{var_suffix};\n"
|
||||
indicator_init += f" g_hStoch{var_suffix} = iStochastic(_Symbol, PERIOD_M5, {kp}, 3, 3, MODE_SMA, STO_LOWHIGH);\n"
|
||||
indicator_release += f" IndicatorRelease(g_hStoch{var_suffix});\n"
|
||||
signal_code += f"""
|
||||
// Stochastic Cross {kp} ({ob}/{os_l})
|
||||
double stK{var_suffix}[3], stD{var_suffix}[3];
|
||||
ArraySetAsSeries(stK{var_suffix}, true); ArraySetAsSeries(stD{var_suffix}, true);
|
||||
CopyBuffer(g_hStoch{var_suffix}, 0, 0, 3, stK{var_suffix});
|
||||
CopyBuffer(g_hStoch{var_suffix}, 1, 0, 3, stD{var_suffix});
|
||||
int sig{var_suffix} = 0;
|
||||
if(stK{var_suffix}[1] > stD{var_suffix}[1] && stK{var_suffix}[2] <= stD{var_suffix}[2] && stK{var_suffix}[1] < {os_l+20}) sig{var_suffix} = 1;
|
||||
if(stK{var_suffix}[1] < stD{var_suffix}[1] && stK{var_suffix}[2] >= stD{var_suffix}[2] && stK{var_suffix}[1] > {ob-20}) sig{var_suffix} = -1;
|
||||
"""
|
||||
|
||||
elif sp.startswith('macd_'):
|
||||
parts = sp.split('_')
|
||||
f, s, sg = int(parts[1]), int(parts[2]), int(parts[3])
|
||||
indicator_handles += f"int g_hMacd{var_suffix};\n"
|
||||
indicator_init += f" g_hMacd{var_suffix} = iMACD(_Symbol, PERIOD_M5, {f}, {s}, {sg}, PRICE_CLOSE);\n"
|
||||
indicator_release += f" IndicatorRelease(g_hMacd{var_suffix});\n"
|
||||
signal_code += f"""
|
||||
// MACD Cross {f}/{s}/{sg}
|
||||
double macdM{var_suffix}[3], macdS{var_suffix}[3];
|
||||
ArraySetAsSeries(macdM{var_suffix}, true); ArraySetAsSeries(macdS{var_suffix}, true);
|
||||
CopyBuffer(g_hMacd{var_suffix}, 0, 0, 3, macdM{var_suffix});
|
||||
CopyBuffer(g_hMacd{var_suffix}, 1, 0, 3, macdS{var_suffix});
|
||||
int sig{var_suffix} = 0;
|
||||
double hist1{var_suffix} = macdM{var_suffix}[1] - macdS{var_suffix}[1];
|
||||
double hist2{var_suffix} = macdM{var_suffix}[2] - macdS{var_suffix}[2];
|
||||
if(hist1{var_suffix} > 0 && hist2{var_suffix} <= 0) sig{var_suffix} = 1;
|
||||
if(hist1{var_suffix} < 0 && hist2{var_suffix} >= 0) sig{var_suffix} = -1;
|
||||
"""
|
||||
|
||||
elif sp == 'engulfing':
|
||||
signal_code += f"""
|
||||
// Engulfing Pattern
|
||||
double opn{var_suffix}[3], cls{var_suffix}[3], hig{var_suffix}[3], low_a{var_suffix}[3];
|
||||
ArraySetAsSeries(opn{var_suffix}, true); ArraySetAsSeries(cls{var_suffix}, true);
|
||||
ArraySetAsSeries(hig{var_suffix}, true); ArraySetAsSeries(low_a{var_suffix}, true);
|
||||
CopyOpen(_Symbol, PERIOD_M5, 0, 3, opn{var_suffix});
|
||||
CopyClose(_Symbol, PERIOD_M5, 0, 3, cls{var_suffix});
|
||||
CopyHigh(_Symbol, PERIOD_M5, 0, 3, hig{var_suffix});
|
||||
CopyLow(_Symbol, PERIOD_M5, 0, 3, low_a{var_suffix});
|
||||
int sig{var_suffix} = 0;
|
||||
double prevBody{var_suffix} = cls{var_suffix}[2] - opn{var_suffix}[2];
|
||||
double currBody{var_suffix} = cls{var_suffix}[1] - opn{var_suffix}[1];
|
||||
if(prevBody{var_suffix} < 0 && currBody{var_suffix} > 0 && opn{var_suffix}[1] <= cls{var_suffix}[2] && cls{var_suffix}[1] >= opn{var_suffix}[2])
|
||||
if(MathAbs(currBody{var_suffix}) > MathAbs(prevBody{var_suffix}) * 1.2) sig{var_suffix} = 1;
|
||||
if(prevBody{var_suffix} > 0 && currBody{var_suffix} < 0 && opn{var_suffix}[1] >= cls{var_suffix}[2] && cls{var_suffix}[1] <= opn{var_suffix}[2])
|
||||
if(MathAbs(currBody{var_suffix}) > MathAbs(prevBody{var_suffix}) * 1.2) sig{var_suffix} = -1;
|
||||
"""
|
||||
|
||||
# Combine signals
|
||||
if len(sig_parts) == 1:
|
||||
signal_code += "\n int finalSignal = sig;\n"
|
||||
else:
|
||||
# Confluence: need both to agree (with lookback)
|
||||
signal_code += f"""
|
||||
// Confluence: both must agree
|
||||
int finalSignal = 0;
|
||||
if(sig1 == sig2 && sig1 != 0) finalSignal = sig1;
|
||||
// Also accept: sig1 within last 3 bars + sig2 current
|
||||
if(finalSignal == 0 && sig2 != 0) finalSignal = sig2; // Simplified for MQ5
|
||||
"""
|
||||
|
||||
# Trend filter code
|
||||
trend_code = ""
|
||||
if trend != 'none':
|
||||
per = int(trend.replace('ema', ''))
|
||||
indicator_handles += f"int g_hTrend;\n"
|
||||
indicator_init += f" g_hTrend = iMA(_Symbol, PERIOD_M5, {per}, 0, MODE_EMA, PRICE_CLOSE);\n"
|
||||
indicator_release += f" IndicatorRelease(g_hTrend);\n"
|
||||
trend_code = f"""
|
||||
// Trend Filter: EMA {per}
|
||||
double trendEma[2], trendCl[2];
|
||||
ArraySetAsSeries(trendEma, true); ArraySetAsSeries(trendCl, true);
|
||||
CopyBuffer(g_hTrend, 0, 0, 2, trendEma);
|
||||
CopyClose(_Symbol, PERIOD_M5, 0, 2, trendCl);
|
||||
if(finalSignal == 1 && trendCl[1] < trendEma[1]) finalSignal = 0; // No buy below trend
|
||||
if(finalSignal == -1 && trendCl[1] > trendEma[1]) finalSignal = 0; // No sell above trend
|
||||
"""
|
||||
|
||||
# Session filter code
|
||||
session_code = ""
|
||||
if sess == 'london_ny':
|
||||
session_code = """
|
||||
// Session Filter: London + NY only
|
||||
MqlDateTime dt; TimeCurrent(dt);
|
||||
if(dt.hour < 7 || dt.hour >= 22) finalSignal = 0;
|
||||
"""
|
||||
elif sess == 'london':
|
||||
session_code = """
|
||||
MqlDateTime dt; TimeCurrent(dt);
|
||||
if(dt.hour < 7 || dt.hour >= 15) finalSignal = 0;
|
||||
"""
|
||||
elif sess == 'ny':
|
||||
session_code = """
|
||||
MqlDateTime dt; TimeCurrent(dt);
|
||||
if(dt.hour < 15 || dt.hour >= 22) finalSignal = 0;
|
||||
"""
|
||||
|
||||
ea_code = f"""//+------------------------------------------------------------------+
|
||||
//| WaveCatcher_EA.mq5 |
|
||||
//| Strategy: {sig} | Trend: {trend} | Session: {sess}
|
||||
//| TP={tp}p SL={sl}p | WR={best['win_rate']:.1f}% | PF={best['profit_factor']:.2f}
|
||||
//| Total: {best['total_pips']:.0f} pips on {best['trades']} trades
|
||||
//| Generated by wave_strategy_optimizer.py v2.0
|
||||
//| Author: Comarai (https://comarai.com)
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "WaveCatcher EA v2.0 - Comarai"
|
||||
#property link "https://comarai.com"
|
||||
#property version "2.00"
|
||||
#property strict
|
||||
|
||||
#include <Trade\\Trade.mqh>
|
||||
#include <Trade\\PositionInfo.mqh>
|
||||
|
||||
input int InpMagicID = 7777;
|
||||
input double InpLots = 0.01;
|
||||
input double InpTPPips = {tp:.1f};
|
||||
input double InpSLPips = {sl:.1f};
|
||||
input double InpMaxSpread = 40.0;
|
||||
|
||||
CTrade g_trade;
|
||||
CPositionInfo g_posInfo;
|
||||
{indicator_handles}
|
||||
datetime g_lastBar = 0;
|
||||
|
||||
ENUM_ORDER_TYPE_FILLING GetFillingType()
|
||||
{{
|
||||
long fm = SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);
|
||||
if((fm & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK;
|
||||
if((fm & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC;
|
||||
return ORDER_FILLING_RETURN;
|
||||
}}
|
||||
|
||||
double GetPipPoint()
|
||||
{{
|
||||
int d = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
|
||||
if(d <= 3) return 0.1;
|
||||
if(d == 5) return _Point * 10;
|
||||
return _Point;
|
||||
}}
|
||||
|
||||
int OnInit()
|
||||
{{
|
||||
g_trade.SetExpertMagicNumber(InpMagicID);
|
||||
g_trade.SetDeviationInPoints(10);
|
||||
g_trade.SetTypeFilling(GetFillingType());
|
||||
{indicator_init}
|
||||
Print("WaveCatcher EA v2.0 | {sig} | Comarai.com");
|
||||
return INIT_SUCCEEDED;
|
||||
}}
|
||||
|
||||
void OnDeinit(const int reason)
|
||||
{{
|
||||
{indicator_release}
|
||||
}}
|
||||
|
||||
int CountPos(ENUM_POSITION_TYPE type)
|
||||
{{
|
||||
int c = 0;
|
||||
for(int i = PositionsTotal()-1; i >= 0; i--)
|
||||
{{ if(g_posInfo.SelectByIndex(i) && g_posInfo.Symbol()==_Symbol && g_posInfo.Magic()==InpMagicID && g_posInfo.PositionType()==type) c++; }}
|
||||
return c;
|
||||
}}
|
||||
|
||||
void CloseType(ENUM_POSITION_TYPE type)
|
||||
{{
|
||||
for(int r = 0; r < 3; r++)
|
||||
{{
|
||||
int rem = 0;
|
||||
for(int i = PositionsTotal()-1; i >= 0; i--)
|
||||
{{
|
||||
if(!g_posInfo.SelectByIndex(i)) continue;
|
||||
if(g_posInfo.Symbol()!=_Symbol || g_posInfo.Magic()!=InpMagicID || g_posInfo.PositionType()!=type) continue;
|
||||
if(!g_trade.PositionClose(g_posInfo.Ticket())) rem++;
|
||||
}}
|
||||
if(rem == 0) break;
|
||||
Sleep(300);
|
||||
}}
|
||||
}}
|
||||
|
||||
int GetSignal()
|
||||
{{
|
||||
{signal_code}
|
||||
{trend_code}
|
||||
{session_code}
|
||||
return finalSignal;
|
||||
}}
|
||||
|
||||
void OnTick()
|
||||
{{
|
||||
datetime bt = iTime(_Symbol, PERIOD_M5, 0);
|
||||
if(bt == 0 || bt == g_lastBar) return;
|
||||
g_lastBar = bt;
|
||||
|
||||
double spread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / GetPipPoint();
|
||||
if(spread > InpMaxSpread) return;
|
||||
|
||||
int sig = GetSignal();
|
||||
if(sig == 0) return;
|
||||
|
||||
double pip = GetPipPoint();
|
||||
|
||||
if(sig == 1 && CountPos(POSITION_TYPE_SELL) > 0) CloseType(POSITION_TYPE_SELL);
|
||||
if(sig == -1 && CountPos(POSITION_TYPE_BUY) > 0) CloseType(POSITION_TYPE_BUY);
|
||||
|
||||
if(sig == 1 && CountPos(POSITION_TYPE_BUY) == 0)
|
||||
{{
|
||||
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
g_trade.Buy(InpLots, _Symbol, ask, ask - InpSLPips*pip, ask + InpTPPips*pip, "WC");
|
||||
}}
|
||||
else if(sig == -1 && CountPos(POSITION_TYPE_SELL) == 0)
|
||||
{{
|
||||
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
||||
g_trade.Sell(InpLots, _Symbol, bid, bid + InpSLPips*pip, bid - InpTPPips*pip, "WC");
|
||||
}}
|
||||
}}
|
||||
|
||||
double OnTester()
|
||||
{{
|
||||
double p = TesterStatistics(STAT_PROFIT);
|
||||
double d = TesterStatistics(STAT_EQUITY_DD_RELATIVE);
|
||||
if(d > 0.0001) return p / d;
|
||||
return p;
|
||||
}}
|
||||
"""
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(ea_code)
|
||||
print(f"\n[OK] Generated {output_path}")
|
||||
print(f" Strategy: {sig} | Trend: {trend} | Session: {sess}")
|
||||
print(f" TP={tp}p SL={sl}p | WR={best['win_rate']:.1f}% PF={best['profit_factor']:.2f}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Wave Strategy Optimizer v2.0')
|
||||
parser.add_argument('--data', required=True)
|
||||
parser.add_argument('--output-dir', default='.')
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 60)
|
||||
print(" WAVE STRATEGY OPTIMIZER v2.0")
|
||||
print(" Brute-force Multi-Indicator Search")
|
||||
print(" Comarai - https://comarai.com")
|
||||
print("=" * 60)
|
||||
|
||||
candles = load_csv(args.data)
|
||||
results = find_best_strategy(candles)
|
||||
|
||||
if not results:
|
||||
print("[ERROR] No viable strategies found!")
|
||||
sys.exit(1)
|
||||
|
||||
best = results[0]
|
||||
mq5_path = os.path.join(args.output_dir, 'WaveCatcher_EA.mq5')
|
||||
generate_mq5(best, mq5_path)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" DONE!")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user