This commit is contained in:
zhutoutoutousan
2026-04-09 11:47:56 +02:00
parent 842a2f8fac
commit b50b430d1a
63 changed files with 12842 additions and 1 deletions
+46
View File
@@ -0,0 +1,46 @@
# XAUUSD H1 — ONNX action model
Same pipeline as **`../xauusd_m15`**, but **H1** bars, **H1-scaled label windows** (~wall-clock parity with M15 defaults), and **`XAUUSD_H1_ActionEA.mq5`**.
## Label scaling (vs M15)
| M15 (bars) | Wall time | H1 (bars) |
|------------|-----------|-----------|
| horizon 32 | ~8 h | 8 |
| local 24 | ~6 h | 6 |
| pullback 20| ~5 h | 5 |
## Setup
1. MT5: **XAUUSD** visible; download **H1** history.
2. Python:
```bash
cd ai/xauusd_h1
pip install -r requirements.txt
python main.py
```
Env: `XAU_SYMBOL`, **`XAU_H1_LOOKBACK`** (default **48**, must match EA **InpLookback**), `XAU_EPOCHS`, `XAU_BATCH`, `SESSION_HOUR_OFFSET`.
3. Copy **`models/XAUUSD_H1_action.onnx`** next to **`XAUUSD_H1_ActionEA.mq5`** (for `#resource` embed) or adjust include path per your workflow.
4. Compile EA on **H1** chart; paste **24** floats into **InpFeatMinStr** / **InpFeatMaxStr** from training stdout.
## Files
| File | Role |
|------|------|
| `main.py` | MT5 H1 fetch, train, `XAUUSD_H1_action.onnx` + meta |
| `labeling.py` | `compute_action_labels` (H1 default horizons) |
| `features.py` | 24-dim features (same order as M15 EA) |
| `XAUUSD_H1_ActionEA.mq5` | Inference + trading |
| `XAUUSD_H1_ActionEA_optimize.set` | Tester optimization skeleton |
Feature semantics: **`../xauusd_m15/FRONTLINE_RSI_INTEGRATION.md`**.
## ONNX
- Input: `[1, lookback, 24]` float32, row **0** = newest bar.
- Output: `[1, 5]` softmax.
Research tooling — not investment advice.
+372
View File
@@ -0,0 +1,372 @@
//+------------------------------------------------------------------+
//| XAUUSD_H1_ActionEA.mq5 |
//| ONNX softmax [5]: HOLD, BUY, SELL_SHORT, CLOSE_LONG, CLOSE_SHORT |
//| 24 features: base 13 + RSI/frontline (see ../xauusd_m15 doc) |
//| Train: ai/xauusd_h1/main.py → XAUUSD_H1_action.onnx |
//| Exits: model CLOSE_* + optional InpTakeProfitATR; adverse ATR |
//+------------------------------------------------------------------+
#property copyright "Profitable EA Project"
#property version "1.00"
#include <Trade\Trade.mqh>
#resource "XAUUSD_H1_action.onnx" as uchar ExtModel[]
#define FEAT_COUNT 24
input group "Model"
input int InpLookback = 48;
// 0 = legacy: p(BUY)>=InpProbBuy etc.; 1 = directional beats HOLD (5-class softmax)
input int InpEntryMode = 1;
input double InpProbBuy = 0.18;
input double InpProbSell = 0.18;
input double InpMinBeatHold = 0.0;
input int InpExitMode = 2;
input double InpProbCloseL = 0.18;
input double InpProbCloseS = 0.18;
input double InpMinCloseBeatHold = 0.0;
input group "Session (match Python SESSION_HOUR_OFFSET)"
input int InpSessionHourOffset = 0;
input group "Scaler: paste 24 floats each from python main.py"
input string InpFeatMinStr = "";
input string InpFeatMaxStr = "";
input group "Risk"
input double InpLotSize = 0.01;
input int InpMagic = 902016;
input int InpSlippage = 30;
input double InpMaxAdverseATR = 2.0;
input double InpTakeProfitATR = 0.0;
double g_feat_min[FEAT_COUNT];
double g_feat_max[FEAT_COUNT];
CTrade trade;
long g_onnx = INVALID_HANDLE;
datetime g_last_bar = 0;
void InitDefaultScalerBounds()
{
double def_min[FEAT_COUNT] = {
0,0,0,0,0,0,-0.05,-0.05,0,-0.02,1.0,0,0.1,
0,0,-1,-0.2,-0.2,0,0,0,0,0,0
};
double def_max[FEAT_COUNT] = {
5000,5000,5000,5000,1,1,0.05,0.05,0.05,0.02,1.02,1,5.0,
1,1,1,0.2,0.2,1,1,1,1,1,1
};
for(int i = 0; i < FEAT_COUNT; i++)
{
g_feat_min[i] = def_min[i];
g_feat_max[i] = def_max[i];
}
}
bool ParseFeatCsv(const string s, double &arr[])
{
if(StringLen(s) < 3) return false;
string parts[];
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("Loaded InpFeatMinStr (24)");
if(StringLen(InpFeatMaxStr) > 0 && ParseFeatCsv(InpFeatMaxStr, g_feat_max))
Print("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;
}
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)
{
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(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);
if(InpExitMode == 1)
return (p3 > p0 + InpMinCloseBeatHold && p3 > p1);
return (p3 > p0 + InpMinCloseBeatHold);
}
bool ModelCloseShort(const double p0, const double p2, const double p4)
{
if(InpExitMode == 0)
return (p4 >= InpProbCloseS);
if(InpExitMode == 1)
return (p4 > p0 + InpMinCloseBeatHold && p4 > p2);
return (p4 > p0 + InpMinCloseBeatHold);
}
void ScaleFeatures(const float &raw[], float &out[])
{
for(int f = 0; f < FEAT_COUNT; f++)
{
double den = g_feat_max[f] - g_feat_min[f];
if(den < 1e-12) den = 1e-12;
double x = (double)raw[f] - g_feat_min[f];
out[f] = (float)MathMax(0.0, MathMin(1.0, x / den));
}
}
bool PrepareMatrix(matrixf &M)
{
int L = InpLookback;
double open[], high[], low[], close[];
long vol[];
datetime bt[];
ArraySetAsSeries(open, true);
ArraySetAsSeries(high, true);
ArraySetAsSeries(low, true);
ArraySetAsSeries(close, true);
ArraySetAsSeries(vol, true);
ArraySetAsSeries(bt, true);
int need = L + 55;
if(CopyOpen(_Symbol, PERIOD_CURRENT, 0, need, open) < L) return false;
if(CopyHigh(_Symbol, PERIOD_CURRENT, 0, need, high) < L) return false;
if(CopyLow(_Symbol, PERIOD_CURRENT, 0, need, low) < L) return false;
if(CopyClose(_Symbol, PERIOD_CURRENT, 0, need, close) < L) return false;
if(CopyTickVolume(_Symbol, PERIOD_CURRENT, 0, need, vol) < L) return false;
if(CopyTime(_Symbol, PERIOD_CURRENT, 0, need, bt) < L) return false;
double rsi7[], rsi14[], rsi21[], ema20[], ema50[], atr[];
ArraySetAsSeries(rsi7, true);
ArraySetAsSeries(rsi14, true);
ArraySetAsSeries(rsi21, true);
ArraySetAsSeries(ema20, true);
ArraySetAsSeries(ema50, true);
ArraySetAsSeries(atr, true);
int h7 = iRSI(_Symbol, PERIOD_CURRENT, 7, PRICE_CLOSE);
int h14 = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
int h21 = iRSI(_Symbol, PERIOD_CURRENT, 21, PRICE_CLOSE);
int hE20 = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_EMA, PRICE_CLOSE);
int hE50 = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_EMA, PRICE_CLOSE);
int hA = iATR(_Symbol, PERIOD_CURRENT, 14);
if(h7 == INVALID_HANDLE || h14 == INVALID_HANDLE || h21 == INVALID_HANDLE ||
hE20 == INVALID_HANDLE || hE50 == INVALID_HANDLE || hA == INVALID_HANDLE)
return false;
if(CopyBuffer(h7, 0, 0, need, rsi7) < L ||
CopyBuffer(h14, 0, 0, need, rsi14) < L ||
CopyBuffer(h21, 0, 0, need, rsi21) < L ||
CopyBuffer(hE20, 0, 0, need, ema20) < L ||
CopyBuffer(hE50, 0, 0, need, ema50) < L ||
CopyBuffer(hA, 0, 0, need, atr) < L)
{
IndicatorRelease(h7); IndicatorRelease(h14); IndicatorRelease(h21);
IndicatorRelease(hE20); IndicatorRelease(hE50); IndicatorRelease(hA);
return false;
}
IndicatorRelease(h7); IndicatorRelease(h14); IndicatorRelease(h21);
IndicatorRelease(hE20); IndicatorRelease(hE50); IndicatorRelease(hA);
M.Resize(L, FEAT_COUNT);
const double RSI_OB = 70.0;
const double RSI_OS = 30.0;
for(int i = 0; i < L; i++)
{
double vma = 0;
int cnt = 0;
for(int k = i; k < i + 20 && k < ArraySize(vol); k++) { vma += (double)vol[k]; cnt++; }
if(cnt < 1) cnt = 1;
vma /= cnt;
double r0 = rsi14[i];
double r1 = (i + 1 < ArraySize(rsi14)) ? rsi14[i + 1] : r0;
double r2 = (i + 2 < ArraySize(rsi14)) ? rsi14[i + 2] : r1;
double rv7 = rsi7[i];
double rv21 = rsi21[i];
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;
double c_ob = (r1 < RSI_OB && r0 >= RSI_OB) ? 1.0 : 0.0;
double c_os = (r1 > RSI_OS && r0 <= RSI_OS) ? 1.0 : 0.0;
double c50u = (r1 < 50.0 && r0 >= 50.0) ? 1.0 : 0.0;
double c50d = (r1 > 50.0 && r0 <= 50.0) ? 1.0 : 0.0;
MqlDateTime st;
TimeToStruct(bt[i], st);
int hr = (st.hour + InpSessionHourOffset) % 24;
if(hr < 0) hr += 24;
double asian = (hr >= 0 && hr < 8) ? 1.0 : 0.0;
float raw[FEAT_COUNT];
raw[0] = (float)open[i];
raw[1] = (float)high[i];
raw[2] = (float)low[i];
raw[3] = (float)close[i];
raw[4] = (float)((double)vol[i] / 1000000.0);
raw[5] = (float)(r0 / 100.0);
raw[6] = (float)((ema20[i] - close[i]) / close[i]);
raw[7] = (float)((ema50[i] - close[i]) / close[i]);
raw[8] = (float)(atr[i] / close[i]);
double pc = (i < L - 1) ? (close[i] - close[i + 1]) / close[i + 1] : 0.0;
raw[9] = (float)pc;
raw[10] = (float)(high[i] / low[i]);
raw[11] = (float)(vma / 1000000.0);
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)spread;
raw[16] = (float)vel;
raw[17] = (float)acc;
raw[18] = (float)dist_mid;
raw[19] = (float)c_ob;
raw[20] = (float)c_os;
raw[21] = (float)c50u;
raw[22] = (float)c50d;
raw[23] = (float)asian;
float sc[FEAT_COUNT];
ScaleFeatures(raw, sc);
for(int j = 0; j < FEAT_COUNT; j++)
M[i][j] = sc[j];
}
return true;
}
void OnTick()
{
datetime t = iTime(_Symbol, PERIOD_CURRENT, 0);
if(t == g_last_bar) return;
g_last_bar = t;
matrixf Min;
if(!PrepareMatrix(Min))
{
Print("PrepareMatrix failed");
return;
}
vectorf out;
out.Resize(5);
if(!OnnxRun(g_onnx, ONNX_NO_CONVERSION, Min, out))
{
Print("OnnxRun failed ", GetLastError());
return;
}
double p0 = out[0], p1 = out[1], p2 = out[2], p3 = out[3], p4 = out[4];
Print("ONNX H1 HOLD=", p0, " BUY=", p1, " SELL=", p2, " CL=", p3, " CS=", p4);
if(!PositionSelect(_Symbol))
{
if(InpEntryMode == 1)
{
double dir = MathMax(p1, p2);
if(dir <= p0 + InpMinBeatHold)
return;
if(p1 >= p2 && p1 > p0 + InpMinBeatHold)
trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "AI H1 BUY");
else if(p2 > p1 && p2 > p0 + InpMinBeatHold)
trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "AI H1 SELL");
}
else
{
if(p1 >= InpProbBuy && p1 >= p2)
trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "AI H1 BUY");
else if(p2 >= InpProbSell && p2 > p1)
trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "AI H1 SELL");
}
return;
}
long typ = (long)PositionGetInteger(POSITION_TYPE);
double opn = PositionGetDouble(POSITION_PRICE_OPEN);
if(AdverseExit(typ, opn))
{
trade.PositionClose(_Symbol);
return;
}
if(ProfitExit(typ, opn))
{
trade.PositionClose(_Symbol);
return;
}
if(typ == POSITION_TYPE_BUY && ModelCloseLong(p0, p1, p3))
trade.PositionClose(_Symbol);
else if(typ == POSITION_TYPE_SELL && ModelCloseShort(p0, p2, p4))
trade.PositionClose(_Symbol);
}
@@ -0,0 +1,24 @@
; XAUUSD_H1_ActionEA — optimization preset (match trained InpLookback to ONNX)
; Copy to MetaQuotes\Terminal\<ID>\MQL5\Profiles\Tester\
;
; Model
InpLookback=48||32||8||96||Y
InpEntryMode=1||0||1||1||Y
InpProbBuy=0.18||0.14||0.02||0.26||Y
InpProbSell=0.18||0.14||0.02||0.26||Y
InpMinBeatHold=0.0||0.0||0.01||0.05||Y
InpExitMode=2||0||1||2||Y
InpProbCloseL=0.18||0.14||0.02||0.26||Y
InpProbCloseS=0.18||0.14||0.02||0.26||Y
InpMinCloseBeatHold=0.0||0.0||0.01||0.04||Y
; Session
InpSessionHourOffset=0||-3||1||3||N
; Scaler
InpFeatMinStr=
InpFeatMaxStr=
; Risk
InpLotSize=0.01||0.01||0.001000||0.100000||N
InpMagic=902016||902016||1||9020160||N
InpSlippage=30||30||1||300||N
InpMaxAdverseATR=2.0||1.0||0.25||3.5||Y
InpTakeProfitATR=0.0||0.0||0.25||3.0||Y
Binary file not shown.
Binary file not shown.
Binary file not shown.
+167
View File
@@ -0,0 +1,167 @@
"""
Feature pipeline: base 13 (EA-compatible) + 11 RSI / session features.
Same 24 dims as XAUUSD M15 EA (see ../xauusd_m15/FRONTLINE_RSI_INTEGRATION.md).
RSI uses Wilder smoothing (ewm alpha=1/period) to align with MT5 iRSI.
"""
from __future__ import annotations
import os
import numpy as np
import pandas as pd
NUM_BASE_FEATURES = 13
NUM_RSI_EXTRA = 11
NUM_FEATURES = NUM_BASE_FEATURES + NUM_RSI_EXTRA # 24
RSI_OVERBOUGHT = 70.0
RSI_OVERSOLD = 30.0
def wilder_rsi(close: pd.Series, period: int) -> np.ndarray:
"""Wilder RSI (matches MetaTrader iRSI closely)."""
delta = close.diff()
gain = delta.clip(lower=0.0)
loss = (-delta).clip(lower=0.0)
avg_g = gain.ewm(alpha=1.0 / period, min_periods=period, adjust=False).mean()
avg_l = loss.ewm(alpha=1.0 / period, min_periods=period, adjust=False).mean()
rs = avg_g / avg_l.replace(0, np.nan)
rsi = 100.0 - (100.0 / (1.0 + rs))
return rsi.fillna(50.0).to_numpy(dtype=np.float64)
def prepare_features_full(
df: pd.DataFrame,
*,
session_hour_offset: int | None = None,
) -> pd.DataFrame:
"""
Build (N, 24) feature table, chronological index matching df.
Drops first ~50 rows (warmup).
"""
if session_hour_offset is None:
session_hour_offset = int(os.environ.get("SESSION_HOUR_OFFSET", "0"))
o = df["open"].to_numpy(dtype=np.float64)
h = df["high"].to_numpy(dtype=np.float64)
l = df["low"].to_numpy(dtype=np.float64)
c = df["close"].astype(float)
vol = df["tick_volume"].to_numpy(dtype=np.float64)
n = len(df)
idx = df.index
rsi7 = wilder_rsi(c, 7)
rsi14 = wilder_rsi(c, 14)
rsi21 = wilder_rsi(c, 21)
ema20 = c.ewm(span=20, adjust=False).mean().to_numpy()
ema50 = c.ewm(span=50, adjust=False).mean().to_numpy()
tr = np.maximum(
h - l,
np.maximum(np.abs(h - np.roll(c.to_numpy(), 1)), np.abs(l - np.roll(c.to_numpy(), 1))),
)
tr[0] = h[0] - l[0]
atr = pd.Series(tr).rolling(14).mean().to_numpy()
vol_ma = np.zeros(n)
for j in range(n):
s = 0.0
cnt = 0
for k in range(j, min(j + 20, n)):
s += vol[k]
cnt += 1
vol_ma[j] = s / cnt if cnt else vol[j]
pc_ea = np.zeros(n)
cvals = c.to_numpy()
for j in range(1, n):
den = cvals[j - 1]
pc_ea[j] = (cvals[j] - den) / den if den else 0.0
hours = np.zeros(n, dtype=np.int32)
for j in range(n):
ts = idx[j]
try:
hts = int(ts.hour)
except Exception:
hts = 0
hours[j] = (hts + session_hour_offset) % 24
rows = []
for j in range(n):
r0 = rsi14[j]
r1 = rsi14[j - 1] if j > 0 else r0
r2 = rsi14[j - 2] if j > 1 else r1
spread = np.clip((r0 - rsi7[j]) / 50.0, -1.0, 1.0)
vel = (r0 - r1) / 25.0
acc = ((r0 - r1) - (r1 - r2)) / 25.0
dist_mid = abs(r0 - 50.0) / 50.0
cross_ob = 1.0 if (r1 < RSI_OVERBOUGHT and r0 >= RSI_OVERBOUGHT) else 0.0
cross_os = 1.0 if (r1 > RSI_OVERSOLD and r0 <= RSI_OVERSOLD) else 0.0
cross_50_up = 1.0 if (r1 < 50.0 and r0 >= 50.0) else 0.0
cross_50_dn = 1.0 if (r1 > 50.0 and r0 <= 50.0) else 0.0
asian = 1.0 if (0 <= hours[j] < 8) else 0.0
rows.append(
[
float(o[j]),
float(h[j]),
float(l[j]),
float(cvals[j]),
float(vol[j] / 1_000_000.0),
float(rsi14[j] / 100.0),
float((ema20[j] - cvals[j]) / cvals[j]) if cvals[j] else 0.0,
float((ema50[j] - cvals[j]) / cvals[j]) if cvals[j] else 0.0,
float(atr[j] / cvals[j]) if cvals[j] else 0.0,
float(pc_ea[j]),
float(h[j] / l[j]) if l[j] else 1.0,
float(vol_ma[j] / 1_000_000.0),
float(vol[j] / vol_ma[j]) if vol_ma[j] > 0 else 1.0,
float(rsi7[j] / 100.0),
float(rsi21[j] / 100.0),
float(spread),
float(vel),
float(acc),
float(dist_mid),
float(cross_ob),
float(cross_os),
float(cross_50_up),
float(cross_50_dn),
float(asian),
]
)
cols = [
"open",
"high",
"low",
"close",
"tick_volume",
"rsi",
"ema20_n",
"ema50_n",
"atr_n",
"price_change",
"high_low_ratio",
"volume_ma",
"volume_ratio",
"rsi7_n",
"rsi21_n",
"rsi_fast_slow_spread",
"rsi_velocity",
"rsi_accel",
"rsi_dist_mid_50",
"rsi_cross_overbought",
"rsi_cross_oversold",
"rsi_cross_50_up",
"rsi_cross_50_down",
"session_asian_utc",
]
out = pd.DataFrame(rows, index=idx, columns=cols)
return out.iloc[50:].copy()
+128
View File
@@ -0,0 +1,128 @@
"""
Buy-low / sell-high style labels for OHLCV bars (no fixed SL/TP in labels).
H1 defaults scale M15 bar counts to ~similar wall-clock horizons:
M15 horizon=32 -> 8h -> H1 horizon=8
M15 local=24 -> 6h -> H1 local=6
M15 pullback=20 -> 5h -> H1 pullback=5
Classes (integer, matches EA):
0 HOLD
1 BUY — forward upside vs ATR + local swing low
2 SELL_SHORT — forward downside vs ATR + local swing high
3 CLOSE_LONG — past-only: pullback from recent range high
4 CLOSE_SHORT — past-only: bounce from recent range low
"""
from __future__ import annotations
import numpy as np
import pandas as pd
def atr_series(df: pd.DataFrame, period: int = 14) -> pd.Series:
high, low, close = df["high"], df["low"], df["close"]
tr = pd.concat(
[
high - low,
(high - close.shift()).abs(),
(low - close.shift()).abs(),
],
axis=1,
).max(axis=1)
return tr.rolling(period).mean()
def compute_action_labels(
df: pd.DataFrame,
*,
horizon: int = 8,
local_window: int = 6,
pullback_window: int = 5,
k_forward_atr: float = 0.75,
local_pct: float = 0.28,
pullback_mult: float = 0.55,
trend_mult: float = 1.05,
) -> pd.Series:
"""
Return a Series of int labels 0..4 aligned to df index.
Last `horizon` rows → HOLD (no forward path for buy/sell scoring).
"""
close = df["close"].values
high = df["high"].values
low = df["low"].values
n = len(df)
atr = atr_series(df, 14).values
labels = np.zeros(n, dtype=np.int64)
lw = local_window
pw = pullback_window
need = max(lw, pw) + 2
for t in range(n):
if t < need or t >= n - horizon:
labels[t] = 0
continue
a = atr[t]
if not np.isfinite(a) or a <= 0:
a = close[t] * 1e-4
sl = low[t + 1 : t + horizon + 1]
sh = high[t + 1 : t + horizon + 1]
fwd_max = float(np.max(sh))
fwd_min = float(np.min(sl))
up_move = (fwd_max - close[t]) / a
down_move = (close[t] - fwd_min) / a
loc_low = float(np.min(low[t - lw : t + 1]))
loc_high = float(np.max(high[t - lw : t + 1]))
rng = max(loc_high - loc_low, a * 0.15)
near_low = (close[t] - loc_low) / rng <= local_pct
near_high = (loc_high - close[t]) / rng <= local_pct
buy_sig = near_low and (up_move >= k_forward_atr) and (up_move >= down_move * 0.85)
sell_sig = near_high and (down_move >= k_forward_atr) and (down_move > up_move * 1.05)
seg_h = high[t - pw : t + 1]
seg_l = low[t - pw : t + 1]
rh = float(np.max(seg_h))
rl = float(np.min(seg_l))
range_atr = (rh - rl) / a
pull_from_high = (rh - close[t]) / a
bounce_from_low = (close[t] - rl) / a
exit_long = (
range_atr >= trend_mult
and pull_from_high >= pullback_mult
and close[t] < close[t - 1]
)
exit_short = (
range_atr >= trend_mult
and bounce_from_low >= pullback_mult
and close[t] > close[t - 1]
)
if exit_long and not buy_sig:
labels[t] = 3
elif exit_short and not sell_sig:
labels[t] = 4
elif buy_sig and not sell_sig:
labels[t] = 1
elif sell_sig and not buy_sig:
labels[t] = 2
elif buy_sig and sell_sig:
labels[t] = 1 if up_move >= down_move else 2
else:
labels[t] = 0
return pd.Series(labels, index=df.index, name="action_label")
def class_weights(y: np.ndarray, n_classes: int = 5) -> dict[int, float]:
from sklearn.utils.class_weight import compute_class_weight
y_int = y.astype(int)
classes = np.arange(n_classes)
cw = compute_class_weight("balanced", classes=classes, y=y_int)
return {i: float(cw[i]) for i in range(n_classes)}
+209
View File
@@ -0,0 +1,209 @@
"""
XAUUSD H1 — ONNX action model (buy / sell short / close long / close short / hold).
Same 24 features as M15 stack; labels use H1-scaled horizons (~wall-clock parity with M15).
Row order matches XAUUSD_H1_ActionEA.mq5 (row 0 = newest bar).
Data: MT5, 20082026 (limited by downloaded history).
"""
from __future__ import annotations
import json
import os
import pickle
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from tensorflow import keras
from tensorflow.keras import layers
from tqdm import tqdm
import tf2onnx
import onnx
from labeling import class_weights, compute_action_labels
from features import NUM_FEATURES, prepare_features_full
NUM_CLASSES = 5
CLASS_NAMES = ["HOLD", "BUY", "SELL_SHORT", "CLOSE_LONG", "CLOSE_SHORT"]
def fetch_mt5_range(
symbol: str,
timeframe: int,
start_date: datetime,
end_date: datetime,
) -> pd.DataFrame:
if not mt5.initialize():
raise RuntimeError(f"MT5 init failed: {mt5.last_error()}")
info = mt5.symbol_info(symbol)
if info is None:
mt5.shutdown()
raise ValueError(f"Symbol {symbol} not found")
if not info.visible and not mt5.symbol_select(symbol, True):
mt5.shutdown()
raise ValueError(f"Cannot select {symbol}")
all_rows: list[dict] = []
chunk_days = 120
cur = start_date
while cur < end_date:
chunk_end = min(cur + timedelta(days=chunk_days), end_date)
rates = mt5.copy_rates_range(symbol, timeframe, cur, chunk_end)
if rates is not None and len(rates) > 1:
for row in rates:
all_rows.append({n: row[n] for n in rates.dtype.names})
cur = chunk_end
if not all_rows:
mt5.shutdown()
raise ValueError("No rates returned — download XAUUSD H1 in MT5 History Center")
df = pd.DataFrame(all_rows)
df["time"] = pd.to_datetime(df["time"], unit="s")
df = df.set_index("time").sort_index()
df = df[~df.index.duplicated(keep="first")]
return df
def create_sequences(
X: np.ndarray, y: np.ndarray, lookback: int
) -> tuple[np.ndarray, np.ndarray]:
xs, ys = [], []
for i in tqdm(range(lookback - 1, len(X)), desc="sequences"):
window = X[i - lookback + 1 : i + 1].copy()
window = window[::-1]
xs.append(window)
ys.append(y[i])
return np.asarray(xs, dtype=np.float32), np.asarray(ys, dtype=np.int64)
def build_model(lookback: int, n_feat: int) -> keras.Model:
inp = layers.Input(shape=(lookback, n_feat))
x = layers.LSTM(96, return_sequences=True)(inp)
x = layers.Dropout(0.25)(x)
x = layers.LSTM(48)(x)
x = layers.Dropout(0.25)(x)
x = layers.Dense(32, activation="relu")(x)
out = layers.Dense(NUM_CLASSES, activation="softmax", name="action_probs")(x)
model = keras.Model(inp, out)
model.compile(
optimizer=keras.optimizers.Adam(1e-3),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
return model
def main() -> int:
symbol = os.environ.get("XAU_SYMBOL", "XAUUSD")
lookback = int(os.environ.get("XAU_H1_LOOKBACK", os.environ.get("XAU_LOOKBACK", "48")))
epochs = int(os.environ.get("XAU_EPOCHS", "40"))
batch_size = int(os.environ.get("XAU_BATCH", "64"))
start_date = datetime(2008, 1, 1)
end_date = datetime(2026, 12, 31)
out_dir = os.path.join(os.path.dirname(__file__), "models")
os.makedirs(out_dir, exist_ok=True)
onnx_path = os.path.join(out_dir, f"{symbol}_H1_action.onnx")
meta_path = os.path.join(out_dir, f"{symbol}_H1_action_meta.json")
print("Fetching MT5 H1 data …")
try:
raw = fetch_mt5_range(symbol, mt5.TIMEFRAME_H1, start_date, end_date)
finally:
mt5.shutdown()
print(f"Bars: {len(raw)} range: {raw.index[0]}{raw.index[-1]}")
feat = prepare_features_full(raw)
labels_full = compute_action_labels(raw)
labels = labels_full.loc[feat.index]
y = labels.loc[feat.index].values.astype(np.int64)
X_raw = feat.values.astype(np.float32)
valid = np.isfinite(X_raw).all(axis=1) & (y >= 0) & (y < NUM_CLASSES)
X_raw = X_raw[valid]
y = y[valid]
print("Label counts:", {CLASS_NAMES[i]: int((y == i).sum()) for i in range(NUM_CLASSES)})
scaler = MinMaxScaler()
Xn = scaler.fit_transform(X_raw).astype(np.float32)
X_seq, y_seq = create_sequences(Xn, y, lookback)
if len(X_seq) < 500:
print("ERROR: Too few sequences — need more H1 history in MT5.")
return 1
X_train, X_val, y_train, y_val = train_test_split(
X_seq, y_seq, test_size=0.15, shuffle=False
)
cw = class_weights(y_train, NUM_CLASSES)
sample_w = np.array([cw[int(c)] for c in y_train], dtype=np.float32)
model = build_model(lookback, NUM_FEATURES)
model.summary()
model.fit(
X_train,
y_train,
sample_weight=sample_w,
validation_data=(X_val, y_val),
epochs=epochs,
batch_size=batch_size,
verbose=1,
callbacks=[
keras.callbacks.EarlyStopping(
monitor="val_loss", patience=8, restore_best_weights=True
),
keras.callbacks.ReduceLROnPlateau(
monitor="val_loss", factor=0.5, patience=4, min_lr=1e-6
),
],
)
spec = (tf.TensorSpec((None, lookback, NUM_FEATURES), tf.float32, name="input"),)
onnx_m, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=13)
onnx.save_model(onnx_m, onnx_path)
with open(onnx_path.replace(".onnx", "_scaler.pkl"), "wb") as f:
pickle.dump(scaler, f)
meta = {
"symbol": symbol,
"timeframe": "H1",
"lookback": lookback,
"num_features": int(NUM_FEATURES),
"feature_columns": feat.columns.tolist(),
"num_classes": NUM_CLASSES,
"class_names": CLASS_NAMES,
"label_horizon_bars": 8,
"label_note": "H1 labeling defaults: horizon=8, local=6, pullback=5 (~M15 wall-clock parity)",
"scaler_feature_min": scaler.data_min_.tolist(),
"scaler_feature_max": scaler.data_max_.tolist(),
"scaler_scale": scaler.scale_.tolist() if hasattr(scaler, "scale_") else None,
"notes": "MinMax in EA; row0=newest. Match EA InpLookback to lookback here.",
}
with open(meta_path, "w", encoding="utf-8") as f:
json.dump(meta, f, indent=2)
print(f"Saved: {onnx_path}")
print(f"Meta: {meta_path}")
print("\n--- Paste into EA InpFeatMinStr / InpFeatMaxStr (comma-separated, %d floats each) ---" % NUM_FEATURES)
print(",".join(f"{x:.8g}" for x in scaler.data_min_))
print(",".join(f"{x:.8g}" for x in scaler.data_max_))
print(f"\nSet EA InpLookback = {lookback} (must match ONNX input dim).")
return 0
if __name__ == "__main__":
sys.exit(main())
Binary file not shown.
@@ -0,0 +1,121 @@
{
"symbol": "XAUUSD",
"timeframe": "H1",
"lookback": 48,
"num_features": 24,
"feature_columns": [
"open",
"high",
"low",
"close",
"tick_volume",
"rsi",
"ema20_n",
"ema50_n",
"atr_n",
"price_change",
"high_low_ratio",
"volume_ma",
"volume_ratio",
"rsi7_n",
"rsi21_n",
"rsi_fast_slow_spread",
"rsi_velocity",
"rsi_accel",
"rsi_dist_mid_50",
"rsi_cross_overbought",
"rsi_cross_oversold",
"rsi_cross_50_up",
"rsi_cross_50_down",
"session_asian_utc"
],
"num_classes": 5,
"class_names": [
"HOLD",
"BUY",
"SELL_SHORT",
"CLOSE_LONG",
"CLOSE_SHORT"
],
"label_horizon_bars": 8,
"label_note": "H1 labeling defaults: horizon=8, local=6, pullback=5 (~M15 wall-clock parity)",
"scaler_feature_min": [
679.5499877929688,
735.0499877929688,
679.5499877929688,
711.2999877929688,
0.0,
0.0778568685054779,
-0.08346110582351685,
-0.13061486184597015,
0.0006287021678872406,
-0.09134025126695633,
1.0,
0.0018113000551238656,
0.0,
0.020070146769285202,
0.1127406507730484,
-0.45013511180877686,
-1.6049232482910156,
-2.0694637298583984,
1.0986201232299209e-05,
0.0,
0.0,
0.0,
0.0,
0.0
],
"scaler_feature_max": [
5562.419921875,
5598.06005859375,
5554.68994140625,
5562.43994140625,
0.15629400312900543,
0.9388294816017151,
0.1516682505607605,
0.17963257431983948,
0.07575831562280655,
0.10734681040048599,
1.138908863067627,
0.11270634829998016,
11.032988548278809,
0.9843139052391052,
0.8853746056556702,
0.44974473118782043,
1.5471272468566895,
2.1628992557525635,
0.8776589632034302,
1.0,
1.0,
1.0,
1.0,
1.0
],
"scaler_scale": [
0.00020479758677538484,
0.00020563394355122,
0.00020512231276370585,
0.00020613710512407124,
6.398198127746582,
1.1614770889282227,
4.2529778480529785,
3.223233938217163,
13.310330390930176,
5.033040523529053,
7.198964595794678,
9.017535209655762,
0.09063727408647537,
1.0370821952819824,
1.2942739725112915,
1.1112594604492188,
0.31725379824638367,
0.23627464473247528,
1.1394089460372925,
1.0,
1.0,
1.0,
1.0,
1.0
],
"notes": "MinMax in EA; row0=newest. Match EA InpLookback to lookback here."
}
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
numpy>=1.23
pandas>=2.0
MetaTrader5>=5.0.45
tensorflow>=2.14
tf2onnx>=1.16
onnx>=1.15
scikit-learn>=1.3
tqdm>=4.66
+21
View File
@@ -0,0 +1,21 @@
"""
Dynamic adverse risk (conceptual mirror of EA InpMaxAdverseATR).
"""
from __future__ import annotations
import numpy as np
def adverse_hit_long(
entry: float,
low_path: np.ndarray,
atr_path: np.ndarray,
max_adverse_atr: float,
) -> int | None:
for i in range(len(low_path)):
atr = max(atr_path[i], entry * 1e-6)
adv = (entry - low_path[i]) / atr
if adv >= max_adverse_atr:
return i
return None
@@ -0,0 +1,43 @@
# Frontline RSI 经验 → `ai/xauusd_m15` 特征映射
本文把 `frontline/MQL5/_united/Strategies` 里与 RSI 相关的**可量化**逻辑,映射到训练用的 **24 维特征**(前 13 维与原版 EA 一致,后 11 维为 RSI/时段扩展)。
## 策略来源与特征对应
| Frontline 模块 | 经验要点 | 模型中的体现 |
|----------------|----------|----------------|
| **RSIReversalAsianStrategy** | 上穿超买 / 下穿超卖的**交叉**;亚洲时段(UTC 0–8)语境 | `rsi_cross_overbought` / `rsi_cross_oversold`(默认 70/30);`session_asian_utc` |
| **RSICrossOverReversalStrategy** | 超买/超卖区附近的**反转入场**、RSI 退出位 | 交叉特征 + `rsi_velocity` / `rsi_accel` 描述短期摆动 |
| **RSIScalpingStrategy** | 极值区外的**回升/回落**(多根 RSI 结构) | `rsi_velocity``rsi_accel`3 根 RSI14 近似) |
| **RSIMidPointHijackStrategy** | 相对 **50** 中轴、快慢 RSI 状态 | `rsi_dist_mid_50``rsi_fast_slow_spread`RSI14 vs RSI7 |
| **多品种 RSI Scalping** | 更短周期敏感 | `rsi7_n`(快周期)、`rsi21_n`(慢周期) |
## 特征索引(与 Python / EA 顺序一致)
| 索引 | 名称 | 说明 |
|------|------|------|
| 04 | OHLC + tick_volume | 与原版一致 |
| 5 | rsi | Wilder RSI(14)/100 |
| 612 | EMA/ATR/价量 | 与原版一致 |
| 13 | rsi7_n | RSI(7)/100 |
| 14 | rsi21_n | RSI(21)/100 |
| 15 | rsi_fast_slow_spread | clip((RSI14RSI7)/50, 1, 1) |
| 16 | rsi_velocity | (RSI14₀−RSI14₁)/25 |
| 17 | rsi_accel | ((RSI14₀−RSI14₁)(RSI14₁−RSI14₂))/25 |
| 18 | rsi_dist_mid_50 | \|RSI1450\|/50 |
| 1922 | cross_* | 0/1,与 frontline 交叉定义一致(上一根→当前根) |
| 23 | session_asian_utc | 小时经偏移后 ∈ [0,8) 则为 1 |
## 时段偏移
MT5 K 线时间多为**服务器时区**。若要与 UTC 亚洲窗对齐,训练时设环境变量 `SESSION_HOUR_OFFSET`EA 使用 `InpSessionHourOffset`,使 `(hour + offset) % 24` 与你在回测里认定的 UTC 一致。
## 未直接编码的规则(可后续扩展)
- **点差、最大持仓时长、Magic 分策略**:可作为额外标量特征或单独过滤层。
- **RSIMidPoint 的「先标记超买再下穿退出线」**:可用连续两 bar 的 cross 组合特征或 LSTM 隐式学习;当前用 cross + dist_mid 近似。
- **Darvas / EMA 等非 RSI 策略**:未并入本 ONNX 特征;可在 `features.py` 中追加列并同步改 `NUM_FEATURES` 与 EA。
## 再训练提醒
修改 `NUM_FEATURES` 后必须:**重新导出 ONNX**、更新 EA 中 `#resource` 模型、`OnnxSetInputShape` 第三维、**24 个 scaler min/max**。
+49
View File
@@ -0,0 +1,49 @@
# XAUUSD M15 — ONNX action model (buy / sell / close)
## What it does
- Pulls **XAUUSD** (**M15**) from **MetaTrader 5** (20082026 requested; actual range depends on History Center).
- **24 features**: 13 legacy OHLC/EMA/ATR/volume + **11 RSI / session** features aligned with **frontline** strategies (crosses, velocity, RSI7/21, Asian window). See **`FRONTLINE_RSI_INTEGRATION.md`**.
- Labels: **buy-low / sell-high** (forward window) + **close-long / close-short** (past-only). RSI enters as **inputs**, not as hard-coded label rules.
- Trains **LSTM → softmax(5)**: `HOLD`, `BUY`, `SELL_SHORT`, `CLOSE_LONG`, `CLOSE_SHORT`.
- Exports **`models/XAUUSD_M15_action.onnx`** + scaler + **`XAUUSD_M15_action_meta.json`** (includes `feature_columns`).
- **EA**: **SL=0, TP=0**; **InpMaxAdverseATR**; **InpSessionHourOffset** should match training `SESSION_HOUR_OFFSET` for Asian flag.
This is research tooling — not investment advice. Past labels do not guarantee live performance.
## Setup
1. MT5 installed, logged in, **XAUUSD** visible; download **M15** history (Tools → History Center or chart scroll).
2. Python 3.10+:
```bash
cd ai/xauusd_m15
pip install -r requirements.txt
python main.py
```
Optional env: `XAU_SYMBOL`, `XAU_LOOKBACK` (default 64), `XAU_EPOCHS`, `XAU_BATCH`, `SESSION_HOUR_OFFSET` (Asian session hour alignment vs server time).
3. Copy `models/XAUUSD_M15_action.onnx` to **`MQL5/Files/`** (same path as `#resource` in the EA).
4. Open `XAUUSD_M15_ActionEA.mq5` in MetaEditor; compile.
5. Paste two lines from training stdout into **InpFeatMinStr** and **InpFeatMaxStr** (comma-separated **24** floats each).
## ONNX I/O
- Input: `[1, lookback, 24]` float32, **row 0 = newest bar**.
- Output: `[1, 5]` softmax probabilities.
## Files
| File | Role |
|------|------|
| `main.py` | Fetch, features, labels, train, ONNX + meta |
| `features.py` | 24-dim pipeline + Wilder RSI |
| `FRONTLINE_RSI_INTEGRATION.md` | frontline 策略 → 特征对照 |
| `labeling.py` | `compute_action_labels` |
| `risk_controls.py` | Adverse ATR helper for Python backtests |
| `XAUUSD_M15_ActionEA.mq5` | Live inference + trading skeleton |
## Tuning labels
Edit parameters in `labeling.compute_action_labels()` (`horizon`, `k_forward_atr`, `pullback_mult`, etc.) and retrain.
+375
View File
@@ -0,0 +1,375 @@
//+------------------------------------------------------------------+
//| XAUUSD_M15_ActionEA.mq5 |
//| ONNX softmax [5]: HOLD, BUY, SELL_SHORT, CLOSE_LONG, CLOSE_SHORT |
//| 24 features: base 13 + RSI/frontline (see FRONTLINE_RSI_*.md) |
//| Exits: model CLOSE_* + optional InpTakeProfitATR; adverse ATR |
//+------------------------------------------------------------------+
#property copyright "Profitable EA Project"
#property version "1.03"
#include <Trade\Trade.mqh>
#resource "XAUUSD_M15_action.onnx" as uchar ExtModel[]
#define FEAT_COUNT 24
input group "Model"
input int InpLookback = 64;
// 0 = legacy: require p(BUY)>=InpProbBuy and p(SELL)>=InpProbSell (use ~0.18 for 5-class softmax)
// 1 = default: open only when directional prob beats HOLD (typical 5-way outputs ~0.150.25 each)
input int InpEntryMode = 1;
input double InpProbBuy = 0.18;
input double InpProbSell = 0.18;
input double InpMinBeatHold = 0.0; // mode 1: require max(p1,p2)-p0 >= this (e.g. 0.02)
// Exit: 0 = p3/p4 >= thresholds (use ~0.18 for 5-class); 1 = CLOSE beats HOLD and beats add (p3>p1 / p4>p2)
// 2 = default: CLOSE beats HOLD only (lets winners exit when pullback signal > hold; still weak in trends)
input int InpExitMode = 2;
input double InpProbCloseL = 0.18;
input double InpProbCloseS = 0.18;
input double InpMinCloseBeatHold = 0.0; // exit modes 12: require p3/p4 > p0 + this
input group "Session (match Python SESSION_HOUR_OFFSET)"
input int InpSessionHourOffset = 0; // add to bar hour so Asian 08 matches training
input group "Scaler: paste 24 floats each from python main.py"
input string InpFeatMinStr = "";
input string InpFeatMaxStr = "";
input group "Risk"
input double InpLotSize = 0.01;
input int InpMagic = 902015;
input int InpSlippage = 30;
input double InpMaxAdverseATR = 2.0;
input double InpTakeProfitATR = 0.0; // >0: close in profit when price move >= this * ATR(14) (banks winners)
double g_feat_min[FEAT_COUNT];
double g_feat_max[FEAT_COUNT];
CTrade trade;
long g_onnx = INVALID_HANDLE;
datetime g_last_bar = 0;
void InitDefaultScalerBounds()
{
double def_min[FEAT_COUNT] = {
0,0,0,0,0,0,-0.05,-0.05,0,-0.02,1.0,0,0.1,
0,0,-1,-0.2,-0.2,0,0,0,0,0,0
};
double def_max[FEAT_COUNT] = {
5000,5000,5000,5000,1,1,0.05,0.05,0.05,0.02,1.02,1,5.0,
1,1,1,0.2,0.2,1,1,1,1,1,1
};
for(int i = 0; i < FEAT_COUNT; i++)
{
g_feat_min[i] = def_min[i];
g_feat_max[i] = def_max[i];
}
}
bool ParseFeatCsv(const string s, double &arr[])
{
if(StringLen(s) < 3) return false;
string parts[];
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("Loaded InpFeatMinStr (24)");
if(StringLen(InpFeatMaxStr) > 0 && ParseFeatCsv(InpFeatMaxStr, g_feat_max))
Print("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;
}
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)
{
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(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);
if(InpExitMode == 1)
return (p3 > p0 + InpMinCloseBeatHold && p3 > p1);
// mode 2: close-long probability beats hold (trends can still keep p1 high; use InpTakeProfitATR then)
return (p3 > p0 + InpMinCloseBeatHold);
}
bool ModelCloseShort(const double p0, const double p2, const double p4)
{
if(InpExitMode == 0)
return (p4 >= InpProbCloseS);
if(InpExitMode == 1)
return (p4 > p0 + InpMinCloseBeatHold && p4 > p2);
return (p4 > p0 + InpMinCloseBeatHold);
}
void ScaleFeatures(const float &raw[], float &out[])
{
for(int f = 0; f < FEAT_COUNT; f++)
{
double den = g_feat_max[f] - g_feat_min[f];
if(den < 1e-12) den = 1e-12;
double x = (double)raw[f] - g_feat_min[f];
out[f] = (float)MathMax(0.0, MathMin(1.0, x / den));
}
}
bool PrepareMatrix(matrixf &M)
{
int L = InpLookback;
double open[], high[], low[], close[];
long vol[];
datetime bt[];
ArraySetAsSeries(open, true);
ArraySetAsSeries(high, true);
ArraySetAsSeries(low, true);
ArraySetAsSeries(close, true);
ArraySetAsSeries(vol, true);
ArraySetAsSeries(bt, true);
int need = L + 55;
if(CopyOpen(_Symbol, PERIOD_CURRENT, 0, need, open) < L) return false;
if(CopyHigh(_Symbol, PERIOD_CURRENT, 0, need, high) < L) return false;
if(CopyLow(_Symbol, PERIOD_CURRENT, 0, need, low) < L) return false;
if(CopyClose(_Symbol, PERIOD_CURRENT, 0, need, close) < L) return false;
if(CopyTickVolume(_Symbol, PERIOD_CURRENT, 0, need, vol) < L) return false;
if(CopyTime(_Symbol, PERIOD_CURRENT, 0, need, bt) < L) return false;
double rsi7[], rsi14[], rsi21[], ema20[], ema50[], atr[];
ArraySetAsSeries(rsi7, true);
ArraySetAsSeries(rsi14, true);
ArraySetAsSeries(rsi21, true);
ArraySetAsSeries(ema20, true);
ArraySetAsSeries(ema50, true);
ArraySetAsSeries(atr, true);
int h7 = iRSI(_Symbol, PERIOD_CURRENT, 7, PRICE_CLOSE);
int h14 = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
int h21 = iRSI(_Symbol, PERIOD_CURRENT, 21, PRICE_CLOSE);
int hE20 = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_EMA, PRICE_CLOSE);
int hE50 = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_EMA, PRICE_CLOSE);
int hA = iATR(_Symbol, PERIOD_CURRENT, 14);
if(h7 == INVALID_HANDLE || h14 == INVALID_HANDLE || h21 == INVALID_HANDLE ||
hE20 == INVALID_HANDLE || hE50 == INVALID_HANDLE || hA == INVALID_HANDLE)
return false;
if(CopyBuffer(h7, 0, 0, need, rsi7) < L ||
CopyBuffer(h14, 0, 0, need, rsi14) < L ||
CopyBuffer(h21, 0, 0, need, rsi21) < L ||
CopyBuffer(hE20, 0, 0, need, ema20) < L ||
CopyBuffer(hE50, 0, 0, need, ema50) < L ||
CopyBuffer(hA, 0, 0, need, atr) < L)
{
IndicatorRelease(h7); IndicatorRelease(h14); IndicatorRelease(h21);
IndicatorRelease(hE20); IndicatorRelease(hE50); IndicatorRelease(hA);
return false;
}
IndicatorRelease(h7); IndicatorRelease(h14); IndicatorRelease(h21);
IndicatorRelease(hE20); IndicatorRelease(hE50); IndicatorRelease(hA);
M.Resize(L, FEAT_COUNT);
const double RSI_OB = 70.0;
const double RSI_OS = 30.0;
for(int i = 0; i < L; i++)
{
double vma = 0;
int cnt = 0;
for(int k = i; k < i + 20 && k < ArraySize(vol); k++) { vma += (double)vol[k]; cnt++; }
if(cnt < 1) cnt = 1;
vma /= cnt;
double r0 = rsi14[i];
double r1 = (i + 1 < ArraySize(rsi14)) ? rsi14[i + 1] : r0;
double r2 = (i + 2 < ArraySize(rsi14)) ? rsi14[i + 2] : r1;
double rv7 = rsi7[i];
double rv21 = rsi21[i];
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;
double c_ob = (r1 < RSI_OB && r0 >= RSI_OB) ? 1.0 : 0.0;
double c_os = (r1 > RSI_OS && r0 <= RSI_OS) ? 1.0 : 0.0;
double c50u = (r1 < 50.0 && r0 >= 50.0) ? 1.0 : 0.0;
double c50d = (r1 > 50.0 && r0 <= 50.0) ? 1.0 : 0.0;
MqlDateTime st;
TimeToStruct(bt[i], st);
int hr = (st.hour + InpSessionHourOffset) % 24;
if(hr < 0) hr += 24;
double asian = (hr >= 0 && hr < 8) ? 1.0 : 0.0;
float raw[FEAT_COUNT];
raw[0] = (float)open[i];
raw[1] = (float)high[i];
raw[2] = (float)low[i];
raw[3] = (float)close[i];
raw[4] = (float)((double)vol[i] / 1000000.0);
raw[5] = (float)(r0 / 100.0);
raw[6] = (float)((ema20[i] - close[i]) / close[i]);
raw[7] = (float)((ema50[i] - close[i]) / close[i]);
raw[8] = (float)(atr[i] / close[i]);
double pc = (i < L - 1) ? (close[i] - close[i + 1]) / close[i + 1] : 0.0;
raw[9] = (float)pc;
raw[10] = (float)(high[i] / low[i]);
raw[11] = (float)(vma / 1000000.0);
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)spread;
raw[16] = (float)vel;
raw[17] = (float)acc;
raw[18] = (float)dist_mid;
raw[19] = (float)c_ob;
raw[20] = (float)c_os;
raw[21] = (float)c50u;
raw[22] = (float)c50d;
raw[23] = (float)asian;
float sc[FEAT_COUNT];
ScaleFeatures(raw, sc);
for(int j = 0; j < FEAT_COUNT; j++)
M[i][j] = sc[j];
}
return true;
}
void OnTick()
{
datetime t = iTime(_Symbol, PERIOD_CURRENT, 0);
if(t == g_last_bar) return;
g_last_bar = t;
matrixf Min;
if(!PrepareMatrix(Min))
{
Print("PrepareMatrix failed");
return;
}
vectorf out;
out.Resize(5);
if(!OnnxRun(g_onnx, ONNX_NO_CONVERSION, Min, out))
{
Print("OnnxRun failed ", GetLastError());
return;
}
double p0 = out[0], p1 = out[1], p2 = out[2], p3 = out[3], p4 = out[4];
Print("ONNX HOLD=", p0, " BUY=", p1, " SELL=", p2, " CL=", p3, " CS=", p4);
if(!PositionSelect(_Symbol))
{
if(InpEntryMode == 1)
{
double dir = MathMax(p1, p2);
if(dir <= p0 + InpMinBeatHold)
return;
if(p1 >= p2 && p1 > p0 + InpMinBeatHold)
trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "AI BUY");
else if(p2 > p1 && p2 > p0 + InpMinBeatHold)
trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "AI SELL");
}
else
{
if(p1 >= InpProbBuy && p1 >= p2)
trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "AI BUY");
else if(p2 >= InpProbSell && p2 > p1)
trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "AI SELL");
}
return;
}
long typ = (long)PositionGetInteger(POSITION_TYPE);
double opn = PositionGetDouble(POSITION_PRICE_OPEN);
if(AdverseExit(typ, opn))
{
trade.PositionClose(_Symbol);
return;
}
if(ProfitExit(typ, opn))
{
trade.PositionClose(_Symbol);
return;
}
if(typ == POSITION_TYPE_BUY && ModelCloseLong(p0, p1, p3))
trade.PositionClose(_Symbol);
else if(typ == POSITION_TYPE_SELL && ModelCloseShort(p0, p2, p4))
trade.PositionClose(_Symbol);
}
@@ -0,0 +1,27 @@
; XAUUSD_M15_ActionEA v1.03 — optimization preset
; Copy to: MetaQuotes\Terminal\<ID>\MQL5\Profiles\Tester\
; Strategy Tester → Inputs → context menu → Load
;
; Format: Name=value||optimize_start||step||stop||Y|N (Y = optimize this parameter)
;
; Model
InpLookback=64||48||8||96||Y
InpEntryMode=1||0||1||1||Y
InpProbBuy=0.18||0.14||0.02||0.26||Y
InpProbSell=0.18||0.14||0.02||0.26||Y
InpMinBeatHold=0.0||0.0||0.01||0.05||Y
InpExitMode=2||0||1||2||Y
InpProbCloseL=0.18||0.14||0.02||0.26||Y
InpProbCloseS=0.18||0.14||0.02||0.26||Y
InpMinCloseBeatHold=0.0||0.0||0.01||0.04||Y
; Session (match Python SESSION_HOUR_OFFSET)
InpSessionHourOffset=0||-3||1||3||N
; Scaler: paste 24 floats from python main.py (not optimizable)
InpFeatMinStr=
InpFeatMaxStr=
; Risk
InpLotSize=0.01||0.01||0.001000||0.100000||N
InpMagic=902015||902015||1||9020150||N
InpSlippage=30||30||1||300||N
InpMaxAdverseATR=2.0||1.0||0.25||3.5||Y
InpTakeProfitATR=0.0||0.0||0.25||3.0||Y
Binary file not shown.
Binary file not shown.
+173
View File
@@ -0,0 +1,173 @@
"""
Feature pipeline: base 13 (EA-compatible) + 11 RSI / session features from frontline experience.
Frontline mapping (see FRONTLINE_RSI_INTEGRATION.md):
- RSIReversalAsianStrategy / RSICrossOverReversal: cross OB/OS, cross 50
- RSIScalpingStrategy: RSI velocity (bounce from extreme uses 3-bar structure → vel/acc)
- RSIMidPointHijack: distance from 50, RSI(7) vs RSI(14) spread
- Asian session gate → binary feature (hour window; offset for server vs UTC)
RSI uses Wilder smoothing (ewm alpha=1/period) to align with MT5 iRSI.
"""
from __future__ import annotations
import os
import numpy as np
import pandas as pd
NUM_BASE_FEATURES = 13
NUM_RSI_EXTRA = 11
NUM_FEATURES = NUM_BASE_FEATURES + NUM_RSI_EXTRA # 24
# Default thresholds aligned with common frontline inputs (Asian / scalping)
RSI_OVERBOUGHT = 70.0
RSI_OVERSOLD = 30.0
def wilder_rsi(close: pd.Series, period: int) -> np.ndarray:
"""Wilder RSI (matches MetaTrader iRSI closely)."""
delta = close.diff()
gain = delta.clip(lower=0.0)
loss = (-delta).clip(lower=0.0)
avg_g = gain.ewm(alpha=1.0 / period, min_periods=period, adjust=False).mean()
avg_l = loss.ewm(alpha=1.0 / period, min_periods=period, adjust=False).mean()
rs = avg_g / avg_l.replace(0, np.nan)
rsi = 100.0 - (100.0 / (1.0 + rs))
return rsi.fillna(50.0).to_numpy(dtype=np.float64)
def prepare_features_full(
df: pd.DataFrame,
*,
session_hour_offset: int | None = None,
) -> pd.DataFrame:
"""
Build (N, 24) feature table, chronological index matching df.
Drops first ~50 rows (warmup) like the original pipeline.
"""
if session_hour_offset is None:
session_hour_offset = int(os.environ.get("SESSION_HOUR_OFFSET", "0"))
o = df["open"].to_numpy(dtype=np.float64)
h = df["high"].to_numpy(dtype=np.float64)
l = df["low"].to_numpy(dtype=np.float64)
c = df["close"].astype(float)
vol = df["tick_volume"].to_numpy(dtype=np.float64)
n = len(df)
idx = df.index
rsi7 = wilder_rsi(c, 7)
rsi14 = wilder_rsi(c, 14)
rsi21 = wilder_rsi(c, 21)
ema20 = c.ewm(span=20, adjust=False).mean().to_numpy()
ema50 = c.ewm(span=50, adjust=False).mean().to_numpy()
tr = np.maximum(
h - l,
np.maximum(np.abs(h - np.roll(c.to_numpy(), 1)), np.abs(l - np.roll(c.to_numpy(), 1))),
)
tr[0] = h[0] - l[0]
atr = pd.Series(tr).rolling(14).mean().to_numpy()
vol_ma = np.zeros(n)
for j in range(n):
s = 0.0
cnt = 0
for k in range(j, min(j + 20, n)):
s += vol[k]
cnt += 1
vol_ma[j] = s / cnt if cnt else vol[j]
pc_ea = np.zeros(n)
cvals = c.to_numpy()
for j in range(1, n):
den = cvals[j - 1]
pc_ea[j] = (cvals[j] - den) / den if den else 0.0
hours = np.zeros(n, dtype=np.int32)
for j in range(n):
ts = idx[j]
try:
hts = int(ts.hour)
except Exception:
hts = 0
hours[j] = (hts + session_hour_offset) % 24
rows = []
for j in range(n):
r0 = rsi14[j]
r1 = rsi14[j - 1] if j > 0 else r0
r2 = rsi14[j - 2] if j > 1 else r1
spread = np.clip((r0 - rsi7[j]) / 50.0, -1.0, 1.0)
vel = (r0 - r1) / 25.0
acc = ((r0 - r1) - (r1 - r2)) / 25.0
dist_mid = abs(r0 - 50.0) / 50.0
cross_ob = 1.0 if (r1 < RSI_OVERBOUGHT and r0 >= RSI_OVERBOUGHT) else 0.0
cross_os = 1.0 if (r1 > RSI_OVERSOLD and r0 <= RSI_OVERSOLD) else 0.0
cross_50_up = 1.0 if (r1 < 50.0 and r0 >= 50.0) else 0.0
cross_50_dn = 1.0 if (r1 > 50.0 and r0 <= 50.0) else 0.0
asian = 1.0 if (0 <= hours[j] < 8) else 0.0
rows.append(
[
float(o[j]),
float(h[j]),
float(l[j]),
float(cvals[j]),
float(vol[j] / 1_000_000.0),
float(rsi14[j] / 100.0),
float((ema20[j] - cvals[j]) / cvals[j]) if cvals[j] else 0.0,
float((ema50[j] - cvals[j]) / cvals[j]) if cvals[j] else 0.0,
float(atr[j] / cvals[j]) if cvals[j] else 0.0,
float(pc_ea[j]),
float(h[j] / l[j]) if l[j] else 1.0,
float(vol_ma[j] / 1_000_000.0),
float(vol[j] / vol_ma[j]) if vol_ma[j] > 0 else 1.0,
float(rsi7[j] / 100.0),
float(rsi21[j] / 100.0),
float(spread),
float(vel),
float(acc),
float(dist_mid),
float(cross_ob),
float(cross_os),
float(cross_50_up),
float(cross_50_dn),
float(asian),
]
)
cols = [
"open",
"high",
"low",
"close",
"tick_volume",
"rsi",
"ema20_n",
"ema50_n",
"atr_n",
"price_change",
"high_low_ratio",
"volume_ma",
"volume_ratio",
"rsi7_n",
"rsi21_n",
"rsi_fast_slow_spread",
"rsi_velocity",
"rsi_accel",
"rsi_dist_mid_50",
"rsi_cross_overbought",
"rsi_cross_oversold",
"rsi_cross_50_up",
"rsi_cross_50_down",
"session_asian_utc",
]
out = pd.DataFrame(rows, index=idx, columns=cols)
return out.iloc[50:].copy()
+130
View File
@@ -0,0 +1,130 @@
"""
Buy-low / sell-high style labels for OHLCV bars (no fixed SL/TP in labels).
Optional context: frontline RSI strategies (Asian reversal, scalping, mid-50)
are encoded as *features* in features.py (crosses, velocity, session), not as
hard rules here — the network learns joint patterns with price/volume.
Classes (integer, matches EA):
0 HOLD
1 BUY — forward upside vs ATR + local swing low
2 SELL_SHORT — forward downside vs ATR + local swing high
3 CLOSE_LONG — past-only: pullback from recent range high
4 CLOSE_SHORT — past-only: bounce from recent range low
CLOSE_* use only bars <= t (no future leak).
BUY/SELL use forward window [t+1, t+horizon] (supervised targets).
"""
from __future__ import annotations
import numpy as np
import pandas as pd
def atr_series(df: pd.DataFrame, period: int = 14) -> pd.Series:
high, low, close = df["high"], df["low"], df["close"]
tr = pd.concat(
[
high - low,
(high - close.shift()).abs(),
(low - close.shift()).abs(),
],
axis=1,
).max(axis=1)
return tr.rolling(period).mean()
def compute_action_labels(
df: pd.DataFrame,
*,
horizon: int = 32,
local_window: int = 24,
pullback_window: int = 20,
k_forward_atr: float = 0.75,
local_pct: float = 0.28,
pullback_mult: float = 0.55,
trend_mult: float = 1.05,
) -> pd.Series:
"""
Return a Series of int labels 0..4 aligned to df index.
Last `horizon` rows → HOLD (no forward path for buy/sell scoring).
"""
close = df["close"].values
high = df["high"].values
low = df["low"].values
n = len(df)
atr = atr_series(df, 14).values
labels = np.zeros(n, dtype=np.int64)
lw = local_window
pw = pullback_window
need = max(lw, pw) + 2
for t in range(n):
if t < need or t >= n - horizon:
labels[t] = 0
continue
a = atr[t]
if not np.isfinite(a) or a <= 0:
a = close[t] * 1e-4
sl = low[t + 1 : t + horizon + 1]
sh = high[t + 1 : t + horizon + 1]
fwd_max = float(np.max(sh))
fwd_min = float(np.min(sl))
up_move = (fwd_max - close[t]) / a
down_move = (close[t] - fwd_min) / a
loc_low = float(np.min(low[t - lw : t + 1]))
loc_high = float(np.max(high[t - lw : t + 1]))
rng = max(loc_high - loc_low, a * 0.15)
near_low = (close[t] - loc_low) / rng <= local_pct
near_high = (loc_high - close[t]) / rng <= local_pct
buy_sig = near_low and (up_move >= k_forward_atr) and (up_move >= down_move * 0.85)
sell_sig = near_high and (down_move >= k_forward_atr) and (down_move > up_move * 1.05)
# Past window [t-pw, t]
seg_h = high[t - pw : t + 1]
seg_l = low[t - pw : t + 1]
rh = float(np.max(seg_h))
rl = float(np.min(seg_l))
range_atr = (rh - rl) / a
pull_from_high = (rh - close[t]) / a
bounce_from_low = (close[t] - rl) / a
exit_long = (
range_atr >= trend_mult
and pull_from_high >= pullback_mult
and close[t] < close[t - 1]
)
exit_short = (
range_atr >= trend_mult
and bounce_from_low >= pullback_mult
and close[t] > close[t - 1]
)
if exit_long and not buy_sig:
labels[t] = 3
elif exit_short and not sell_sig:
labels[t] = 4
elif buy_sig and not sell_sig:
labels[t] = 1
elif sell_sig and not buy_sig:
labels[t] = 2
elif buy_sig and sell_sig:
labels[t] = 1 if up_move >= down_move else 2
else:
labels[t] = 0
return pd.Series(labels, index=df.index, name="action_label")
def class_weights(y: np.ndarray, n_classes: int = 5) -> dict[int, float]:
from sklearn.utils.class_weight import compute_class_weight
y_int = y.astype(int)
classes = np.arange(n_classes)
cw = compute_class_weight("balanced", classes=classes, y=y_int)
return {i: float(cw[i]) for i in range(n_classes)}
+210
View File
@@ -0,0 +1,210 @@
"""
XAUUSD M15 — ONNX action model (buy / sell short / close long / close short / hold).
Features: 24 dims — base 13 + RSI/frontline stack (see features.py, FRONTLINE_RSI_INTEGRATION.md).
Row order matches XAUUSD_M15_ActionEA.mq5 (row 0 = newest bar).
Data: MT5, 20082026 (limited by downloaded history).
"""
from __future__ import annotations
import json
import os
import pickle
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from tensorflow import keras
from tensorflow.keras import layers
from tqdm import tqdm
import tf2onnx
import onnx
from labeling import class_weights, compute_action_labels
from features import NUM_FEATURES, prepare_features_full
NUM_CLASSES = 5
CLASS_NAMES = ["HOLD", "BUY", "SELL_SHORT", "CLOSE_LONG", "CLOSE_SHORT"]
def fetch_mt5_range(
symbol: str,
timeframe: int,
start_date: datetime,
end_date: datetime,
) -> pd.DataFrame:
if not mt5.initialize():
raise RuntimeError(f"MT5 init failed: {mt5.last_error()}")
info = mt5.symbol_info(symbol)
if info is None:
mt5.shutdown()
raise ValueError(f"Symbol {symbol} not found")
if not info.visible and not mt5.symbol_select(symbol, True):
mt5.shutdown()
raise ValueError(f"Cannot select {symbol}")
all_rows: list[dict] = []
chunk_days = 30
cur = start_date
while cur < end_date:
chunk_end = min(cur + timedelta(days=chunk_days), end_date)
rates = mt5.copy_rates_range(symbol, timeframe, cur, chunk_end)
if rates is not None and len(rates) > 1:
for row in rates:
all_rows.append({n: row[n] for n in rates.dtype.names})
cur = chunk_end
if not all_rows:
mt5.shutdown()
raise ValueError("No rates returned — download XAUUSD M15 in MT5 History Center")
df = pd.DataFrame(all_rows)
df["time"] = pd.to_datetime(df["time"], unit="s")
df = df.set_index("time").sort_index()
df = df[~df.index.duplicated(keep="first")]
return df
def create_sequences(
X: np.ndarray, y: np.ndarray, lookback: int
) -> tuple[np.ndarray, np.ndarray]:
"""
Window ends at bar i (chronological). Rows: newest-first inside each window
(matches MT5 series arrays in EA).
"""
xs, ys = [], []
for i in tqdm(range(lookback - 1, len(X)), desc="sequences"):
window = X[i - lookback + 1 : i + 1].copy()
window = window[::-1] # newest bar first → same as EA matrix row 0
xs.append(window)
ys.append(y[i])
return np.asarray(xs, dtype=np.float32), np.asarray(ys, dtype=np.int64)
def build_model(lookback: int, n_feat: int) -> keras.Model:
inp = layers.Input(shape=(lookback, n_feat))
x = layers.LSTM(96, return_sequences=True)(inp)
x = layers.Dropout(0.25)(x)
x = layers.LSTM(48)(x)
x = layers.Dropout(0.25)(x)
x = layers.Dense(32, activation="relu")(x)
out = layers.Dense(NUM_CLASSES, activation="softmax", name="action_probs")(x)
model = keras.Model(inp, out)
model.compile(
optimizer=keras.optimizers.Adam(1e-3),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
return model
def main() -> int:
symbol = os.environ.get("XAU_SYMBOL", "XAUUSD")
lookback = int(os.environ.get("XAU_LOOKBACK", "64"))
epochs = int(os.environ.get("XAU_EPOCHS", "40"))
batch_size = int(os.environ.get("XAU_BATCH", "64"))
start_date = datetime(2008, 1, 1)
end_date = datetime(2026, 12, 31)
out_dir = os.path.join(os.path.dirname(__file__), "models")
os.makedirs(out_dir, exist_ok=True)
onnx_path = os.path.join(out_dir, f"{symbol}_M15_action.onnx")
meta_path = os.path.join(out_dir, f"{symbol}_M15_action_meta.json")
print("Fetching MT5 data …")
try:
raw = fetch_mt5_range(symbol, mt5.TIMEFRAME_M15, start_date, end_date)
finally:
mt5.shutdown()
print(f"Bars: {len(raw)} range: {raw.index[0]}{raw.index[-1]}")
feat = prepare_features_full(raw)
labels_full = compute_action_labels(raw)
labels = labels_full.loc[feat.index]
y = labels.loc[feat.index].values.astype(np.int64)
X_raw = feat.values.astype(np.float32)
valid = np.isfinite(X_raw).all(axis=1) & (y >= 0) & (y < NUM_CLASSES)
X_raw = X_raw[valid]
y = y[valid]
print("Label counts:", {CLASS_NAMES[i]: int((y == i).sum()) for i in range(NUM_CLASSES)})
scaler = MinMaxScaler()
Xn = scaler.fit_transform(X_raw).astype(np.float32)
X_seq, y_seq = create_sequences(Xn, y, lookback)
if len(X_seq) < 500:
print("ERROR: Too few sequences — need more M15 history in MT5.")
return 1
X_train, X_val, y_train, y_val = train_test_split(
X_seq, y_seq, test_size=0.15, shuffle=False
)
cw = class_weights(y_train, NUM_CLASSES)
sample_w = np.array([cw[int(c)] for c in y_train], dtype=np.float32)
model = build_model(lookback, NUM_FEATURES)
model.summary()
model.fit(
X_train,
y_train,
sample_weight=sample_w,
validation_data=(X_val, y_val),
epochs=epochs,
batch_size=batch_size,
verbose=1,
callbacks=[
keras.callbacks.EarlyStopping(
monitor="val_loss", patience=8, restore_best_weights=True
),
keras.callbacks.ReduceLROnPlateau(
monitor="val_loss", factor=0.5, patience=4, min_lr=1e-6
),
],
)
spec = (tf.TensorSpec((None, lookback, NUM_FEATURES), tf.float32, name="input"),)
onnx_m, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=13)
onnx.save_model(onnx_m, onnx_path)
with open(onnx_path.replace(".onnx", "_scaler.pkl"), "wb") as f:
pickle.dump(scaler, f)
meta = {
"symbol": symbol,
"timeframe": "M15",
"lookback": lookback,
"num_features": int(NUM_FEATURES),
"feature_columns": feat.columns.tolist(),
"num_classes": NUM_CLASSES,
"class_names": CLASS_NAMES,
"scaler_feature_min": scaler.data_min_.tolist(),
"scaler_feature_max": scaler.data_max_.tolist(),
"scaler_scale": scaler.scale_.tolist() if hasattr(scaler, "scale_") else None,
"notes": "MinMax in EA; row0=newest. See FRONTLINE_RSI_INTEGRATION.md.",
}
with open(meta_path, "w", encoding="utf-8") as f:
json.dump(meta, f, indent=2)
print(f"Saved: {onnx_path}")
print(f"Meta: {meta_path}")
print("\n--- Paste into EA InpFeatMinStr / InpFeatMaxStr (comma-separated, %d floats each) ---" % NUM_FEATURES)
print(",".join(f"{x:.8g}" for x in scaler.data_min_))
print(",".join(f"{x:.8g}" for x in scaler.data_max_))
return 0
if __name__ == "__main__":
sys.exit(main())
Binary file not shown.
@@ -0,0 +1,119 @@
{
"symbol": "XAUUSD",
"timeframe": "M15",
"lookback": 64,
"num_features": 24,
"feature_columns": [
"open",
"high",
"low",
"close",
"tick_volume",
"rsi",
"ema20_n",
"ema50_n",
"atr_n",
"price_change",
"high_low_ratio",
"volume_ma",
"volume_ratio",
"rsi7_n",
"rsi21_n",
"rsi_fast_slow_spread",
"rsi_velocity",
"rsi_accel",
"rsi_dist_mid_50",
"rsi_cross_overbought",
"rsi_cross_oversold",
"rsi_cross_50_up",
"rsi_cross_50_down",
"session_asian_utc"
],
"num_classes": 5,
"class_names": [
"HOLD",
"BUY",
"SELL_SHORT",
"CLOSE_LONG",
"CLOSE_SHORT"
],
"scaler_feature_min": [
1616.6700439453125,
1618.8499755859375,
1614.8199462890625,
1616.6800537109375,
0.0,
0.08494461327791214,
-0.03888450935482979,
-0.0373079888522625,
0.0001997762155951932,
-0.036351919174194336,
1.0,
0.00017494999337941408,
0.0,
0.02623281255364418,
0.12661120295524597,
-0.5349156260490417,
-2.0385215282440186,
-2.397653818130493,
2.298711478943005e-06,
0.0,
0.0,
0.0,
0.0,
0.0
],
"scaler_feature_max": [
5585.740234375,
5598.06005859375,
5577.580078125,
5585.740234375,
0.013647999614477158,
0.9493793845176697,
0.06255777180194855,
0.06892234832048416,
0.01657661236822605,
0.04392698407173157,
1.056401252746582,
0.010342299938201904,
5.7435832023620605,
0.9802423715591431,
0.9216755032539368,
0.5254908204078674,
1.544172763824463,
2.009221076965332,
0.8987588286399841,
1.0,
1.0,
1.0,
1.0,
1.0
],
"scaler_scale": [
0.00025194816407747567,
0.0002513061626814306,
0.00025234936038032174,
0.00025194883346557617,
73.27081298828125,
1.156825304031372,
9.85782241821289,
9.413507461547852,
61.06185531616211,
12.456572532653809,
17.7301025390625,
98.35404205322266,
0.17410734295845032,
1.0482075214385986,
1.2577598094940186,
0.9430346488952637,
0.2791195511817932,
0.22691819071769714,
1.112648367881775,
1.0,
1.0,
1.0,
1.0,
1.0
],
"notes": "MinMax in EA; row0=newest. See FRONTLINE_RSI_INTEGRATION.md."
}
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
numpy>=1.23
pandas>=2.0
MetaTrader5>=5.0.45
tensorflow>=2.14
tf2onnx>=1.16
onnx>=1.15
scikit-learn>=1.3
tqdm>=4.66
+25
View File
@@ -0,0 +1,25 @@
"""
Dynamic adverse risk (conceptual mirror of EA InpMaxAdverseATR).
For backtests in Python: given entry price, ATR series, and bid/ask path,
exit when (entry - bid)/atr >= max_adv for long.
"""
from __future__ import annotations
import numpy as np
def adverse_hit_long(
entry: float,
low_path: np.ndarray,
atr_path: np.ndarray,
max_adverse_atr: float,
) -> int | None:
"""Return first index where adverse >= threshold, else None."""
for i in range(len(low_path)):
atr = max(atr_path[i], entry * 1e-6)
adv = (entry - low_path[i]) / atr
if adv >= max_adverse_atr:
return i
return None