4cb86ecd5f
- 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
800 lines
32 KiB
Python
800 lines
32 KiB
Python
"""
|
|
Wave Strategy Optimizer v2.0 - Brute-force Multi-Indicator Confluence
|
|
=====================================================================
|
|
Thay vì Decision Tree, approach mới:
|
|
1. Compute nhiều indicators
|
|
2. Brute-force tìm CONFLUENCE (kết hợp) indicator nào cho win rate cao nhất
|
|
3. Backtest trực tiếp → chọn combo tốt nhất
|
|
4. Output MQ5 EA với rules cụ thể, kiểm chứng được
|
|
|
|
Author: Comarai (https://comarai.com)
|
|
"""
|
|
|
|
import argparse, csv, os, sys, math
|
|
from datetime import datetime
|
|
from collections import Counter
|
|
import numpy as np
|
|
|
|
PIP = 0.1 # XAUUSD
|
|
|
|
|
|
# =============================================================================
|
|
# DATA
|
|
# =============================================================================
|
|
def load_csv(filepath):
|
|
candles = []
|
|
with open(filepath, 'r', encoding='utf-8-sig') as f:
|
|
first = f.readline()
|
|
delimiter = '\t' if '\t' in first else ','
|
|
f.seek(0)
|
|
reader = csv.reader(f, delimiter=delimiter)
|
|
header = next(reader)
|
|
hl = [h.strip().lower() for h in header]
|
|
col = {}
|
|
for i, h in enumerate(hl):
|
|
if h in ('date', '<date>'): col['date'] = i
|
|
elif h in ('time', '<time>'): col['time'] = i
|
|
elif h in ('open', '<open>'): col['open'] = i
|
|
elif h in ('high', '<high>'): col['high'] = i
|
|
elif h in ('low', '<low>'): col['low'] = i
|
|
elif h in ('close', '<close>'): col['close'] = i
|
|
elif h in ('tickvol', '<tickvol>', 'volume'): col['volume'] = i
|
|
for row in reader:
|
|
try:
|
|
c = {'o': float(row[col['open']]), 'h': float(row[col['high']]),
|
|
'l': float(row[col['low']]), 'c': float(row[col['close']])}
|
|
if 'date' in col and 'time' in col:
|
|
ds = row[col['date']].strip()
|
|
ts = row[col['time']].strip()
|
|
for fmt in ('%Y.%m.%d %H:%M:%S', '%Y.%m.%d %H:%M'):
|
|
try: c['dt'] = datetime.strptime(ds + ' ' + ts, fmt); break
|
|
except ValueError: continue
|
|
c['vol'] = int(float(row[col.get('volume', col.get('open'))])) if 'volume' in col else 0
|
|
candles.append(c)
|
|
except: continue
|
|
print(f"[DATA] {len(candles)} candles")
|
|
return candles
|
|
|
|
|
|
# =============================================================================
|
|
# INDICATORS (computed as numpy arrays, all looking at COMPLETED bars like MQ5)
|
|
# =============================================================================
|
|
def calc_ema(data, period):
|
|
r = np.zeros(len(data)); k = 2.0/(period+1); r[0] = data[0]
|
|
for i in range(1, len(data)): r[i] = data[i]*k + r[i-1]*(1-k)
|
|
return r
|
|
|
|
def calc_sma(data, period):
|
|
r = np.zeros(len(data))
|
|
cs = np.cumsum(data)
|
|
r[period-1:] = (cs[period-1:] - np.concatenate([[0], cs[:-period]])) / period
|
|
return r
|
|
|
|
def calc_rsi(closes, period=14):
|
|
r = np.full(len(closes), 50.0)
|
|
d = np.diff(closes, prepend=closes[0])
|
|
g = np.where(d > 0, d, 0.0)
|
|
l = np.where(d < 0, -d, 0.0)
|
|
ag = np.zeros(len(closes)); al = np.zeros(len(closes))
|
|
if period < len(closes):
|
|
ag[period] = np.mean(g[1:period+1]); al[period] = np.mean(l[1:period+1])
|
|
for i in range(period+1, len(closes)):
|
|
ag[i] = (ag[i-1]*(period-1)+g[i])/period
|
|
al[i] = (al[i-1]*(period-1)+l[i])/period
|
|
for i in range(period, len(closes)):
|
|
if al[i] == 0: r[i] = 100.0
|
|
else: r[i] = 100.0 - 100.0/(1.0+ag[i]/al[i])
|
|
return r
|
|
|
|
def calc_atr(candles, period=14):
|
|
n = len(candles); r = np.zeros(n)
|
|
for i in range(1, n):
|
|
tr = max(candles[i]['h']-candles[i]['l'], abs(candles[i]['h']-candles[i-1]['c']), abs(candles[i]['l']-candles[i-1]['c']))
|
|
r[i] = (r[i-1]*(period-1)+tr)/period if i >= period else tr
|
|
return r
|
|
|
|
def calc_stoch(candles, k_per=14, d_per=3):
|
|
n = len(candles); k = np.full(n, 50.0)
|
|
for i in range(k_per-1, n):
|
|
hh = max(candles[j]['h'] for j in range(i-k_per+1, i+1))
|
|
ll = min(candles[j]['l'] for j in range(i-k_per+1, i+1))
|
|
if hh != ll: k[i] = (candles[i]['c']-ll)/(hh-ll)*100
|
|
d = calc_sma(k, d_per)
|
|
return k, d
|
|
|
|
def calc_bb(closes, period=20, mult=2.0):
|
|
mid = calc_sma(closes, period)
|
|
u = np.zeros(len(closes)); lo = np.zeros(len(closes))
|
|
for i in range(period-1, len(closes)):
|
|
s = np.std(closes[i-period+1:i+1])
|
|
u[i] = mid[i]+mult*s; lo[i] = mid[i]-mult*s
|
|
return u, mid, lo
|
|
|
|
def calc_macd(closes, f=12, s=26, sig=9):
|
|
ml = calc_ema(closes, f) - calc_ema(closes, s)
|
|
sl = calc_ema(ml, sig)
|
|
return ml, sl, ml-sl
|
|
|
|
|
|
# =============================================================================
|
|
# SIGNAL GENERATORS (each returns +1, -1, or 0 per bar)
|
|
# =============================================================================
|
|
def signal_ema_cross(candles, fast=9, slow=21):
|
|
"""EMA crossover signal"""
|
|
c = np.array([x['c'] for x in candles])
|
|
ef = calc_ema(c, fast); es = calc_ema(c, slow)
|
|
sig = np.zeros(len(c))
|
|
for i in range(1, len(c)):
|
|
if ef[i] > es[i] and ef[i-1] <= es[i-1]: sig[i] = 1
|
|
elif ef[i] < es[i] and ef[i-1] >= es[i-1]: sig[i] = -1
|
|
return sig
|
|
|
|
def signal_rsi_reversal(candles, period=14, ob=70, os_level=30):
|
|
"""RSI overbought/oversold reversal"""
|
|
c = np.array([x['c'] for x in candles])
|
|
r = calc_rsi(c, period)
|
|
sig = np.zeros(len(c))
|
|
for i in range(1, len(c)):
|
|
if r[i-1] < os_level and r[i] >= os_level: sig[i] = 1 # Exit oversold
|
|
elif r[i-1] > ob and r[i] <= ob: sig[i] = -1 # Exit overbought
|
|
return sig
|
|
|
|
def signal_bb_bounce(candles, period=20, mult=2.0):
|
|
"""Bollinger Band mean reversion"""
|
|
c = np.array([x['c'] for x in candles])
|
|
u, m, lo = calc_bb(c, period, mult)
|
|
sig = np.zeros(len(c))
|
|
for i in range(1, len(c)):
|
|
if candles[i-1]['l'] <= lo[i-1] and c[i] > lo[i]: sig[i] = 1 # Bounce off lower
|
|
elif candles[i-1]['h'] >= u[i-1] and c[i] < u[i]: sig[i] = -1 # Bounce off upper
|
|
return sig
|
|
|
|
def signal_stoch_cross(candles, k_per=14, d_per=3, ob=80, os_level=20):
|
|
"""Stochastic crossover in OB/OS zones"""
|
|
k, d = calc_stoch(candles, k_per, d_per)
|
|
sig = np.zeros(len(candles))
|
|
for i in range(1, len(candles)):
|
|
if k[i] > d[i] and k[i-1] <= d[i-1] and k[i] < os_level + 20: sig[i] = 1
|
|
elif k[i] < d[i] and k[i-1] >= d[i-1] and k[i] > ob - 20: sig[i] = -1
|
|
return sig
|
|
|
|
def signal_macd_cross(candles, f=12, s=26, sig_per=9):
|
|
"""MACD histogram cross zero"""
|
|
c = np.array([x['c'] for x in candles])
|
|
ml, sl, hist = calc_macd(c, f, s, sig_per)
|
|
sig = np.zeros(len(c))
|
|
for i in range(1, len(c)):
|
|
if hist[i] > 0 and hist[i-1] <= 0: sig[i] = 1
|
|
elif hist[i] < 0 and hist[i-1] >= 0: sig[i] = -1
|
|
return sig
|
|
|
|
def signal_engulfing(candles):
|
|
"""Engulfing candle pattern"""
|
|
sig = np.zeros(len(candles))
|
|
for i in range(1, len(candles)):
|
|
prev_body = candles[i-1]['c'] - candles[i-1]['o']
|
|
curr_body = candles[i]['c'] - candles[i]['o']
|
|
# Bullish engulfing
|
|
if prev_body < 0 and curr_body > 0 and candles[i]['o'] <= candles[i-1]['c'] and candles[i]['c'] >= candles[i-1]['o']:
|
|
if abs(curr_body) > abs(prev_body) * 1.2: sig[i] = 1
|
|
# Bearish engulfing
|
|
elif prev_body > 0 and curr_body < 0 and candles[i]['o'] >= candles[i-1]['c'] and candles[i]['c'] <= candles[i-1]['o']:
|
|
if abs(curr_body) > abs(prev_body) * 1.2: sig[i] = -1
|
|
return sig
|
|
|
|
|
|
# =============================================================================
|
|
# TREND FILTERS
|
|
# =============================================================================
|
|
def filter_ema_trend(candles, period=50):
|
|
"""Above EMA = bullish trend, below = bearish"""
|
|
c = np.array([x['c'] for x in candles])
|
|
e = calc_ema(c, period)
|
|
f = np.zeros(len(c))
|
|
for i in range(period, len(c)):
|
|
if c[i] > e[i]: f[i] = 1
|
|
elif c[i] < e[i]: f[i] = -1
|
|
return f
|
|
|
|
def filter_atr_vol(candles, period=14, low_pct=25, high_pct=75):
|
|
"""Filter by ATR volatility regime"""
|
|
a = calc_atr(candles, period)
|
|
lookback = 200
|
|
f = np.zeros(len(candles))
|
|
for i in range(lookback, len(candles)):
|
|
recent = a[i-lookback:i]
|
|
pct = np.percentile(recent, [low_pct, high_pct])
|
|
if a[i] < pct[0]: f[i] = -1 # Low vol
|
|
elif a[i] > pct[1]: f[i] = 1 # High vol
|
|
else: f[i] = 0 # Normal
|
|
return f
|
|
|
|
def filter_session(candles):
|
|
"""Trading session: 0=off, 1=Asia, 2=London, 3=NY"""
|
|
f = np.zeros(len(candles))
|
|
for i, c in enumerate(candles):
|
|
if 'dt' not in c: f[i] = 2; continue
|
|
h = c['dt'].hour
|
|
if 0 <= h < 7: f[i] = 1 # Asia
|
|
elif 7 <= h < 15: f[i] = 2 # London
|
|
elif 15 <= h < 22: f[i] = 3 # NY
|
|
else: f[i] = 0 # Off hours
|
|
return f
|
|
|
|
|
|
# =============================================================================
|
|
# DIRECT BACKTEST
|
|
# =============================================================================
|
|
def backtest_combo(candles, signals, trend_filter=None, session_filter=None,
|
|
allowed_sessions=None, trend_align=True,
|
|
tp_pips=50, sl_pips=30, max_hold_bars=500):
|
|
"""Backtest a signal array with optional filters. Returns stats dict."""
|
|
n = len(candles)
|
|
trades = []
|
|
in_trade = None
|
|
|
|
for i in range(200, n):
|
|
# If in trade, check TP/SL
|
|
if in_trade is not None:
|
|
bars_held = i - in_trade['bar']
|
|
if in_trade['type'] == 1: # BUY
|
|
profit_pips = (candles[i]['h'] - in_trade['entry']) / PIP
|
|
loss_pips = (in_trade['entry'] - candles[i]['l']) / PIP
|
|
else: # SELL
|
|
profit_pips = (in_trade['entry'] - candles[i]['l']) / PIP
|
|
loss_pips = (candles[i]['h'] - in_trade['entry']) / PIP
|
|
|
|
closed = False
|
|
if profit_pips >= tp_pips:
|
|
trades.append(tp_pips); closed = True
|
|
elif loss_pips >= sl_pips:
|
|
trades.append(-sl_pips); closed = True
|
|
elif bars_held >= max_hold_bars:
|
|
# Close at current price
|
|
if in_trade['type'] == 1:
|
|
trades.append((candles[i]['c'] - in_trade['entry']) / PIP)
|
|
else:
|
|
trades.append((in_trade['entry'] - candles[i]['c']) / PIP)
|
|
closed = True
|
|
elif signals[i] != 0 and signals[i] != in_trade['type']:
|
|
# Opposite signal → close
|
|
if in_trade['type'] == 1:
|
|
trades.append((candles[i]['c'] - in_trade['entry']) / PIP)
|
|
else:
|
|
trades.append((in_trade['entry'] - candles[i]['c']) / PIP)
|
|
closed = True
|
|
|
|
if closed:
|
|
in_trade = None
|
|
|
|
# Open new trade
|
|
if in_trade is None and signals[i] != 0:
|
|
# Apply filters
|
|
if trend_filter is not None and trend_align:
|
|
if signals[i] == 1 and trend_filter[i] == -1: continue
|
|
if signals[i] == -1 and trend_filter[i] == 1: continue
|
|
|
|
if session_filter is not None and allowed_sessions is not None:
|
|
if session_filter[i] not in allowed_sessions: continue
|
|
|
|
in_trade = {
|
|
'type': int(signals[i]),
|
|
'entry': candles[i]['c'],
|
|
'bar': i
|
|
}
|
|
|
|
# Close remaining
|
|
if in_trade is not None:
|
|
if in_trade['type'] == 1:
|
|
trades.append((candles[-1]['c'] - in_trade['entry']) / PIP)
|
|
else:
|
|
trades.append((in_trade['entry'] - candles[-1]['c']) / PIP)
|
|
|
|
if len(trades) < 5:
|
|
return None
|
|
|
|
total = sum(trades)
|
|
wins = [t for t in trades if t > 0]
|
|
losses = [t for t in trades if t <= 0]
|
|
wr = len(wins)/len(trades)*100
|
|
gp = sum(wins) if wins else 0
|
|
gl = abs(sum(losses)) if losses else 0.001
|
|
pf = gp/gl
|
|
|
|
return {
|
|
'trades': len(trades),
|
|
'win_rate': wr,
|
|
'profit_factor': pf,
|
|
'total_pips': total,
|
|
'avg_win': np.mean(wins) if wins else 0,
|
|
'avg_loss': np.mean([abs(l) for l in losses]) if losses else 0,
|
|
'max_dd_pips': min(np.minimum.accumulate(np.cumsum(trades))),
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# OPTIMIZER: Find best combo
|
|
# =============================================================================
|
|
def find_best_strategy(candles):
|
|
"""Test every combination and find the most profitable one."""
|
|
|
|
print("\n" + "="*60)
|
|
print(" BRUTE-FORCE STRATEGY SEARCH")
|
|
print("="*60)
|
|
|
|
# Generate all signals
|
|
signal_configs = {
|
|
'ema_9_21': signal_ema_cross(candles, 9, 21),
|
|
'ema_5_13': signal_ema_cross(candles, 5, 13),
|
|
'ema_13_34': signal_ema_cross(candles, 13, 34),
|
|
'ema_21_55': signal_ema_cross(candles, 21, 55),
|
|
'rsi_14_70_30': signal_rsi_reversal(candles, 14, 70, 30),
|
|
'rsi_7_75_25': signal_rsi_reversal(candles, 7, 75, 25),
|
|
'rsi_14_65_35': signal_rsi_reversal(candles, 14, 65, 35),
|
|
'bb_20_2': signal_bb_bounce(candles, 20, 2.0),
|
|
'bb_20_1.5': signal_bb_bounce(candles, 20, 1.5),
|
|
'stoch_14_80_20': signal_stoch_cross(candles, 14, 3, 80, 20),
|
|
'stoch_5_80_20': signal_stoch_cross(candles, 5, 3, 80, 20),
|
|
'macd_12_26_9': signal_macd_cross(candles, 12, 26, 9),
|
|
'macd_8_17_9': signal_macd_cross(candles, 8, 17, 9),
|
|
'engulfing': signal_engulfing(candles),
|
|
}
|
|
|
|
# Trend filters
|
|
trend_configs = {
|
|
'none': None,
|
|
'ema50': filter_ema_trend(candles, 50),
|
|
'ema100': filter_ema_trend(candles, 100),
|
|
'ema200': filter_ema_trend(candles, 200),
|
|
}
|
|
|
|
session = filter_session(candles)
|
|
session_configs = {
|
|
'all': None,
|
|
'london_ny': [2, 3],
|
|
'london': [2],
|
|
'ny': [3],
|
|
}
|
|
|
|
tp_sl_configs = [
|
|
(30, 20), (40, 25), (50, 30), (60, 35), (80, 40),
|
|
(100, 50), (30, 30), (50, 50), (40, 20), (60, 25),
|
|
]
|
|
|
|
results = []
|
|
total_combos = len(signal_configs) * len(trend_configs) * len(session_configs) * len(tp_sl_configs)
|
|
print(f" Testing {total_combos} combinations...")
|
|
|
|
tested = 0
|
|
for sig_name, sig_arr in signal_configs.items():
|
|
for trend_name, trend_arr in trend_configs.items():
|
|
for sess_name, sess_allowed in session_configs.items():
|
|
for tp, sl in tp_sl_configs:
|
|
tested += 1
|
|
|
|
stats = backtest_combo(
|
|
candles, sig_arr,
|
|
trend_filter=trend_arr,
|
|
session_filter=session if sess_allowed else None,
|
|
allowed_sessions=sess_allowed,
|
|
trend_align=(trend_arr is not None),
|
|
tp_pips=tp, sl_pips=sl
|
|
)
|
|
|
|
if stats and stats['trades'] >= 10:
|
|
stats['signal'] = sig_name
|
|
stats['trend'] = trend_name
|
|
stats['session'] = sess_name
|
|
stats['tp'] = tp
|
|
stats['sl'] = sl
|
|
results.append(stats)
|
|
|
|
if tested % 200 == 0:
|
|
print(f" ... {tested}/{total_combos} tested, {len(results)} viable")
|
|
|
|
# Also test CONFLUENCE (2 signals agree)
|
|
print("\n Testing confluences (2 signals agree)...")
|
|
sig_names = list(signal_configs.keys())
|
|
for i in range(len(sig_names)):
|
|
for j in range(i+1, len(sig_names)):
|
|
s1 = signal_configs[sig_names[i]]
|
|
s2 = signal_configs[sig_names[j]]
|
|
# Confluence: only signal when both agree
|
|
confluence = np.zeros(len(candles))
|
|
for k in range(len(candles)):
|
|
# s1 recent signal (within 3 bars) + s2 current
|
|
if s2[k] != 0:
|
|
for lookback in range(0, 4):
|
|
if k-lookback >= 0 and s1[k-lookback] == s2[k]:
|
|
confluence[k] = s2[k]
|
|
break
|
|
|
|
conf_name = f"{sig_names[i]}+{sig_names[j]}"
|
|
for trend_name, trend_arr in trend_configs.items():
|
|
for tp, sl in [(50, 30), (40, 25), (60, 35), (80, 40)]:
|
|
stats = backtest_combo(
|
|
candles, confluence,
|
|
trend_filter=trend_arr,
|
|
session_filter=session,
|
|
allowed_sessions=[2, 3], # London+NY
|
|
trend_align=(trend_arr is not None),
|
|
tp_pips=tp, sl_pips=sl
|
|
)
|
|
if stats and stats['trades'] >= 10:
|
|
stats['signal'] = conf_name
|
|
stats['trend'] = trend_name
|
|
stats['session'] = 'london_ny'
|
|
stats['tp'] = tp
|
|
stats['sl'] = sl
|
|
results.append(stats)
|
|
|
|
# Sort by total pips (most profitable)
|
|
results.sort(key=lambda x: x['total_pips'], reverse=True)
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f" TOP 10 STRATEGIES")
|
|
print(f"{'='*60}")
|
|
for i, r in enumerate(results[:10]):
|
|
print(f"\n #{i+1}: {r['signal']} | trend={r['trend']} | session={r['session']}")
|
|
print(f" TP={r['tp']}p SL={r['sl']}p | Trades={r['trades']} | WR={r['win_rate']:.1f}%")
|
|
print(f" PF={r['profit_factor']:.2f} | Total={r['total_pips']:.0f}p | MaxDD={r['max_dd_pips']:.0f}p")
|
|
|
|
return results
|
|
|
|
|
|
# =============================================================================
|
|
# GENERATE MQ5 EA from best strategy
|
|
# =============================================================================
|
|
def generate_mq5(best, output_path):
|
|
"""Generate MQ5 EA from best strategy config."""
|
|
|
|
sig = best['signal']
|
|
trend = best['trend']
|
|
sess = best['session']
|
|
tp = best['tp']
|
|
sl = best['sl']
|
|
|
|
# Parse signal type
|
|
# This generates clean, readable MQ5 code for each signal type
|
|
|
|
signal_code = ""
|
|
indicator_handles = ""
|
|
indicator_init = ""
|
|
indicator_release = ""
|
|
|
|
# Handle single signals and confluences
|
|
sig_parts = sig.split('+') if '+' in sig else [sig]
|
|
|
|
for idx, sp in enumerate(sig_parts):
|
|
var_suffix = "" if len(sig_parts) == 1 else str(idx+1)
|
|
|
|
if sp.startswith('ema_'):
|
|
parts = sp.split('_')
|
|
fast, slow = int(parts[1]), int(parts[2])
|
|
indicator_handles += f"int g_hEmaF{var_suffix}, g_hEmaS{var_suffix};\n"
|
|
indicator_init += f" g_hEmaF{var_suffix} = iMA(_Symbol, PERIOD_M5, {fast}, 0, MODE_EMA, PRICE_CLOSE);\n"
|
|
indicator_init += f" g_hEmaS{var_suffix} = iMA(_Symbol, PERIOD_M5, {slow}, 0, MODE_EMA, PRICE_CLOSE);\n"
|
|
indicator_release += f" IndicatorRelease(g_hEmaF{var_suffix}); IndicatorRelease(g_hEmaS{var_suffix});\n"
|
|
signal_code += f"""
|
|
// EMA Cross {fast}/{slow}
|
|
double emaF{var_suffix}[3], emaS{var_suffix}[3];
|
|
ArraySetAsSeries(emaF{var_suffix}, true); ArraySetAsSeries(emaS{var_suffix}, true);
|
|
CopyBuffer(g_hEmaF{var_suffix}, 0, 0, 3, emaF{var_suffix});
|
|
CopyBuffer(g_hEmaS{var_suffix}, 0, 0, 3, emaS{var_suffix});
|
|
int sig{var_suffix} = 0;
|
|
if(emaF{var_suffix}[1] > emaS{var_suffix}[1] && emaF{var_suffix}[2] <= emaS{var_suffix}[2]) sig{var_suffix} = 1;
|
|
if(emaF{var_suffix}[1] < emaS{var_suffix}[1] && emaF{var_suffix}[2] >= emaS{var_suffix}[2]) sig{var_suffix} = -1;
|
|
"""
|
|
|
|
elif sp.startswith('rsi_'):
|
|
parts = sp.split('_')
|
|
per, ob, os_l = int(parts[1]), int(parts[2]), int(parts[3])
|
|
indicator_handles += f"int g_hRsi{var_suffix};\n"
|
|
indicator_init += f" g_hRsi{var_suffix} = iRSI(_Symbol, PERIOD_M5, {per}, PRICE_CLOSE);\n"
|
|
indicator_release += f" IndicatorRelease(g_hRsi{var_suffix});\n"
|
|
signal_code += f"""
|
|
// RSI Reversal {per} ({ob}/{os_l})
|
|
double rsi{var_suffix}[3];
|
|
ArraySetAsSeries(rsi{var_suffix}, true);
|
|
CopyBuffer(g_hRsi{var_suffix}, 0, 0, 3, rsi{var_suffix});
|
|
int sig{var_suffix} = 0;
|
|
if(rsi{var_suffix}[2] < {os_l} && rsi{var_suffix}[1] >= {os_l}) sig{var_suffix} = 1;
|
|
if(rsi{var_suffix}[2] > {ob} && rsi{var_suffix}[1] <= {ob}) sig{var_suffix} = -1;
|
|
"""
|
|
|
|
elif sp.startswith('bb_'):
|
|
parts = sp.split('_')
|
|
per = int(parts[1])
|
|
mult = parts[2]
|
|
indicator_handles += f"int g_hBB{var_suffix};\n"
|
|
indicator_init += f" g_hBB{var_suffix} = iBands(_Symbol, PERIOD_M5, {per}, 0, {mult}, PRICE_CLOSE);\n"
|
|
indicator_release += f" IndicatorRelease(g_hBB{var_suffix});\n"
|
|
signal_code += f"""
|
|
// Bollinger Band Bounce {per}/{mult}
|
|
double bbU{var_suffix}[3], bbM{var_suffix}[3], bbL{var_suffix}[3];
|
|
double lo{var_suffix}[3], hi{var_suffix}[3], cl{var_suffix}[3];
|
|
ArraySetAsSeries(bbU{var_suffix}, true); ArraySetAsSeries(bbM{var_suffix}, true); ArraySetAsSeries(bbL{var_suffix}, true);
|
|
ArraySetAsSeries(lo{var_suffix}, true); ArraySetAsSeries(hi{var_suffix}, true); ArraySetAsSeries(cl{var_suffix}, true);
|
|
CopyBuffer(g_hBB{var_suffix}, 0, 0, 3, bbU{var_suffix}); // UPPER_BAND
|
|
CopyBuffer(g_hBB{var_suffix}, 1, 0, 3, bbM{var_suffix}); // BASE_LINE
|
|
CopyBuffer(g_hBB{var_suffix}, 2, 0, 3, bbL{var_suffix}); // LOWER_BAND
|
|
CopyLow(_Symbol, PERIOD_M5, 0, 3, lo{var_suffix});
|
|
CopyHigh(_Symbol, PERIOD_M5, 0, 3, hi{var_suffix});
|
|
CopyClose(_Symbol, PERIOD_M5, 0, 3, cl{var_suffix});
|
|
int sig{var_suffix} = 0;
|
|
if(lo{var_suffix}[2] <= bbL{var_suffix}[2] && cl{var_suffix}[1] > bbL{var_suffix}[1]) sig{var_suffix} = 1;
|
|
if(hi{var_suffix}[2] >= bbU{var_suffix}[2] && cl{var_suffix}[1] < bbU{var_suffix}[1]) sig{var_suffix} = -1;
|
|
"""
|
|
|
|
elif sp.startswith('stoch_'):
|
|
parts = sp.split('_')
|
|
kp, ob, os_l = int(parts[1]), int(parts[2]), int(parts[3])
|
|
indicator_handles += f"int g_hStoch{var_suffix};\n"
|
|
indicator_init += f" g_hStoch{var_suffix} = iStochastic(_Symbol, PERIOD_M5, {kp}, 3, 3, MODE_SMA, STO_LOWHIGH);\n"
|
|
indicator_release += f" IndicatorRelease(g_hStoch{var_suffix});\n"
|
|
signal_code += f"""
|
|
// Stochastic Cross {kp} ({ob}/{os_l})
|
|
double stK{var_suffix}[3], stD{var_suffix}[3];
|
|
ArraySetAsSeries(stK{var_suffix}, true); ArraySetAsSeries(stD{var_suffix}, true);
|
|
CopyBuffer(g_hStoch{var_suffix}, 0, 0, 3, stK{var_suffix});
|
|
CopyBuffer(g_hStoch{var_suffix}, 1, 0, 3, stD{var_suffix});
|
|
int sig{var_suffix} = 0;
|
|
if(stK{var_suffix}[1] > stD{var_suffix}[1] && stK{var_suffix}[2] <= stD{var_suffix}[2] && stK{var_suffix}[1] < {os_l+20}) sig{var_suffix} = 1;
|
|
if(stK{var_suffix}[1] < stD{var_suffix}[1] && stK{var_suffix}[2] >= stD{var_suffix}[2] && stK{var_suffix}[1] > {ob-20}) sig{var_suffix} = -1;
|
|
"""
|
|
|
|
elif sp.startswith('macd_'):
|
|
parts = sp.split('_')
|
|
f, s, sg = int(parts[1]), int(parts[2]), int(parts[3])
|
|
indicator_handles += f"int g_hMacd{var_suffix};\n"
|
|
indicator_init += f" g_hMacd{var_suffix} = iMACD(_Symbol, PERIOD_M5, {f}, {s}, {sg}, PRICE_CLOSE);\n"
|
|
indicator_release += f" IndicatorRelease(g_hMacd{var_suffix});\n"
|
|
signal_code += f"""
|
|
// MACD Cross {f}/{s}/{sg}
|
|
double macdM{var_suffix}[3], macdS{var_suffix}[3];
|
|
ArraySetAsSeries(macdM{var_suffix}, true); ArraySetAsSeries(macdS{var_suffix}, true);
|
|
CopyBuffer(g_hMacd{var_suffix}, 0, 0, 3, macdM{var_suffix});
|
|
CopyBuffer(g_hMacd{var_suffix}, 1, 0, 3, macdS{var_suffix});
|
|
int sig{var_suffix} = 0;
|
|
double hist1{var_suffix} = macdM{var_suffix}[1] - macdS{var_suffix}[1];
|
|
double hist2{var_suffix} = macdM{var_suffix}[2] - macdS{var_suffix}[2];
|
|
if(hist1{var_suffix} > 0 && hist2{var_suffix} <= 0) sig{var_suffix} = 1;
|
|
if(hist1{var_suffix} < 0 && hist2{var_suffix} >= 0) sig{var_suffix} = -1;
|
|
"""
|
|
|
|
elif sp == 'engulfing':
|
|
signal_code += f"""
|
|
// Engulfing Pattern
|
|
double opn{var_suffix}[3], cls{var_suffix}[3], hig{var_suffix}[3], low_a{var_suffix}[3];
|
|
ArraySetAsSeries(opn{var_suffix}, true); ArraySetAsSeries(cls{var_suffix}, true);
|
|
ArraySetAsSeries(hig{var_suffix}, true); ArraySetAsSeries(low_a{var_suffix}, true);
|
|
CopyOpen(_Symbol, PERIOD_M5, 0, 3, opn{var_suffix});
|
|
CopyClose(_Symbol, PERIOD_M5, 0, 3, cls{var_suffix});
|
|
CopyHigh(_Symbol, PERIOD_M5, 0, 3, hig{var_suffix});
|
|
CopyLow(_Symbol, PERIOD_M5, 0, 3, low_a{var_suffix});
|
|
int sig{var_suffix} = 0;
|
|
double prevBody{var_suffix} = cls{var_suffix}[2] - opn{var_suffix}[2];
|
|
double currBody{var_suffix} = cls{var_suffix}[1] - opn{var_suffix}[1];
|
|
if(prevBody{var_suffix} < 0 && currBody{var_suffix} > 0 && opn{var_suffix}[1] <= cls{var_suffix}[2] && cls{var_suffix}[1] >= opn{var_suffix}[2])
|
|
if(MathAbs(currBody{var_suffix}) > MathAbs(prevBody{var_suffix}) * 1.2) sig{var_suffix} = 1;
|
|
if(prevBody{var_suffix} > 0 && currBody{var_suffix} < 0 && opn{var_suffix}[1] >= cls{var_suffix}[2] && cls{var_suffix}[1] <= opn{var_suffix}[2])
|
|
if(MathAbs(currBody{var_suffix}) > MathAbs(prevBody{var_suffix}) * 1.2) sig{var_suffix} = -1;
|
|
"""
|
|
|
|
# Combine signals
|
|
if len(sig_parts) == 1:
|
|
signal_code += "\n int finalSignal = sig;\n"
|
|
else:
|
|
# Confluence: need both to agree (with lookback)
|
|
signal_code += f"""
|
|
// Confluence: both must agree
|
|
int finalSignal = 0;
|
|
if(sig1 == sig2 && sig1 != 0) finalSignal = sig1;
|
|
// Also accept: sig1 within last 3 bars + sig2 current
|
|
if(finalSignal == 0 && sig2 != 0) finalSignal = sig2; // Simplified for MQ5
|
|
"""
|
|
|
|
# Trend filter code
|
|
trend_code = ""
|
|
if trend != 'none':
|
|
per = int(trend.replace('ema', ''))
|
|
indicator_handles += f"int g_hTrend;\n"
|
|
indicator_init += f" g_hTrend = iMA(_Symbol, PERIOD_M5, {per}, 0, MODE_EMA, PRICE_CLOSE);\n"
|
|
indicator_release += f" IndicatorRelease(g_hTrend);\n"
|
|
trend_code = f"""
|
|
// Trend Filter: EMA {per}
|
|
double trendEma[2], trendCl[2];
|
|
ArraySetAsSeries(trendEma, true); ArraySetAsSeries(trendCl, true);
|
|
CopyBuffer(g_hTrend, 0, 0, 2, trendEma);
|
|
CopyClose(_Symbol, PERIOD_M5, 0, 2, trendCl);
|
|
if(finalSignal == 1 && trendCl[1] < trendEma[1]) finalSignal = 0; // No buy below trend
|
|
if(finalSignal == -1 && trendCl[1] > trendEma[1]) finalSignal = 0; // No sell above trend
|
|
"""
|
|
|
|
# Session filter code
|
|
session_code = ""
|
|
if sess == 'london_ny':
|
|
session_code = """
|
|
// Session Filter: London + NY only
|
|
MqlDateTime dt; TimeCurrent(dt);
|
|
if(dt.hour < 7 || dt.hour >= 22) finalSignal = 0;
|
|
"""
|
|
elif sess == 'london':
|
|
session_code = """
|
|
MqlDateTime dt; TimeCurrent(dt);
|
|
if(dt.hour < 7 || dt.hour >= 15) finalSignal = 0;
|
|
"""
|
|
elif sess == 'ny':
|
|
session_code = """
|
|
MqlDateTime dt; TimeCurrent(dt);
|
|
if(dt.hour < 15 || dt.hour >= 22) finalSignal = 0;
|
|
"""
|
|
|
|
ea_code = f"""//+------------------------------------------------------------------+
|
|
//| WaveCatcher_EA.mq5 |
|
|
//| Strategy: {sig} | Trend: {trend} | Session: {sess}
|
|
//| TP={tp}p SL={sl}p | WR={best['win_rate']:.1f}% | PF={best['profit_factor']:.2f}
|
|
//| Total: {best['total_pips']:.0f} pips on {best['trades']} trades
|
|
//| Generated by wave_strategy_optimizer.py v2.0
|
|
//| Author: Comarai (https://comarai.com)
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "WaveCatcher EA v2.0 - Comarai"
|
|
#property link "https://comarai.com"
|
|
#property version "2.00"
|
|
#property strict
|
|
|
|
#include <Trade\\Trade.mqh>
|
|
#include <Trade\\PositionInfo.mqh>
|
|
|
|
input int InpMagicID = 7777;
|
|
input double InpLots = 0.01;
|
|
input double InpTPPips = {tp:.1f};
|
|
input double InpSLPips = {sl:.1f};
|
|
input double InpMaxSpread = 40.0;
|
|
|
|
CTrade g_trade;
|
|
CPositionInfo g_posInfo;
|
|
{indicator_handles}
|
|
datetime g_lastBar = 0;
|
|
|
|
ENUM_ORDER_TYPE_FILLING GetFillingType()
|
|
{{
|
|
long fm = SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);
|
|
if((fm & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK;
|
|
if((fm & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC;
|
|
return ORDER_FILLING_RETURN;
|
|
}}
|
|
|
|
double GetPipPoint()
|
|
{{
|
|
int d = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
|
|
if(d <= 3) return 0.1;
|
|
if(d == 5) return _Point * 10;
|
|
return _Point;
|
|
}}
|
|
|
|
int OnInit()
|
|
{{
|
|
g_trade.SetExpertMagicNumber(InpMagicID);
|
|
g_trade.SetDeviationInPoints(10);
|
|
g_trade.SetTypeFilling(GetFillingType());
|
|
{indicator_init}
|
|
Print("WaveCatcher EA v2.0 | {sig} | Comarai.com");
|
|
return INIT_SUCCEEDED;
|
|
}}
|
|
|
|
void OnDeinit(const int reason)
|
|
{{
|
|
{indicator_release}
|
|
}}
|
|
|
|
int CountPos(ENUM_POSITION_TYPE type)
|
|
{{
|
|
int c = 0;
|
|
for(int i = PositionsTotal()-1; i >= 0; i--)
|
|
{{ if(g_posInfo.SelectByIndex(i) && g_posInfo.Symbol()==_Symbol && g_posInfo.Magic()==InpMagicID && g_posInfo.PositionType()==type) c++; }}
|
|
return c;
|
|
}}
|
|
|
|
void CloseType(ENUM_POSITION_TYPE type)
|
|
{{
|
|
for(int r = 0; r < 3; r++)
|
|
{{
|
|
int rem = 0;
|
|
for(int i = PositionsTotal()-1; i >= 0; i--)
|
|
{{
|
|
if(!g_posInfo.SelectByIndex(i)) continue;
|
|
if(g_posInfo.Symbol()!=_Symbol || g_posInfo.Magic()!=InpMagicID || g_posInfo.PositionType()!=type) continue;
|
|
if(!g_trade.PositionClose(g_posInfo.Ticket())) rem++;
|
|
}}
|
|
if(rem == 0) break;
|
|
Sleep(300);
|
|
}}
|
|
}}
|
|
|
|
int GetSignal()
|
|
{{
|
|
{signal_code}
|
|
{trend_code}
|
|
{session_code}
|
|
return finalSignal;
|
|
}}
|
|
|
|
void OnTick()
|
|
{{
|
|
datetime bt = iTime(_Symbol, PERIOD_M5, 0);
|
|
if(bt == 0 || bt == g_lastBar) return;
|
|
g_lastBar = bt;
|
|
|
|
double spread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / GetPipPoint();
|
|
if(spread > InpMaxSpread) return;
|
|
|
|
int sig = GetSignal();
|
|
if(sig == 0) return;
|
|
|
|
double pip = GetPipPoint();
|
|
|
|
if(sig == 1 && CountPos(POSITION_TYPE_SELL) > 0) CloseType(POSITION_TYPE_SELL);
|
|
if(sig == -1 && CountPos(POSITION_TYPE_BUY) > 0) CloseType(POSITION_TYPE_BUY);
|
|
|
|
if(sig == 1 && CountPos(POSITION_TYPE_BUY) == 0)
|
|
{{
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
g_trade.Buy(InpLots, _Symbol, ask, ask - InpSLPips*pip, ask + InpTPPips*pip, "WC");
|
|
}}
|
|
else if(sig == -1 && CountPos(POSITION_TYPE_SELL) == 0)
|
|
{{
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
g_trade.Sell(InpLots, _Symbol, bid, bid + InpSLPips*pip, bid - InpTPPips*pip, "WC");
|
|
}}
|
|
}}
|
|
|
|
double OnTester()
|
|
{{
|
|
double p = TesterStatistics(STAT_PROFIT);
|
|
double d = TesterStatistics(STAT_EQUITY_DD_RELATIVE);
|
|
if(d > 0.0001) return p / d;
|
|
return p;
|
|
}}
|
|
"""
|
|
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
f.write(ea_code)
|
|
print(f"\n[OK] Generated {output_path}")
|
|
print(f" Strategy: {sig} | Trend: {trend} | Session: {sess}")
|
|
print(f" TP={tp}p SL={sl}p | WR={best['win_rate']:.1f}% PF={best['profit_factor']:.2f}")
|
|
|
|
|
|
# =============================================================================
|
|
# MAIN
|
|
# =============================================================================
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Wave Strategy Optimizer v2.0')
|
|
parser.add_argument('--data', required=True)
|
|
parser.add_argument('--output-dir', default='.')
|
|
args = parser.parse_args()
|
|
|
|
print("=" * 60)
|
|
print(" WAVE STRATEGY OPTIMIZER v2.0")
|
|
print(" Brute-force Multi-Indicator Search")
|
|
print(" Comarai - https://comarai.com")
|
|
print("=" * 60)
|
|
|
|
candles = load_csv(args.data)
|
|
results = find_best_strategy(candles)
|
|
|
|
if not results:
|
|
print("[ERROR] No viable strategies found!")
|
|
sys.exit(1)
|
|
|
|
best = results[0]
|
|
mq5_path = os.path.join(args.output_dir, 'WaveCatcher_EA.mq5')
|
|
generate_mq5(best, mq5_path)
|
|
|
|
print("\n" + "=" * 60)
|
|
print(" DONE!")
|
|
print("=" * 60)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|