Prepare source-only public release for develop.

Add cluster audit pipeline, united EA updates, brochure generators, and publication hygiene (gitignore, MT5 path desensitization, pre-upload scan). Remove tracked reports, models, and binary artifacts from the repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhutoutoutousan
2026-07-02 15:03:43 +02:00
co-authored by Cursor
parent 3f75a08848
commit 605faf5310
1014 changed files with 83437 additions and 10413 deletions
+36
View File
@@ -0,0 +1,36 @@
# AI / Machine Learning
ONNX-based price-action models for MetaTrader 5.
## Projects
| Directory | Symbol / TF | Notes |
|-----------|-------------|-------|
| [`xauusd_h1/`](xauusd_h1/) | XAUUSD H1 | Action classification EA |
| [`xauusd_m15/`](xauusd_m15/) | XAUUSD M15 | Shorter horizon |
| [`eurusd1h/`](eurusd1h/) | EURUSD H1 | |
| [`eurusd1min/`](eurusd1min/) | EURUSD M15 model | |
| [`btcusd1min/`](btcusd1min/) | BTCUSD M1 | |
| [`rsi-divergence/`](rsi-divergence/) | Divergence detector + EA |
| [`dummy/`](dummy/) | XAUUSD sandbox | Full train → ONNX → backtest walkthrough |
## Quick start (sandbox)
```bash
cd ai/dummy
pip install -r requirements.txt
python train_onnx_model.py
python quick_backtest.py
```
Trained artifacts (`models/*.onnx`, `*.pkl`) are **gitignored** — generate locally after clone.
## MQL5 integration
Each project includes an `.mq5` EA that loads ONNX via `#resource` or file path. See per-folder `README.md` and `MT5_SETUP.md` (dummy).
## Requirements
- Python 3.10+
- `MetaTrader5`, `onnxruntime`, `scikit-learn`, `pandas`, `numpy`
- Local MT5 terminal with history for your symbol
Binary file not shown.
Binary file not shown.
+418 -143
View File
@@ -1,46 +1,84 @@
//+------------------------------------------------------------------+
//| EURUSD_H1_ActionEA.mq5 |
//| ai/eurusd1h/main.py — 24 features, 5-class softmax |
//| Classes: 0=HOLD 1=BUY 2=SELL_SHORT 3=CLOSE_LONG 4=CLOSE_SHORT |
//| Entry: strict trio winner among p0,p1,p2 only. |
//| Exit: unique 5-class argmax != held side (1 long, 2 short). |
//| No SL / TP / ATR stops. Attach EURUSD H1. |
//| ai/eurusd1h/main.py — 24 features, 5-class softmax ONNX |
//| Train: python main.py → models/EURUSD_H1_action.onnx |
//| Attach EURUSD H1. MT5 optimize via run_mt5_tester.py |
//+------------------------------------------------------------------+
#property copyright "Profitable EA Project"
#property version "1.00"
#property description "EURUSD H1 action ONNX; ordinal entry/exit; no fixed SL/TP"
#property version "1.20"
#property description "EURUSD H1 action ONNX; aggregate + prob/ATR exits (optimizable)"
#include <Trade\Trade.mqh>
#resource "models\\EURUSD_H1_action.onnx" as uchar ExtModel[]
#define FEAT_COUNT 24
#define PRED_HIST_CAP 32
#define REL_EPS 1e-9
input group "Model"
input int InpLookback = 48;
input int InpEntryMode = 1;
input double InpProbBuy = 0.18;
input double InpProbSell = 0.18;
input double InpMinBeatHold = 0.04;
input int InpExitMode = 1; // 0=fixed prob; 1/2=close must beat HOLD and stay-in-trade (2 legacy; old 2 vs-HOLD-only removed)
input double InpProbCloseL = 0.18;
input double InpProbCloseS = 0.18;
input double InpMinCloseBeatHold = 0.03;
input int InpMinBarsInTradeModelExit = 1; // min bars before model exit (0=off); pure mode uses 5-class winner
input bool InpPureRelative = false; // true: no prob cutoffs/edges — entry=trio strict winner, exit=5-class strict winner != side
input bool InpUseCloseHeadExit = true; // legacy only when InpPureRelative=false (CL/CS vs HOLD/stay; see InpExitMode)
input bool InpUseDirFlipExit = true; // legacy only when InpPureRelative=false (gap edges InpFlipExitEdge)
input double InpFlipExitEdge = 0.03; // legacy dir-flip min gap (ignored when InpPureRelative)
input int InpMinBarsAfterExit = 6; // after any close, wait this many flat bars before a new entry (0=off)
input int InpCooldownBarsAfterAdverse = 12; // extra flat-bar pause after adverse (ATR) stop; 0 = use only MinBarsAfterExit
input group "Decision (aggregate + sample, lowers trade churn)"
input int InpSampleEveryNBars = 2; // run ONNX / refresh history every N new bars (>=1)
input int InpAggWindow = 4; // rolling mean over last K samples (>=1)
input int InpMinAggSamples = 2; // need this many samples in window before new entries
input int InpMinBarsBetweenEntries = 0; // after an open, wait this many flat bars before next entry (0=off)
input double InpMinDirEdge = 0.03; // legacy entry mode 1 only (ignored when InpPureRelative)
input bool InpRequireStayOverClose = true; // legacy entry (ignored when InpPureRelative)
input group "Session (match Python SESSION_HOUR_OFFSET)"
input int InpSessionHourOffset = 0;
input group "Scaler override (empty = EURUSD_H1_action_meta.json)"
input string InpFeatMinStr = "";
input string InpFeatMaxStr = "";
input group "Timing"
input int InpMinBarsInTrade = 1; // model exit only after this many bars in position (0=off)
input group "Trade"
input group "Risk"
input double InpLotSize = 0.01;
input int InpMagic = 902601;
input int InpSlippage = 30;
input group "Hard exits (fixed ATR in price — optional)"
input bool InpUseAdverseAtrExit = true; // stop by adverse move in ATR multiples (off = model-only risk)
input bool InpUseProfitAtrExit = false; // take-profit in ATR multiples (needs InpTakeProfitATR > 0)
input double InpMaxAdverseATR = 3.5;
input double InpTakeProfitATR = 0.0;
double g_feat_min[FEAT_COUNT];
double g_feat_max[FEAT_COUNT];
CTrade trade;
long g_onnx = INVALID_HANDLE;
long g_onnx = INVALID_HANDLE;
datetime g_last_bar = 0;
void InitDefaultScalerFromMeta()
double g_pred_hist[PRED_HIST_CAP][5];
int g_pred_hist_len = 0;
double g_smooth[5] = {0.2, 0.2, 0.2, 0.2, 0.2};
ulong g_bar_index = 0;
int g_entry_cooldown_bars = 0;
int g_agg_w = 4;
int g_sample_n = 2;
int g_min_agg_samples = 2;
void InitDefaultScalerBounds()
{
// EURUSD_H1_action_meta.json scaler_feature_min / max (train fit)
// MinMax bounds from ai/eurusd1h/models/EURUSD_H1_action_meta.json
double def_min[FEAT_COUNT] = {
0.9539399743080139,
0.9559400081634521,
@@ -104,12 +142,229 @@ bool ParseFeatCsv(const string s, double &arr[])
{
if(StringLen(s) < 3) return false;
string parts[];
if(StringSplit(s, ',', parts) != FEAT_COUNT) return false;
int n = StringSplit(s, ',', parts);
if(n != FEAT_COUNT) return false;
for(int i = 0; i < FEAT_COUNT; i++)
arr[i] = StringToDouble(parts[i]);
return true;
}
int OnInit()
{
InitDefaultScalerBounds();
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(InpSlippage);
trade.SetTypeFilling(ORDER_FILLING_IOC);
if(StringLen(InpFeatMinStr) > 0 && ParseFeatCsv(InpFeatMinStr, g_feat_min))
Print("EURUSD Action EA: loaded InpFeatMinStr (24)");
if(StringLen(InpFeatMaxStr) > 0 && ParseFeatCsv(InpFeatMaxStr, g_feat_max))
Print("EURUSD Action EA: loaded InpFeatMaxStr (24)");
g_onnx = OnnxCreateFromBuffer(ExtModel, ONNX_DEBUG_LOGS);
if(g_onnx == INVALID_HANDLE)
{
Print("OnnxCreateFromBuffer failed ", GetLastError());
return INIT_FAILED;
}
const long inShape[] = {1, InpLookback, FEAT_COUNT};
if(!OnnxSetInputShape(g_onnx, 0, inShape))
{
Print("OnnxSetInputShape failed ", GetLastError());
OnnxRelease(g_onnx);
return INIT_FAILED;
}
const long outShape[] = {1, 5};
if(!OnnxSetOutputShape(g_onnx, 0, outShape))
{
Print("OnnxSetOutputShape failed ", GetLastError());
OnnxRelease(g_onnx);
return INIT_FAILED;
}
g_agg_w = MathMax(1, MathMin(InpAggWindow, PRED_HIST_CAP));
g_sample_n = MathMax(1, InpSampleEveryNBars);
g_min_agg_samples = MathMax(1, MathMin(InpMinAggSamples, g_agg_w));
g_pred_hist_len = 0;
g_bar_index = 0;
g_entry_cooldown_bars = 0;
for(int k = 0; k < 5; k++)
g_smooth[k] = 0.2;
const bool has_atr = InpUseAdverseAtrExit || (InpUseProfitAtrExit && InpTakeProfitATR > 0.0);
const bool has_model_exit = InpPureRelative || InpUseCloseHeadExit || InpUseDirFlipExit;
if(!has_atr && !has_model_exit)
Print("EURUSD_H1_ActionEA: WARNING — no exit path enabled (enable InpPureRelative and/or legacy exits / ATR)");
Print("EURUSD_H1_ActionEA: ONNX OK. Chart TF=", EnumToString(PERIOD_CURRENT), "; Lookback=", InpLookback,
" sampleEvery=", g_sample_n, " aggWindow=", g_agg_w, " minAggSamples=", g_min_agg_samples,
" pureRelative=", InpPureRelative,
" entryCooldownBars=", InpMinBarsBetweenEntries, " minDirEdge=", InpMinDirEdge,
" stayOverClose=", InpRequireStayOverClose,
" exitMode=", InpExitMode, " minBarsInTradeModelExit=", InpMinBarsInTradeModelExit,
" closeHeadExit=", InpUseCloseHeadExit, " dirFlipExit=", InpUseDirFlipExit, " flipExitEdge=", InpFlipExitEdge,
" minBarsAfterExit=", InpMinBarsAfterExit, " cooldownAfterAdverse=", InpCooldownBarsAfterAdverse,
" useAdverseATR=", InpUseAdverseAtrExit, " useProfitATR=", InpUseProfitAtrExit,
" maxAdverseATR=", InpMaxAdverseATR, " takeProfitATR=", InpTakeProfitATR);
return INIT_SUCCEEDED;
}
void OnDeinit(const int r)
{
if(g_onnx != INVALID_HANDLE) OnnxRelease(g_onnx);
}
double AtrNow()
{
double b[];
ArraySetAsSeries(b, true);
int h = iATR(_Symbol, PERIOD_CURRENT, 14);
if(h == INVALID_HANDLE) return 0;
if(CopyBuffer(h, 0, 0, 2, b) < 1) { IndicatorRelease(h); return 0; }
double v = b[0];
IndicatorRelease(h);
return v;
}
bool AdverseExit(const long type, const double open_price)
{
if(!InpUseAdverseAtrExit || InpMaxAdverseATR <= 0.0)
return false;
double atr = AtrNow();
if(atr <= 0) return false;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(type == POSITION_TYPE_BUY)
{
double adv = (open_price - bid) / atr;
return adv >= InpMaxAdverseATR;
}
double adv = (ask - open_price) / atr;
return adv >= InpMaxAdverseATR;
}
bool ProfitExit(const long type, const double open_price)
{
if(!InpUseProfitAtrExit || InpTakeProfitATR <= 0.0)
return false;
double atr = AtrNow();
if(atr <= 0.0) return false;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(type == POSITION_TYPE_BUY)
return (bid - open_price) >= InpTakeProfitATR * atr;
return (open_price - ask) >= InpTakeProfitATR * atr;
}
bool ModelCloseLong(const double p0, const double p1, const double p3)
{
if(InpExitMode == 0)
return (p3 >= InpProbCloseL);
// Modes 1/2 (and default): close-long must beat HOLD and stay-long (BUY). Old mode-2 "vs HOLD only" fired almost every bar on softmax.
return (p3 > p0 + InpMinCloseBeatHold && p3 > p1);
}
bool ModelCloseShort(const double p0, const double p2, const double p4)
{
if(InpExitMode == 0)
return (p4 >= InpProbCloseS);
return (p4 > p0 + InpMinCloseBeatHold && p4 > p2);
}
bool ModelDirFlipExitLong(const double p0, const double p1, const double p2)
{
if(!InpUseDirFlipExit)
return false;
const double e = MathMax(0.0, InpFlipExitEdge);
return (p2 > p1 + e && p2 > p0 + InpMinBeatHold);
}
bool ModelDirFlipExitShort(const double p0, const double p1, const double p2)
{
if(!InpUseDirFlipExit)
return false;
const double e = MathMax(0.0, InpFlipExitEdge);
return (p1 > p2 + e && p1 > p0 + InpMinBeatHold);
}
int TrioStrictWinner012(const double p0, const double p1, const double p2)
{
if(p0 > p1 + REL_EPS && p0 > p2 + REL_EPS)
return 0;
if(p1 > p0 + REL_EPS && p1 > p2 + REL_EPS)
return 1;
if(p2 > p0 + REL_EPS && p2 > p1 + REL_EPS)
return 2;
return -1;
}
int FiveStrictWinner01234(const double p0, const double p1, const double p2, const double p3, const double p4)
{
const double p[5] = {p0, p1, p2, p3, p4};
int best = 0;
for(int k = 1; k < 5; k++)
if(p[k] > p[best])
best = k;
const double m = p[best];
int cnt = 0;
for(int k = 0; k < 5; k++)
if(p[k] + REL_EPS >= m)
cnt++;
if(cnt != 1)
return -1;
return best;
}
int PositionBarsInTrade()
{
if(!PositionSelect(_Symbol))
return 0;
const datetime tOpen = (datetime)PositionGetInteger(POSITION_TIME);
const int sh = iBarShift(_Symbol, PERIOD_CURRENT, tOpen, false);
if(sh < 0)
return 9999;
return sh + 1;
}
void ApplyExitCooldown(const bool adverse_stop)
{
int b = MathMax(0, InpMinBarsAfterExit);
if(adverse_stop)
b = MathMax(b, MathMax(0, InpCooldownBarsAfterAdverse));
if(b > 0)
g_entry_cooldown_bars = MathMax(g_entry_cooldown_bars, b);
}
void PushPrediction(const double p0, const double p1, const double p2, const double p3, const double p4, const int maxKeep)
{
for(int i = PRED_HIST_CAP - 1; i > 0; i--)
for(int k = 0; k < 5; k++)
g_pred_hist[i][k] = g_pred_hist[i - 1][k];
g_pred_hist[0][0] = p0;
g_pred_hist[0][1] = p1;
g_pred_hist[0][2] = p2;
g_pred_hist[0][3] = p3;
g_pred_hist[0][4] = p4;
int cap = MathMax(1, MathMin(maxKeep, PRED_HIST_CAP));
g_pred_hist_len = MathMin(g_pred_hist_len + 1, cap);
}
void RecomputeSmooth(const int aggWindow)
{
int w = MathMax(1, MathMin(aggWindow, PRED_HIST_CAP));
int n = MathMin(w, g_pred_hist_len);
if(n < 1)
return;
for(int k = 0; k < 5; k++)
{
double s = 0.0;
for(int i = 0; i < n; i++)
s += g_pred_hist[i][k];
g_smooth[k] = s / (double)n;
}
}
void ScaleFeatures(const float &raw[], float &out[])
{
for(int f = 0; f < FEAT_COUNT; f++)
@@ -192,9 +447,9 @@ bool PrepareMatrix(matrixf &M)
double rv7 = rsi7[i];
double rv21 = rsi21[i];
double spr = (r0 - rv7) / 50.0;
if(spr > 1.0) spr = 1.0;
if(spr < -1.0) spr = -1.0;
double spread = (r0 - rv7) / 50.0;
if(spread > 1.0) spread = 1.0;
if(spread < -1.0) spread = -1.0;
double vel = (r0 - r1) / 25.0;
double acc = ((r0 - r1) - (r1 - r2)) / 25.0;
double dist_mid = MathAbs(r0 - 50.0) / 50.0;
@@ -226,7 +481,7 @@ bool PrepareMatrix(matrixf &M)
raw[12] = (float)(vma > 0 ? (double)vol[i] / vma : 1.0);
raw[13] = (float)(rv7 / 100.0);
raw[14] = (float)(rv21 / 100.0);
raw[15] = (float)spr;
raw[15] = (float)spread;
raw[16] = (float)vel;
raw[17] = (float)acc;
raw[18] = (float)dist_mid;
@@ -244,143 +499,163 @@ bool PrepareMatrix(matrixf &M)
return true;
}
int TrioStrictWinner012(const double p0, const double p1, const double p2)
{
if(p0 > p1 + REL_EPS && p0 > p2 + REL_EPS) return 0;
if(p1 > p0 + REL_EPS && p1 > p2 + REL_EPS) return 1;
if(p2 > p0 + REL_EPS && p2 > p1 + REL_EPS) return 2;
return -1;
}
int FiveStrictWinner01234(const double p0, const double p1, const double p2, const double p3, const double p4)
{
const double p[5] = {p0, p1, p2, p3, p4};
int best = 0;
for(int k = 1; k < 5; k++)
if(p[k] > p[best])
best = k;
const double m = p[best];
int cnt = 0;
for(int k = 0; k < 5; k++)
if(p[k] + REL_EPS >= m)
cnt++;
if(cnt != 1)
return -1;
return best;
}
bool SelectOurPosition()
{
if(!PositionSelect(_Symbol))
return false;
if((long)PositionGetInteger(POSITION_MAGIC) != InpMagic)
return false;
return true;
}
int PositionBarsInTrade()
{
if(!SelectOurPosition())
return 0;
const datetime tOpen = (datetime)PositionGetInteger(POSITION_TIME);
const int sh = iBarShift(_Symbol, PERIOD_CURRENT, tOpen, false);
if(sh < 0)
return 9999;
return sh + 1;
}
int OnInit()
{
InitDefaultScalerFromMeta();
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(InpSlippage);
trade.SetTypeFilling(ORDER_FILLING_IOC);
if(StringLen(InpFeatMinStr) > 0 && ParseFeatCsv(InpFeatMinStr, g_feat_min))
Print("EURUSD Action EA: loaded InpFeatMinStr");
if(StringLen(InpFeatMaxStr) > 0 && ParseFeatCsv(InpFeatMaxStr, g_feat_max))
Print("EURUSD Action EA: loaded InpFeatMaxStr");
g_onnx = OnnxCreateFromBuffer(ExtModel, ONNX_DEBUG_LOGS);
if(g_onnx == INVALID_HANDLE)
{
Print("OnnxCreateFromBuffer failed ", GetLastError());
return INIT_FAILED;
}
const long inShape[] = {1, InpLookback, FEAT_COUNT};
if(!OnnxSetInputShape(g_onnx, 0, inShape))
{
Print("OnnxSetInputShape failed ", GetLastError());
OnnxRelease(g_onnx);
return INIT_FAILED;
}
const long outShape[] = {1, 5};
if(!OnnxSetOutputShape(g_onnx, 0, outShape))
{
Print("OnnxSetOutputShape failed ", GetLastError());
OnnxRelease(g_onnx);
return INIT_FAILED;
}
if(_Period != PERIOD_H1)
Print("EURUSD_H1_ActionEA: chart period is ", EnumToString((ENUM_TIMEFRAMES)_Period),
" — training is H1; mismatch may hurt.");
Print("EURUSD_H1_ActionEA: ONNX OK. Ordinal entry/exit, no SL/TP. Lookback=", InpLookback);
return INIT_SUCCEEDED;
}
void OnDeinit(const int r)
{
if(g_onnx != INVALID_HANDLE)
OnnxRelease(g_onnx);
}
void OnTick()
{
datetime t = iTime(_Symbol, PERIOD_CURRENT, 0);
if(t == g_last_bar)
return;
if(t == g_last_bar) return;
g_last_bar = t;
matrixf Min;
if(!PrepareMatrix(Min))
const bool had_pos = PositionSelect(_Symbol);
const bool flat = !had_pos;
if(flat && g_entry_cooldown_bars > 0)
g_entry_cooldown_bars--;
g_bar_index++;
const bool do_sample = (g_sample_n < 2) || ((g_bar_index % (ulong)g_sample_n) == 0);
bool fresh_predict = false;
if(do_sample)
{
Print("EURUSD Action EA: PrepareMatrix failed");
return;
matrixf Min;
if(!PrepareMatrix(Min))
{
Print("EURUSD Action EA: PrepareMatrix failed");
if(!had_pos)
return;
}
else
{
vectorf out;
out.Resize(5);
if(!OnnxRun(g_onnx, ONNX_NO_CONVERSION, Min, out))
{
Print("OnnxRun failed ", GetLastError());
if(!had_pos)
return;
}
else
{
PushPrediction(out[0], out[1], out[2], out[3], out[4], g_agg_w);
RecomputeSmooth(g_agg_w);
fresh_predict = true;
}
}
}
vectorf out;
out.Resize(5);
if(!OnnxRun(g_onnx, ONNX_NO_CONVERSION, Min, out))
const double p0 = g_smooth[0];
const double p1 = g_smooth[1];
const double p2 = g_smooth[2];
const double p3 = g_smooth[3];
const double p4 = g_smooth[4];
if(flat)
{
Print("OnnxRun failed ", GetLastError());
if(!do_sample || !fresh_predict)
return;
if(g_pred_hist_len < g_min_agg_samples)
return;
if(g_entry_cooldown_bars > 0)
return;
if(InpEntryMode == 1)
{
if(InpPureRelative)
{
const int w3 = TrioStrictWinner012(p0, p1, p2);
if(w3 == 1)
{
if(trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act BUY"))
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
}
else if(w3 == 2)
{
if(trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act SELL"))
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
}
}
else
{
double dir = MathMax(p1, p2);
if(dir <= p0 + InpMinBeatHold)
return;
const double edge = MathMax(0.0, InpMinDirEdge);
const bool stay_ok_buy = (!InpRequireStayOverClose) || (p1 > p3);
const bool stay_ok_sell = (!InpRequireStayOverClose) || (p2 > p4);
if(p1 >= p2 && p1 > p0 + InpMinBeatHold && (p1 - p2) >= edge && stay_ok_buy)
{
if(trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act BUY"))
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
}
else if(p2 > p1 && p2 > p0 + InpMinBeatHold && (p2 - p1) >= edge && stay_ok_sell)
{
if(trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act SELL"))
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
}
}
}
else
{
if(p1 >= InpProbBuy && p1 >= p2)
{
if(trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act BUY"))
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
}
else if(p2 >= InpProbSell && p2 > p1)
{
if(trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act SELL"))
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
}
}
return;
}
const double p0 = out[0], p1 = out[1], p2 = out[2], p3 = out[3], p4 = out[4];
if(!SelectOurPosition())
long typ = (long)PositionGetInteger(POSITION_TYPE);
double opn = PositionGetDouble(POSITION_PRICE_OPEN);
if(AdverseExit(typ, opn))
{
const int w3 = TrioStrictWinner012(p0, p1, p2);
if(w3 == 1)
trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act BUY");
else if(w3 == 2)
trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act SELL");
if(trade.PositionClose(_Symbol))
ApplyExitCooldown(true);
return;
}
if(ProfitExit(typ, opn))
{
if(trade.PositionClose(_Symbol))
ApplyExitCooldown(false);
return;
}
const bool allow = (InpMinBarsInTrade <= 0) || (PositionBarsInTrade() >= InpMinBarsInTrade);
if(!allow)
return;
const int w5 = FiveStrictWinner01234(p0, p1, p2, p3, p4);
const long typ = (long)PositionGetInteger(POSITION_TYPE);
bool close_it = false;
if(typ == POSITION_TYPE_BUY)
close_it = (w5 != -1 && w5 != 1);
else if(typ == POSITION_TYPE_SELL)
close_it = (w5 != -1 && w5 != 2);
if(close_it)
trade.PositionClose(_Symbol);
const int bars_in = PositionBarsInTrade();
const bool allow_model_exit = (InpMinBarsInTradeModelExit <= 0) || (bars_in >= InpMinBarsInTradeModelExit);
if(allow_model_exit)
{
bool want_close = false;
if(InpPureRelative)
{
const int w5 = FiveStrictWinner01234(p0, p1, p2, p3, p4);
if(typ == POSITION_TYPE_BUY)
want_close = (w5 != -1 && w5 != 1);
else
want_close = (w5 != -1 && w5 != 2);
}
else
{
if(typ == POSITION_TYPE_BUY)
{
const bool head = InpUseCloseHeadExit && ModelCloseLong(p0, p1, p3);
const bool flip = ModelDirFlipExitLong(p0, p1, p2);
want_close = (head || flip);
}
else
{
const bool head = InpUseCloseHeadExit && ModelCloseShort(p0, p2, p4);
const bool flip = ModelDirFlipExitShort(p0, p1, p2);
want_close = (head || flip);
}
}
if(want_close)
{
if(trade.PositionClose(_Symbol))
ApplyExitCooldown(false);
}
}
}
+31
View File
@@ -0,0 +1,31 @@
; EURUSD H1 Action EA — default backtest (legacy prob + ATR stop)
InpLookback=48
InpEntryMode=1
InpProbBuy=0.18
InpProbSell=0.18
InpMinBeatHold=0.04
InpExitMode=2
InpProbCloseL=0.18
InpProbCloseS=0.18
InpMinCloseBeatHold=0.03
InpMinBarsInTradeModelExit=2
InpPureRelative=false
InpUseCloseHeadExit=true
InpUseDirFlipExit=true
InpFlipExitEdge=0.03
InpMinBarsAfterExit=4
InpCooldownBarsAfterAdverse=8
InpSampleEveryNBars=2
InpAggWindow=4
InpMinAggSamples=2
InpMinBarsBetweenEntries=2
InpMinDirEdge=0.03
InpRequireStayOverClose=true
InpSessionHourOffset=0
InpLotSize=0.10
InpMagic=902601
InpSlippage=30
InpUseAdverseAtrExit=true
InpUseProfitAtrExit=false
InpMaxAdverseATR=2.5
InpTakeProfitATR=0.0
@@ -0,0 +1,31 @@
; EURUSD H1 Action EA — genetic optimization
InpLookback=48||48||1||48||N
InpEntryMode=1||0||1||1||Y
InpProbBuy=0.18||0.14||0.02||0.30||Y
InpProbSell=0.18||0.14||0.02||0.30||Y
InpMinBeatHold=0.04||0.0||0.01||0.12||Y
InpExitMode=2||0||1||2||Y
InpProbCloseL=0.18||0.12||0.02||0.30||Y
InpProbCloseS=0.18||0.12||0.02||0.30||Y
InpMinCloseBeatHold=0.03||0.0||0.01||0.10||Y
InpMinBarsInTradeModelExit=2||0||1||8||Y
InpPureRelative=false||false||0||true||Y
InpUseCloseHeadExit=true||false||0||true||Y
InpUseDirFlipExit=true||false||0||true||Y
InpFlipExitEdge=0.03||0.01||0.01||0.08||Y
InpMinBarsAfterExit=4||0||2||12||Y
InpCooldownBarsAfterAdverse=8||0||4||24||Y
InpSampleEveryNBars=2||1||1||4||Y
InpAggWindow=4||2||1||8||Y
InpMinAggSamples=2||1||1||4||Y
InpMinBarsBetweenEntries=2||0||1||8||Y
InpMinDirEdge=0.03||0.01||0.01||0.10||Y
InpRequireStayOverClose=true||false||0||true||Y
InpSessionHourOffset=0||-2||1||2||N
InpLotSize=0.10||0.01||0.01||0.10||N
InpMagic=902601||902601||1||902601||N
InpSlippage=30||30||1||100||N
InpUseAdverseAtrExit=true||false||0||true||Y
InpUseProfitAtrExit=false||false||0||true||Y
InpMaxAdverseATR=2.5||1.5||0.25||4.0||Y
InpTakeProfitATR=0.0||0.0||0.5||4.0||Y
Binary file not shown.
Binary file not shown.
+63 -63
View File
@@ -38,37 +38,37 @@
"CLOSE_SHORT"
],
"mt5_bar_range": [
"2010-03-17 23:00:00",
"2026-04-24 23:00:00"
"2011-11-22 00:00:00",
"2026-06-25 23:00:00"
],
"scaler_fit_on": "all_valid_feature_rows_full_mt5_range",
"validation_split": {
"mode": "chronological_tail_fraction",
"val_fraction": 0.12,
"train_sequences": 87914,
"val_sequences": 11989
"train_sequences": 20267,
"val_sequences": 2764
},
"clustering": "KMeans n=12 on forward returns (1,2,4,8,16); train-only fit",
"scaler_feature_min": [
0.9539399743080139,
0.9559400081634521,
0.9536200165748596,
0.9538999795913696,
9.999999974752427e-07,
0.07019035518169403,
-0.02143237181007862,
-0.028110405430197716,
0.0002704667276702821,
-0.02017582766711712,
1.0,
0.0004555500054266304,
0.0002461568801663816,
0.022001149132847786,
0.11231997609138489,
-0.5339273810386658,
-1.623793125152588,
-1.837566614151001,
6.83732741890708e-06,
0.9591299891471863,
0.9670900106430054,
0.9535899758338928,
0.9593700170516968,
0.00010299999848939478,
0.10443684458732605,
-0.04120740666985512,
-0.04531921073794365,
0.0003194698365405202,
-0.025094643235206604,
1.0000925064086914,
0.0006479999865405262,
0.017702626064419746,
0.02270212210714817,
0.1845751404762268,
-0.4544973373413086,
-1.7025094032287598,
-1.832268238067627,
1.076605258276686e-05,
0.0,
0.0,
0.0,
@@ -76,25 +76,25 @@
0.0
],
"scaler_feature_max": [
1.493149995803833,
1.4938499927520752,
1.4904999732971191,
1.493190050125122,
0.06699500232934952,
0.9350273013114929,
0.028008731082081795,
0.03147505968809128,
0.009249407798051834,
0.01742853783071041,
1.0232577323913574,
0.02111775055527687,
7.801275253295898,
0.9864169955253601,
0.8837512731552124,
0.49708572030067444,
1.8055412769317627,
1.927569031715393,
0.8700546026229858,
1.3932499885559082,
1.399340033531189,
1.3910000324249268,
1.3933900594711304,
0.5568050146102905,
0.9161010384559631,
0.05199148878455162,
0.08236432075500488,
0.018198959529399872,
0.03056110255420208,
1.047353744506836,
0.2869023084640503,
10.783438682556152,
0.9746928811073303,
0.8693897128105164,
0.4820457994937897,
1.3869965076446533,
1.7234584093093872,
0.8322020173072815,
1.0,
1.0,
1.0,
@@ -102,32 +102,32 @@
1.0
],
"scaler_scale": [
1.854564905166626,
1.8590470552444458,
1.8626137971878052,
1.8542896509170532,
14.926709175109863,
1.1562873125076294,
20.226085662841797,
16.782615661621094,
111.3717041015625,
26.5926570892334,
42.99645233154297,
48.39755630493164,
0.12818820774555206,
1.03689706325531,
1.296291708946228,
0.969919741153717,
0.2916017770767212,
0.2655946612358093,
1.1493622064590454,
2.3035106658935547,
2.3134758472442627,
2.286184310913086,
2.3040411472320557,
1.7962931394577026,
1.2320365905761719,
10.729741096496582,
7.8318634033203125,
55.93000793457031,
17.96759605407715,
21.15898895263672,
3.4933969974517822,
0.09288728982210159,
1.0504302978515625,
1.4602493047714233,
1.0677565336227417,
0.32367634773254395,
0.281236469745636,
1.2016469240188599,
1.0,
1.0,
1.0,
1.0,
1.0
],
"tail_val_accuracy": 0.2594878673553467,
"tail_val_loss": 1.5548540353775024,
"tail_val_accuracy": 0.23371924459934235,
"tail_val_loss": 1.5593042373657227,
"ea_note": "Copy ai/yt/US500_H1_ArticleEA.mq5 pattern: #resource ONNX + paste scaler from meta."
}
Binary file not shown.
+266
View File
@@ -0,0 +1,266 @@
"""
Launch MT5 Strategy Tester for EURUSD H1 Action ONNX EA.
Usage:
python run_mt5_tester.py backtest
python run_mt5_tester.py optimize
python run_mt5_tester.py backtest --from 2020.01.01 --to 2026.01.01
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
import MetaTrader5 as mt5
LAB = Path(__file__).resolve().parent
EA_SRC = LAB / "EURUSD_H1_ActionEA.mq5"
MODEL_SRC = LAB / "models" / "EURUSD_H1_action.onnx"
DEFAULT_SET = LAB / "EURUSD_H1_ActionEA.set"
OPT_SET = LAB / "EURUSD_H1_ActionEA_optimize.set"
LABELS = {
"profit_factor": ("Profit Factor", "盈利因子"),
"net_profit": ("Total Net Profit", "总净盈利"),
"total_trades": ("Total Trades", "交易总计"),
"sharpe": ("Sharpe Ratio", "夏普比率"),
"equity_dd": ("Equity Drawdown Maximal", "最大回撤"),
}
def read_text(path: Path) -> str:
text = path.read_text(encoding="utf-16", errors="ignore")
if not text.strip():
text = path.read_text(encoding="utf-8", errors="ignore")
return text
def grab_metric(text: str, key: str) -> str | None:
for label in LABELS[key]:
for pat in (
rf">{re.escape(label)}</td>\s*<td[^>]*>(?:<b>)?([^<]+)",
rf">{re.escape(label)}:</td>\s*<td[^>]*>(?:<b>)?([^<]+)",
):
m = re.search(pat, text, re.I)
if m:
return m.group(1).strip()
return None
def parse_report(data: Path, report: str) -> dict:
xml_path = data / f"{report}.xml"
if xml_path.exists():
text = xml_path.read_text(encoding="utf-8", errors="ignore")
m = re.search(
r"<Row>\s*<Cell[^>]*><Data[^>]*>Pass</Data>.*?</Row>\s*<Row>(.*?)</Row>",
text,
re.S,
)
if m:
cells = re.findall(r'<Data ss:Type="(?:Number|String)">([^<]+)</Data>', m.group(1))
if len(cells) >= 10:
return {
"ready": True,
"report": str(xml_path),
"net_profit": float(cells[2]),
"profit_factor": float(cells[4]),
"sharpe": float(cells[6]),
"max_drawdown": f"{cells[8]}%",
"total_trades": int(float(cells[9])),
}
for path in sorted(data.glob(f"**/{report}*.htm*"), key=lambda p: p.stat().st_mtime, reverse=True):
text = read_text(path)
pf = grab_metric(text, "profit_factor")
profit = grab_metric(text, "net_profit")
trades = grab_metric(text, "total_trades")
sharpe = grab_metric(text, "sharpe")
dd = grab_metric(text, "equity_dd")
if pf or profit or trades:
return {
"profit_factor": float(pf) if pf else None,
"net_profit": _num(profit),
"total_trades": int(float(trades)) if trades and trades[0].isdigit() else None,
"sharpe": float(sharpe) if sharpe else None,
"max_drawdown": dd,
"report": str(path),
"ready": True,
}
return {"ready": False}
def _num(s: str | None) -> float | None:
if not s:
return None
s = s.replace(" ", "").replace(",", "")
if s.endswith("%"):
return float(s[:-1])
return float(s)
def mt5_context() -> dict:
if not mt5.initialize():
raise RuntimeError(f"MT5 init failed: {mt5.last_error()}")
info = mt5.terminal_info()
acc = mt5.account_info()
ctx = {
"data": Path(info.data_path),
"mt5_path": Path(info.path),
"login": acc.login if acc else 0,
"server": acc.server if acc else "",
}
mt5.shutdown()
return ctx
def deploy_ea(data: Path, mt5_path: Path) -> Path:
if not MODEL_SRC.exists():
raise FileNotFoundError(
f"Missing ONNX: {MODEL_SRC}\nRun: cd ai/eurusd1h && python main.py"
)
dst_dir = data / "MQL5" / "Experts" / "ai" / "eurusd1h"
models_dir = dst_dir / "models"
models_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(MODEL_SRC, models_dir / "EURUSD_H1_action.onnx")
dst = dst_dir / "EURUSD_H1_ActionEA.mq5"
shutil.copy2(EA_SRC, dst)
log = dst_dir / "compile.log"
subprocess.run(
[str(mt5_path / "metaeditor64.exe"), f"/compile:{dst}", f"/log:{log}"],
timeout=180,
capture_output=True,
)
time.sleep(3)
ex5 = dst_dir / "EURUSD_H1_ActionEA.ex5"
if not ex5.exists():
tail = log.read_text(encoding="utf-8", errors="ignore")[-2500:] if log.exists() else ""
raise RuntimeError(f"Compile failed:\n{dst}\n{tail}")
pub = data / "MQL5" / "Experts" / "EURUSD_H1_ActionEA.ex5"
shutil.copy2(ex5, pub)
return pub
def copy_set_to_tester(data: Path, set_path: Path, set_name: str) -> None:
profiles = data / "MQL5" / "Profiles" / "Tester"
profiles.mkdir(parents=True, exist_ok=True)
shutil.copy2(set_path, profiles / set_name)
def build_ini(**kw) -> str:
return f"""[Common]
Login={kw['login']}
Server={kw['server']}
[Tester]
Expert=EURUSD_H1_ActionEA.ex5
ExpertParameters={kw['set_name']}
Symbol={kw['symbol']}
Period={kw['period']}
Optimization={kw['optimization']}
Model=1
Dates=1
FromDate={kw['from_date']}
ToDate={kw['to_date']}
ForwardMode=0
Deposit={kw['deposit']}
Currency=USD
Leverage={kw['leverage']}
ExecutionMode=0
Report={kw['report']}
ReplaceReport=1
ShutdownTerminal=1
Visual={1 if kw['visual'] else 0}
"""
def run_tester(ctx: dict, **kw) -> dict:
data: Path = ctx["data"]
mt5_path: Path = ctx["mt5_path"]
deploy_ea(data, mt5_path)
copy_set_to_tester(data, kw["set_path"], kw["set_name"])
ini = data / f"{kw['report']}.ini"
ini.write_text(
build_ini(
login=ctx["login"],
server=ctx["server"],
set_name=kw["set_name"],
report=kw["report"],
symbol=kw["symbol"],
period=kw["period"],
optimization=2 if kw["mode"] == "optimize" else 0,
from_date=kw["from_date"],
to_date=kw["to_date"],
deposit=kw["deposit"],
leverage=kw["leverage"],
visual=kw["visual"],
),
encoding="utf-8",
)
for ext in (".htm", ".html", ".xml"):
p = data / f"{kw['report']}{ext}"
if p.exists():
p.unlink(missing_ok=True)
subprocess.run(["taskkill", "/IM", "terminal64.exe", "/F"], capture_output=True)
subprocess.run(["taskkill", "/IM", "metatester64.exe", "/F"], capture_output=True)
time.sleep(4)
print(f"Starting MT5 ({kw['mode']}) EA=EURUSD_H1_ActionEA {kw['symbol']} {kw['period']}")
print(f" Set: {kw['set_name']} {kw['from_date']} -> {kw['to_date']}")
t0 = time.time()
timeout = kw.get("timeout_sec", 7200 if kw["mode"] == "optimize" else 3600)
subprocess.run([str(mt5_path / "terminal64.exe"), f"/config:{ini}"], timeout=timeout)
metrics = parse_report(data, kw["report"])
metrics["elapsed_sec"] = round(time.time() - t0, 1)
metrics["mode"] = kw["mode"]
return metrics
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("mode", choices=["backtest", "optimize"])
p.add_argument("--symbol", default="EURUSD")
p.add_argument("--period", default="H1")
p.add_argument("--from", dest="from_date", default="2020.01.01")
p.add_argument("--to", dest="to_date", default="2026.01.01")
p.add_argument("--deposit", type=float, default=10000)
p.add_argument("--leverage", type=int, default=100)
p.add_argument("--visual", action="store_true")
p.add_argument("--set", dest="set_file", default="")
args = p.parse_args()
ctx = mt5_context()
set_path = Path(args.set_file) if args.set_file else (OPT_SET if args.mode == "optimize" else DEFAULT_SET)
report = f"EURUSD_H1_{args.symbol}_{args.mode}"
metrics = run_tester(
ctx,
mode=args.mode,
set_path=set_path,
set_name=set_path.name,
report=report,
symbol=args.symbol,
period=args.period,
from_date=args.from_date,
to_date=args.to_date,
deposit=args.deposit,
leverage=args.leverage,
visual=args.visual,
)
out = LAB / "mt5_results.json"
with open(out, "w", encoding="utf-8") as f:
json.dump(metrics, f, indent=2)
if metrics.get("ready"):
print("\n=== MT5 Report ===")
for k in ("net_profit", "profit_factor", "total_trades", "sharpe", "max_drawdown", "elapsed_sec"):
if metrics.get(k) is not None:
print(f" {k}: {metrics[k]}")
print(f" report: {metrics.get('report')}")
print(f" saved: {out}")
else:
print("Report not found — check MT5 Tester journal.")
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.