Update
This commit is contained in:
@@ -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.
|
||||
@@ -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.
@@ -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()
|
||||
@@ -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)}
|
||||
@@ -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, 2008–2026 (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.
@@ -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
|
||||
@@ -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 顺序一致)
|
||||
|
||||
| 索引 | 名称 | 说明 |
|
||||
|------|------|------|
|
||||
| 0–4 | OHLC + tick_volume | 与原版一致 |
|
||||
| 5 | rsi | Wilder RSI(14)/100 |
|
||||
| 6–12 | EMA/ATR/价量 | 与原版一致 |
|
||||
| 13 | rsi7_n | RSI(7)/100 |
|
||||
| 14 | rsi21_n | RSI(21)/100 |
|
||||
| 15 | rsi_fast_slow_spread | clip((RSI14−RSI7)/50, −1, 1) |
|
||||
| 16 | rsi_velocity | (RSI14₀−RSI14₁)/25 |
|
||||
| 17 | rsi_accel | ((RSI14₀−RSI14₁)−(RSI14₁−RSI14₂))/25 |
|
||||
| 18 | rsi_dist_mid_50 | \|RSI14−50\|/50 |
|
||||
| 19–22 | 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**。
|
||||
@@ -0,0 +1,49 @@
|
||||
# XAUUSD M15 — ONNX action model (buy / sell / close)
|
||||
|
||||
## What it does
|
||||
|
||||
- Pulls **XAUUSD** (**M15**) from **MetaTrader 5** (2008–2026 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.
|
||||
@@ -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.15–0.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 1–2: require p3/p4 > p0 + this
|
||||
|
||||
input group "Session (match Python SESSION_HOUR_OFFSET)"
|
||||
input int InpSessionHourOffset = 0; // add to bar hour so Asian 0–8 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.
@@ -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()
|
||||
@@ -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)}
|
||||
@@ -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, 2008–2026 (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.
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user