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 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()