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