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:
Phạm Phú Nguyễn Hưng
2026-03-17 04:16:24 +07:00
commit 4cb86ecd5f
5 changed files with 3693 additions and 0 deletions
+18
View File
@@ -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
+162
View File
@@ -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
![MetaTrader 5](https://img.shields.io/badge/MetaTrader-5-blue)
![MQL5](https://img.shields.io/badge/Language-MQL5-orange)
![Python](https://img.shields.io/badge/Optimizer-Python-yellow)
![License](https://img.shields.io/badge/License-MIT-green)
## 🎯 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>
+877
View File
@@ -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()
+799
View File
@@ -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()