878 lines
36 KiB
Python
878 lines
36 KiB
Python
|
|
"""
|
|||
|
|
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()
|