UpDATE
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| EURUSD_H1_ActionEA.mq5 |
|
||||
//| ai/eurusd1h/main.py — 24 features, 5-class softmax |
|
||||
//| Classes: 0=HOLD 1=BUY 2=SELL_SHORT 3=CLOSE_LONG 4=CLOSE_SHORT |
|
||||
//| Entry: strict trio winner among p0,p1,p2 only. |
|
||||
//| Exit: unique 5-class argmax != held side (1 long, 2 short). |
|
||||
//| No SL / TP / ATR stops. Attach EURUSD H1. |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "Profitable EA Project"
|
||||
#property version "1.00"
|
||||
#property description "EURUSD H1 action ONNX; ordinal entry/exit; no fixed SL/TP"
|
||||
|
||||
#include <Trade\Trade.mqh>
|
||||
|
||||
#resource "models\\EURUSD_H1_action.onnx" as uchar ExtModel[]
|
||||
|
||||
#define FEAT_COUNT 24
|
||||
#define REL_EPS 1e-9
|
||||
|
||||
input group "Model"
|
||||
input int InpLookback = 48;
|
||||
input int InpSessionHourOffset = 0;
|
||||
input string InpFeatMinStr = "";
|
||||
input string InpFeatMaxStr = "";
|
||||
|
||||
input group "Timing"
|
||||
input int InpMinBarsInTrade = 1; // model exit only after this many bars in position (0=off)
|
||||
|
||||
input group "Trade"
|
||||
input double InpLotSize = 0.01;
|
||||
input int InpMagic = 902601;
|
||||
input int InpSlippage = 30;
|
||||
|
||||
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 InitDefaultScalerFromMeta()
|
||||
{
|
||||
// EURUSD_H1_action_meta.json scaler_feature_min / max (train fit)
|
||||
double def_min[FEAT_COUNT] = {
|
||||
0.9539399743080139,
|
||||
0.9559400081634521,
|
||||
0.9536200165748596,
|
||||
0.9538999795913696,
|
||||
9.999999974752427e-07,
|
||||
0.07019035518169403,
|
||||
-0.02143237181007862,
|
||||
-0.028110405430197716,
|
||||
0.0002704667276702821,
|
||||
-0.02017582766711712,
|
||||
1.0,
|
||||
0.0004555500054266304,
|
||||
0.0002461568801663816,
|
||||
0.022001149132847786,
|
||||
0.11231997609138489,
|
||||
-0.5339273810386658,
|
||||
-1.623793125152588,
|
||||
-1.837566614151001,
|
||||
6.83732741890708e-06,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
};
|
||||
double def_max[FEAT_COUNT] = {
|
||||
1.493149995803833,
|
||||
1.4938499927520752,
|
||||
1.4904999732971191,
|
||||
1.493190050125122,
|
||||
0.06699500232934952,
|
||||
0.9350273013114929,
|
||||
0.028008731082081795,
|
||||
0.03147505968809128,
|
||||
0.009249407798051834,
|
||||
0.01742853783071041,
|
||||
1.0232577323913574,
|
||||
0.02111775055527687,
|
||||
7.801275253295898,
|
||||
0.9864169955253601,
|
||||
0.8837512731552124,
|
||||
0.49708572030067444,
|
||||
1.8055412769317627,
|
||||
1.927569031715393,
|
||||
0.8700546026229858,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
};
|
||||
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[];
|
||||
if(StringSplit(s, ',', parts) != FEAT_COUNT) return false;
|
||||
for(int i = 0; i < FEAT_COUNT; i++)
|
||||
arr[i] = StringToDouble(parts[i]);
|
||||
return true;
|
||||
}
|
||||
|
||||
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 spr = (r0 - rv7) / 50.0;
|
||||
if(spr > 1.0) spr = 1.0;
|
||||
if(spr < -1.0) spr = -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)spr;
|
||||
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;
|
||||
}
|
||||
|
||||
int TrioStrictWinner012(const double p0, const double p1, const double p2)
|
||||
{
|
||||
if(p0 > p1 + REL_EPS && p0 > p2 + REL_EPS) return 0;
|
||||
if(p1 > p0 + REL_EPS && p1 > p2 + REL_EPS) return 1;
|
||||
if(p2 > p0 + REL_EPS && p2 > p1 + REL_EPS) return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int FiveStrictWinner01234(const double p0, const double p1, const double p2, const double p3, const double p4)
|
||||
{
|
||||
const double p[5] = {p0, p1, p2, p3, p4};
|
||||
int best = 0;
|
||||
for(int k = 1; k < 5; k++)
|
||||
if(p[k] > p[best])
|
||||
best = k;
|
||||
const double m = p[best];
|
||||
int cnt = 0;
|
||||
for(int k = 0; k < 5; k++)
|
||||
if(p[k] + REL_EPS >= m)
|
||||
cnt++;
|
||||
if(cnt != 1)
|
||||
return -1;
|
||||
return best;
|
||||
}
|
||||
|
||||
bool SelectOurPosition()
|
||||
{
|
||||
if(!PositionSelect(_Symbol))
|
||||
return false;
|
||||
if((long)PositionGetInteger(POSITION_MAGIC) != InpMagic)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int PositionBarsInTrade()
|
||||
{
|
||||
if(!SelectOurPosition())
|
||||
return 0;
|
||||
const datetime tOpen = (datetime)PositionGetInteger(POSITION_TIME);
|
||||
const int sh = iBarShift(_Symbol, PERIOD_CURRENT, tOpen, false);
|
||||
if(sh < 0)
|
||||
return 9999;
|
||||
return sh + 1;
|
||||
}
|
||||
|
||||
int OnInit()
|
||||
{
|
||||
InitDefaultScalerFromMeta();
|
||||
trade.SetExpertMagicNumber(InpMagic);
|
||||
trade.SetDeviationInPoints(InpSlippage);
|
||||
trade.SetTypeFilling(ORDER_FILLING_IOC);
|
||||
|
||||
if(StringLen(InpFeatMinStr) > 0 && ParseFeatCsv(InpFeatMinStr, g_feat_min))
|
||||
Print("EURUSD Action EA: loaded InpFeatMinStr");
|
||||
if(StringLen(InpFeatMaxStr) > 0 && ParseFeatCsv(InpFeatMaxStr, g_feat_max))
|
||||
Print("EURUSD Action EA: loaded InpFeatMaxStr");
|
||||
|
||||
g_onnx = OnnxCreateFromBuffer(ExtModel, ONNX_DEBUG_LOGS);
|
||||
if(g_onnx == INVALID_HANDLE)
|
||||
{
|
||||
Print("OnnxCreateFromBuffer failed ", GetLastError());
|
||||
return INIT_FAILED;
|
||||
}
|
||||
const long inShape[] = {1, InpLookback, FEAT_COUNT};
|
||||
if(!OnnxSetInputShape(g_onnx, 0, inShape))
|
||||
{
|
||||
Print("OnnxSetInputShape failed ", GetLastError());
|
||||
OnnxRelease(g_onnx);
|
||||
return INIT_FAILED;
|
||||
}
|
||||
const long outShape[] = {1, 5};
|
||||
if(!OnnxSetOutputShape(g_onnx, 0, outShape))
|
||||
{
|
||||
Print("OnnxSetOutputShape failed ", GetLastError());
|
||||
OnnxRelease(g_onnx);
|
||||
return INIT_FAILED;
|
||||
}
|
||||
|
||||
if(_Period != PERIOD_H1)
|
||||
Print("EURUSD_H1_ActionEA: chart period is ", EnumToString((ENUM_TIMEFRAMES)_Period),
|
||||
" — training is H1; mismatch may hurt.");
|
||||
|
||||
Print("EURUSD_H1_ActionEA: ONNX OK. Ordinal entry/exit, no SL/TP. Lookback=", InpLookback);
|
||||
return INIT_SUCCEEDED;
|
||||
}
|
||||
|
||||
void OnDeinit(const int r)
|
||||
{
|
||||
if(g_onnx != INVALID_HANDLE)
|
||||
OnnxRelease(g_onnx);
|
||||
}
|
||||
|
||||
void OnTick()
|
||||
{
|
||||
datetime t = iTime(_Symbol, PERIOD_CURRENT, 0);
|
||||
if(t == g_last_bar)
|
||||
return;
|
||||
g_last_bar = t;
|
||||
|
||||
matrixf Min;
|
||||
if(!PrepareMatrix(Min))
|
||||
{
|
||||
Print("EURUSD Action EA: PrepareMatrix failed");
|
||||
return;
|
||||
}
|
||||
vectorf out;
|
||||
out.Resize(5);
|
||||
if(!OnnxRun(g_onnx, ONNX_NO_CONVERSION, Min, out))
|
||||
{
|
||||
Print("OnnxRun failed ", GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
const double p0 = out[0], p1 = out[1], p2 = out[2], p3 = out[3], p4 = out[4];
|
||||
|
||||
if(!SelectOurPosition())
|
||||
{
|
||||
const int w3 = TrioStrictWinner012(p0, p1, p2);
|
||||
if(w3 == 1)
|
||||
trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act BUY");
|
||||
else if(w3 == 2)
|
||||
trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "EURUSD act SELL");
|
||||
return;
|
||||
}
|
||||
|
||||
const bool allow = (InpMinBarsInTrade <= 0) || (PositionBarsInTrade() >= InpMinBarsInTrade);
|
||||
if(!allow)
|
||||
return;
|
||||
|
||||
const int w5 = FiveStrictWinner01234(p0, p1, p2, p3, p4);
|
||||
const long typ = (long)PositionGetInteger(POSITION_TYPE);
|
||||
bool close_it = false;
|
||||
if(typ == POSITION_TYPE_BUY)
|
||||
close_it = (w5 != -1 && w5 != 1);
|
||||
else if(typ == POSITION_TYPE_SELL)
|
||||
close_it = (w5 != -1 && w5 != 2);
|
||||
|
||||
if(close_it)
|
||||
trade.PositionClose(_Symbol);
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
# EURUSD H1 — ONNX action model (full MT5 history)
|
||||
|
||||
Same methodology as `ai/yt/train_article_split.py`:
|
||||
|
||||
- **24 features** + **5 softmax classes** (`ai/xauusd_h1/features.py`, `labeling.py`).
|
||||
- **MinMaxScaler** is fit on **every** valid feature row MT5 returns (no 2010–2020 cut).
|
||||
- **Training sequences**: all but a **chronological tail** (default **12%**) used only for `val_loss` / EarlyStopping (does not remove data from the scaler).
|
||||
- Optional **KMeans** on forward-return fingerprints + class-balanced `sample_weight` on the train split.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
cd ai/eurusd1h
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
Requires MetaTrader 5 with **EURUSD H1** history downloaded (Tools → History Center).
|
||||
|
||||
## Environment overrides
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|-----------------|-----------|----------------------------------------------|
|
||||
| `EUR_SYMBOL` | `EURUSD` | MT5 symbol |
|
||||
| `EUR_LOOKBACK` | `48` | Sequence length |
|
||||
| `EUR_EPOCHS` | `40` | Max epochs |
|
||||
| `EUR_BATCH` | `64` | Batch size |
|
||||
| `EUR_CLUSTERS` | `12` | KMeans clusters (`0` = off) |
|
||||
| `EUR_VAL_FRAC` | `0.12` | Fraction of sequences at **end** for val |
|
||||
|
||||
## Outputs
|
||||
|
||||
- `models/EURUSD_H1_action.onnx`
|
||||
- `models/EURUSD_H1_action_meta.json`
|
||||
- `models/EURUSD_H1_action_scaler.pkl`
|
||||
|
||||
Deploy like `ai/yt/US500_H1_ArticleEA.mq5`: embed ONNX, set `InpLookback`, paste `scaler_feature_min` / `max` from the meta JSON into the EA inputs.
|
||||
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
EURUSD H1 — same stack as ai/yt (24 features, 5-class softmax, optional KMeans weights).
|
||||
|
||||
MinMaxScaler is fit on **all** feature rows returned by MT5 (full downloaded history).
|
||||
A chronological **tail** slice (default 12%% of sequences) is used only for val_loss /
|
||||
EarlyStopping; all earlier sequences are used for training.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import tensorflow as tf
|
||||
import tf2onnx
|
||||
import onnx
|
||||
from sklearn.cluster import KMeans
|
||||
from sklearn.preprocessing import MinMaxScaler
|
||||
from tensorflow import keras
|
||||
from tensorflow.keras import layers
|
||||
from tqdm import tqdm
|
||||
|
||||
_XH1 = Path(__file__).resolve().parent.parent / "xauusd_h1"
|
||||
sys.path.insert(0, str(_XH1))
|
||||
from features import NUM_FEATURES, prepare_features_full # noqa: E402
|
||||
from labeling import atr_series, class_weights, compute_action_labels # noqa: E402
|
||||
|
||||
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 EURUSD 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 forward_return_fingerprints(
|
||||
df: pd.DataFrame,
|
||||
feat_index: pd.DatetimeIndex,
|
||||
horizons: tuple[int, ...] = (1, 2, 4, 8, 16),
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
close = df["close"].to_numpy(dtype=np.float64)
|
||||
atr = atr_series(df, 14).to_numpy(dtype=np.float64)
|
||||
pos = df.index.get_indexer(feat_index)
|
||||
n = len(feat_index)
|
||||
d = len(horizons)
|
||||
M = np.zeros((n, d), dtype=np.float64)
|
||||
valid = np.ones(n, dtype=bool)
|
||||
max_h = max(horizons)
|
||||
for j, i in enumerate(pos):
|
||||
if i < 0 or i + max_h >= len(close):
|
||||
valid[j] = False
|
||||
continue
|
||||
a = float(atr[i]) if np.isfinite(atr[i]) and atr[i] > 0 else close[i] * 1e-4
|
||||
for k, h in enumerate(horizons):
|
||||
if i + h >= len(close):
|
||||
valid[j] = False
|
||||
break
|
||||
M[j, k] = (close[i + h] - close[i]) / a
|
||||
return M, valid
|
||||
|
||||
|
||||
def create_sequences(
|
||||
X: np.ndarray,
|
||||
y: np.ndarray,
|
||||
times: np.ndarray,
|
||||
lookback: int,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
xs, ys, t_end = [], [], []
|
||||
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])
|
||||
t_end.append(times[i])
|
||||
return (
|
||||
np.asarray(xs, dtype=np.float32),
|
||||
np.asarray(ys, dtype=np.int64),
|
||||
np.asarray(t_end),
|
||||
)
|
||||
|
||||
|
||||
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("EUR_SYMBOL", "EURUSD")
|
||||
lookback = int(os.environ.get("EUR_LOOKBACK", "48"))
|
||||
epochs = int(os.environ.get("EUR_EPOCHS", "40"))
|
||||
batch_size = int(os.environ.get("EUR_BATCH", "64"))
|
||||
n_clusters = int(os.environ.get("EUR_CLUSTERS", "12"))
|
||||
val_frac = float(os.environ.get("EUR_VAL_FRAC", "0.12"))
|
||||
val_frac = min(max(val_frac, 0.05), 0.35)
|
||||
|
||||
fetch_start = datetime(1990, 1, 1)
|
||||
fetch_end = datetime(2030, 12, 31)
|
||||
|
||||
out_dir = Path(__file__).resolve().parent / "models"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
onnx_path = out_dir / f"{symbol}_H1_action.onnx"
|
||||
meta_path = out_dir / f"{symbol}_H1_action_meta.json"
|
||||
|
||||
print(f"Symbol={symbol} H1 | fetch [{fetch_start.date()} .. {fetch_end.date()}]")
|
||||
print("Scaler: ALL bars | Val: chronological tail for early stopping only")
|
||||
print("Fetching MT5 …")
|
||||
try:
|
||||
raw = fetch_mt5_range(symbol, mt5.TIMEFRAME_H1, fetch_start, fetch_end)
|
||||
finally:
|
||||
mt5.shutdown()
|
||||
|
||||
if len(raw) < 500:
|
||||
print("ERROR: Not enough H1 bars — check EURUSD history in MT5.")
|
||||
return 1
|
||||
|
||||
print(f"Bars: {len(raw)} range: {raw.index[0]} → {raw.index[-1]}")
|
||||
|
||||
feat = prepare_features_full(raw)
|
||||
labels = compute_action_labels(raw).loc[feat.index]
|
||||
y = labels.values.astype(np.int64)
|
||||
X_raw = feat.values.astype(np.float32)
|
||||
times = feat.index.to_numpy()
|
||||
|
||||
valid = np.isfinite(X_raw).all(axis=1) & (y >= 0) & (y < NUM_CLASSES)
|
||||
X_raw = X_raw[valid]
|
||||
y = y[valid]
|
||||
times = times[valid]
|
||||
|
||||
scaler = MinMaxScaler()
|
||||
scaler.fit(X_raw)
|
||||
Xn = scaler.transform(X_raw).astype(np.float32)
|
||||
|
||||
X_seq, y_seq, t_end = create_sequences(Xn, y, times, lookback)
|
||||
if len(X_seq) < 500:
|
||||
print("ERROR: Too few sequences.")
|
||||
return 1
|
||||
|
||||
n_seq = len(X_seq)
|
||||
split_i = int(n_seq * (1.0 - val_frac))
|
||||
split_i = max(split_i, lookback + 100)
|
||||
split_i = min(split_i, n_seq - 200)
|
||||
train_m = np.zeros(n_seq, dtype=bool)
|
||||
train_m[:split_i] = True
|
||||
val_m = ~train_m
|
||||
|
||||
X_train, y_train = X_seq[train_m], y_seq[train_m]
|
||||
X_val, y_val = X_seq[val_m], y_seq[val_m]
|
||||
print(
|
||||
f"Sequences train={len(X_train)} val_tail={len(X_val)} ({100*val_frac:.1f}%%) "
|
||||
f"lookback={lookback}"
|
||||
)
|
||||
|
||||
cw = class_weights(y_train, NUM_CLASSES)
|
||||
tr_idx = np.flatnonzero(train_m)
|
||||
base_w = np.array([cw[int(y_seq[i])] for i in tr_idx], dtype=np.float32)
|
||||
sample_w = base_w.copy()
|
||||
|
||||
if n_clusters > 1:
|
||||
fp, fp_ok = forward_return_fingerprints(raw, pd.DatetimeIndex(times))
|
||||
fp_seq = fp[lookback - 1 :]
|
||||
ok_seq = fp_ok[lookback - 1 :]
|
||||
fp_tr = fp_seq[tr_idx]
|
||||
ok_tr = ok_seq[tr_idx]
|
||||
fit_mask = ok_tr & np.isfinite(fp_tr).all(axis=1)
|
||||
if int(fit_mask.sum()) >= n_clusters * 5:
|
||||
km = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
|
||||
km.fit(fp_tr[fit_mask])
|
||||
labels_tr = np.full(len(tr_idx), -1, dtype=np.int32)
|
||||
labels_tr[fit_mask] = km.predict(fp_tr[fit_mask])
|
||||
counts = np.zeros(n_clusters, dtype=np.float64)
|
||||
for c in labels_tr:
|
||||
if 0 <= c < n_clusters:
|
||||
counts[c] += 1.0
|
||||
counts = np.maximum(counts, 1.0)
|
||||
total_assigned = max(int((labels_tr >= 0).sum()), 1)
|
||||
w_cl = np.ones(len(tr_idx), dtype=np.float32)
|
||||
for j in range(len(tr_idx)):
|
||||
c = int(labels_tr[j])
|
||||
if c >= 0:
|
||||
w_cl[j] = float(total_assigned / (n_clusters * counts[c]))
|
||||
sample_w = base_w * w_cl
|
||||
sample_w *= len(sample_w) / float(np.sum(sample_w))
|
||||
print(f"KMeans clusters={n_clusters} (train subset only)")
|
||||
else:
|
||||
print("Skipping KMeans: not enough valid fingerprints.")
|
||||
|
||||
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=10, restore_best_weights=True
|
||||
),
|
||||
keras.callbacks.ReduceLROnPlateau(
|
||||
monitor="val_loss", factor=0.5, patience=4, min_lr=1e-6
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
loss, acc = model.evaluate(X_val, y_val, verbose=0)
|
||||
print(f"Tail val_loss={loss:.4f} val_accuracy={acc:.4f}")
|
||||
|
||||
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, str(onnx_path))
|
||||
|
||||
with open(str(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,
|
||||
"mt5_bar_range": [str(raw.index[0]), str(raw.index[-1])],
|
||||
"scaler_fit_on": "all_valid_feature_rows_full_mt5_range",
|
||||
"validation_split": {
|
||||
"mode": "chronological_tail_fraction",
|
||||
"val_fraction": val_frac,
|
||||
"train_sequences": int(train_m.sum()),
|
||||
"val_sequences": int(val_m.sum()),
|
||||
},
|
||||
"clustering": (
|
||||
f"KMeans n={n_clusters} on forward returns (1,2,4,8,16); train-only fit"
|
||||
if n_clusters > 1
|
||||
else "disabled"
|
||||
),
|
||||
"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,
|
||||
"tail_val_accuracy": float(acc),
|
||||
"tail_val_loss": float(loss),
|
||||
"ea_note": "Copy ai/yt/US500_H1_ArticleEA.mq5 pattern: #resource ONNX + paste scaler from meta.",
|
||||
}
|
||||
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 (%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}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Binary file not shown.
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"symbol": "EURUSD",
|
||||
"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"
|
||||
],
|
||||
"mt5_bar_range": [
|
||||
"2010-03-17 23:00:00",
|
||||
"2026-04-24 23:00:00"
|
||||
],
|
||||
"scaler_fit_on": "all_valid_feature_rows_full_mt5_range",
|
||||
"validation_split": {
|
||||
"mode": "chronological_tail_fraction",
|
||||
"val_fraction": 0.12,
|
||||
"train_sequences": 87914,
|
||||
"val_sequences": 11989
|
||||
},
|
||||
"clustering": "KMeans n=12 on forward returns (1,2,4,8,16); train-only fit",
|
||||
"scaler_feature_min": [
|
||||
0.9539399743080139,
|
||||
0.9559400081634521,
|
||||
0.9536200165748596,
|
||||
0.9538999795913696,
|
||||
9.999999974752427e-07,
|
||||
0.07019035518169403,
|
||||
-0.02143237181007862,
|
||||
-0.028110405430197716,
|
||||
0.0002704667276702821,
|
||||
-0.02017582766711712,
|
||||
1.0,
|
||||
0.0004555500054266304,
|
||||
0.0002461568801663816,
|
||||
0.022001149132847786,
|
||||
0.11231997609138489,
|
||||
-0.5339273810386658,
|
||||
-1.623793125152588,
|
||||
-1.837566614151001,
|
||||
6.83732741890708e-06,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"scaler_feature_max": [
|
||||
1.493149995803833,
|
||||
1.4938499927520752,
|
||||
1.4904999732971191,
|
||||
1.493190050125122,
|
||||
0.06699500232934952,
|
||||
0.9350273013114929,
|
||||
0.028008731082081795,
|
||||
0.03147505968809128,
|
||||
0.009249407798051834,
|
||||
0.01742853783071041,
|
||||
1.0232577323913574,
|
||||
0.02111775055527687,
|
||||
7.801275253295898,
|
||||
0.9864169955253601,
|
||||
0.8837512731552124,
|
||||
0.49708572030067444,
|
||||
1.8055412769317627,
|
||||
1.927569031715393,
|
||||
0.8700546026229858,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scaler_scale": [
|
||||
1.854564905166626,
|
||||
1.8590470552444458,
|
||||
1.8626137971878052,
|
||||
1.8542896509170532,
|
||||
14.926709175109863,
|
||||
1.1562873125076294,
|
||||
20.226085662841797,
|
||||
16.782615661621094,
|
||||
111.3717041015625,
|
||||
26.5926570892334,
|
||||
42.99645233154297,
|
||||
48.39755630493164,
|
||||
0.12818820774555206,
|
||||
1.03689706325531,
|
||||
1.296291708946228,
|
||||
0.969919741153717,
|
||||
0.2916017770767212,
|
||||
0.2655946612358093,
|
||||
1.1493622064590454,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"tail_val_accuracy": 0.2594878673553467,
|
||||
"tail_val_loss": 1.5548540353775024,
|
||||
"ea_note": "Copy ai/yt/US500_H1_ArticleEA.mq5 pattern: #resource ONNX + paste scaler from meta."
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
-r ../xauusd_h1/requirements.txt
|
||||
@@ -0,0 +1,663 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| US500_H1_ArticleEA.mq5 |
|
||||
//| ai/yt: article-split ONNX (train 2010–2019 / OOS 2020–2024) |
|
||||
//| Train: python train_article_split.py → models/*.onnx |
|
||||
//| Attach to US500 (or broker equivalent) H1 chart. |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "Profitable EA Project"
|
||||
#property version "1.05"
|
||||
#property description "Embedded US500 H1 article-split ONNX; scaler from US500_H1_article_split_meta.json"
|
||||
|
||||
#include <Trade\Trade.mqh>
|
||||
|
||||
#resource "models\\US500_H1_article_split.onnx" as uchar ExtModel[]
|
||||
|
||||
#define FEAT_COUNT 24
|
||||
#define PRED_HIST_CAP 32
|
||||
#define REL_EPS 1e-9
|
||||
|
||||
input group "Model"
|
||||
input int InpLookback = 48;
|
||||
input int InpEntryMode = 1;
|
||||
input double InpProbBuy = 0.18;
|
||||
input double InpProbSell = 0.18;
|
||||
input double InpMinBeatHold = 0.0;
|
||||
input int InpExitMode = 1; // 0=fixed prob; 1/2=close must beat HOLD and stay-in-trade (2 legacy; old 2 vs-HOLD-only removed)
|
||||
input double InpProbCloseL = 0.18;
|
||||
input double InpProbCloseS = 0.18;
|
||||
input double InpMinCloseBeatHold = 0.0;
|
||||
input int InpMinBarsInTradeModelExit = 1; // min bars before model exit (0=off); pure mode uses 5-class winner
|
||||
input bool InpPureRelative = true; // true: no prob cutoffs/edges — entry=trio strict winner, exit=5-class strict winner != side
|
||||
input bool InpUseCloseHeadExit = true; // legacy only when InpPureRelative=false (CL/CS vs HOLD/stay; see InpExitMode)
|
||||
input bool InpUseDirFlipExit = true; // legacy only when InpPureRelative=false (gap edges InpFlipExitEdge)
|
||||
input double InpFlipExitEdge = 0.03; // legacy dir-flip min gap (ignored when InpPureRelative)
|
||||
input int InpMinBarsAfterExit = 6; // after any close, wait this many flat bars before a new entry (0=off)
|
||||
input int InpCooldownBarsAfterAdverse = 12; // extra flat-bar pause after adverse (ATR) stop; 0 = use only MinBarsAfterExit
|
||||
|
||||
input group "Decision (aggregate + sample, lowers trade churn)"
|
||||
input int InpSampleEveryNBars = 2; // run ONNX / refresh history every N new bars (>=1)
|
||||
input int InpAggWindow = 4; // rolling mean over last K samples (>=1)
|
||||
input int InpMinAggSamples = 2; // need this many samples in window before new entries
|
||||
input int InpMinBarsBetweenEntries = 0; // after an open, wait this many flat bars before next entry (0=off)
|
||||
input double InpMinDirEdge = 0.03; // legacy entry mode 1 only (ignored when InpPureRelative)
|
||||
input bool InpRequireStayOverClose = true; // legacy entry (ignored when InpPureRelative)
|
||||
|
||||
input group "Session (match Python SESSION_HOUR_OFFSET)"
|
||||
input int InpSessionHourOffset = 0;
|
||||
|
||||
input group "Scaler override (empty = use built-in US500 train split)"
|
||||
input string InpFeatMinStr = "";
|
||||
input string InpFeatMaxStr = "";
|
||||
|
||||
input group "Risk"
|
||||
input double InpLotSize = 0.01;
|
||||
input int InpMagic = 902503;
|
||||
input int InpSlippage = 30;
|
||||
|
||||
input group "Hard exits (fixed ATR in price — optional)"
|
||||
input bool InpUseAdverseAtrExit = false; // stop by adverse move in ATR multiples (off = model-only risk)
|
||||
input bool InpUseProfitAtrExit = false; // take-profit in ATR multiples (needs InpTakeProfitATR > 0)
|
||||
input double InpMaxAdverseATR = 3.5;
|
||||
input double InpTakeProfitATR = 0.0;
|
||||
|
||||
double g_feat_min[FEAT_COUNT];
|
||||
double g_feat_max[FEAT_COUNT];
|
||||
|
||||
CTrade trade;
|
||||
long g_onnx = INVALID_HANDLE;
|
||||
datetime g_last_bar = 0;
|
||||
|
||||
double g_pred_hist[PRED_HIST_CAP][5];
|
||||
int g_pred_hist_len = 0;
|
||||
double g_smooth[5] = {0.2, 0.2, 0.2, 0.2, 0.2};
|
||||
ulong g_bar_index = 0;
|
||||
int g_entry_cooldown_bars = 0;
|
||||
int g_agg_w = 4;
|
||||
int g_sample_n = 2;
|
||||
int g_min_agg_samples = 2;
|
||||
|
||||
void InitDefaultScalerBounds()
|
||||
{
|
||||
// MinMax bounds from ai/yt/models/US500_H1_article_split_meta.json (train-only scaler)
|
||||
double def_min[FEAT_COUNT] = {
|
||||
1352.5,
|
||||
1352.5999755859375,
|
||||
1347.9000244140625,
|
||||
1352.0999755859375,
|
||||
0.0,
|
||||
0.04497450217604637,
|
||||
-0.030356179922819138,
|
||||
-0.04839427396655083,
|
||||
0.00028562467196024954,
|
||||
-0.047754231840372086,
|
||||
1.0,
|
||||
0.00017100000695791095,
|
||||
0.0,
|
||||
0.01168255414813757,
|
||||
0.0760856345295906,
|
||||
-0.49618232250213623,
|
||||
-1.6348180770874023,
|
||||
-1.731970191001892,
|
||||
0.000006116794793342706,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
};
|
||||
double def_max[FEAT_COUNT] = {
|
||||
3250.199951171875,
|
||||
3251.5,
|
||||
3249.5,
|
||||
3250.199951171875,
|
||||
26050000896.0,
|
||||
0.887104868888855,
|
||||
0.09538312256336212,
|
||||
0.10739167034626007,
|
||||
0.02898731827735901,
|
||||
0.036042287945747375,
|
||||
1.0754634141921997,
|
||||
6759499776.0,
|
||||
20.0,
|
||||
0.9637425541877747,
|
||||
0.8267387747764587,
|
||||
0.5573697686195374,
|
||||
1.2618913650512695,
|
||||
1.8784747123718262,
|
||||
0.9100509881973267,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
};
|
||||
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("US500 Article EA: loaded InpFeatMinStr (24)");
|
||||
if(StringLen(InpFeatMaxStr) > 0 && ParseFeatCsv(InpFeatMaxStr, g_feat_max))
|
||||
Print("US500 Article EA: loaded InpFeatMaxStr (24)");
|
||||
|
||||
g_onnx = OnnxCreateFromBuffer(ExtModel, ONNX_DEBUG_LOGS);
|
||||
if(g_onnx == INVALID_HANDLE)
|
||||
{
|
||||
Print("OnnxCreateFromBuffer failed ", GetLastError());
|
||||
return INIT_FAILED;
|
||||
}
|
||||
|
||||
const long inShape[] = {1, InpLookback, FEAT_COUNT};
|
||||
if(!OnnxSetInputShape(g_onnx, 0, inShape))
|
||||
{
|
||||
Print("OnnxSetInputShape failed ", GetLastError());
|
||||
OnnxRelease(g_onnx);
|
||||
return INIT_FAILED;
|
||||
}
|
||||
const long outShape[] = {1, 5};
|
||||
if(!OnnxSetOutputShape(g_onnx, 0, outShape))
|
||||
{
|
||||
Print("OnnxSetOutputShape failed ", GetLastError());
|
||||
OnnxRelease(g_onnx);
|
||||
return INIT_FAILED;
|
||||
}
|
||||
|
||||
g_agg_w = MathMax(1, MathMin(InpAggWindow, PRED_HIST_CAP));
|
||||
g_sample_n = MathMax(1, InpSampleEveryNBars);
|
||||
g_min_agg_samples = MathMax(1, MathMin(InpMinAggSamples, g_agg_w));
|
||||
g_pred_hist_len = 0;
|
||||
g_bar_index = 0;
|
||||
g_entry_cooldown_bars = 0;
|
||||
for(int k = 0; k < 5; k++)
|
||||
g_smooth[k] = 0.2;
|
||||
|
||||
const bool has_atr = InpUseAdverseAtrExit || (InpUseProfitAtrExit && InpTakeProfitATR > 0.0);
|
||||
const bool has_model_exit = InpPureRelative || InpUseCloseHeadExit || InpUseDirFlipExit;
|
||||
if(!has_atr && !has_model_exit)
|
||||
Print("US500_H1_ArticleEA: WARNING — no exit path enabled (enable InpPureRelative and/or legacy exits / ATR)");
|
||||
|
||||
Print("US500_H1_ArticleEA: ONNX OK. Chart TF=", EnumToString(PERIOD_CURRENT), "; Lookback=", InpLookback,
|
||||
" sampleEvery=", g_sample_n, " aggWindow=", g_agg_w, " minAggSamples=", g_min_agg_samples,
|
||||
" pureRelative=", InpPureRelative,
|
||||
" entryCooldownBars=", InpMinBarsBetweenEntries, " minDirEdge=", InpMinDirEdge,
|
||||
" stayOverClose=", InpRequireStayOverClose,
|
||||
" exitMode=", InpExitMode, " minBarsInTradeModelExit=", InpMinBarsInTradeModelExit,
|
||||
" closeHeadExit=", InpUseCloseHeadExit, " dirFlipExit=", InpUseDirFlipExit, " flipExitEdge=", InpFlipExitEdge,
|
||||
" minBarsAfterExit=", InpMinBarsAfterExit, " cooldownAfterAdverse=", InpCooldownBarsAfterAdverse,
|
||||
" useAdverseATR=", InpUseAdverseAtrExit, " useProfitATR=", InpUseProfitAtrExit,
|
||||
" maxAdverseATR=", InpMaxAdverseATR, " takeProfitATR=", InpTakeProfitATR);
|
||||
return INIT_SUCCEEDED;
|
||||
}
|
||||
|
||||
void OnDeinit(const int r)
|
||||
{
|
||||
if(g_onnx != INVALID_HANDLE) OnnxRelease(g_onnx);
|
||||
}
|
||||
|
||||
double AtrNow()
|
||||
{
|
||||
double b[];
|
||||
ArraySetAsSeries(b, true);
|
||||
int h = iATR(_Symbol, PERIOD_CURRENT, 14);
|
||||
if(h == INVALID_HANDLE) return 0;
|
||||
if(CopyBuffer(h, 0, 0, 2, b) < 1) { IndicatorRelease(h); return 0; }
|
||||
double v = b[0];
|
||||
IndicatorRelease(h);
|
||||
return v;
|
||||
}
|
||||
|
||||
bool AdverseExit(const long type, const double open_price)
|
||||
{
|
||||
if(!InpUseAdverseAtrExit || InpMaxAdverseATR <= 0.0)
|
||||
return false;
|
||||
double atr = AtrNow();
|
||||
if(atr <= 0) return false;
|
||||
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
||||
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
if(type == POSITION_TYPE_BUY)
|
||||
{
|
||||
double adv = (open_price - bid) / atr;
|
||||
return adv >= InpMaxAdverseATR;
|
||||
}
|
||||
double adv = (ask - open_price) / atr;
|
||||
return adv >= InpMaxAdverseATR;
|
||||
}
|
||||
|
||||
bool ProfitExit(const long type, const double open_price)
|
||||
{
|
||||
if(!InpUseProfitAtrExit || InpTakeProfitATR <= 0.0)
|
||||
return false;
|
||||
double atr = AtrNow();
|
||||
if(atr <= 0.0) return false;
|
||||
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
||||
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
if(type == POSITION_TYPE_BUY)
|
||||
return (bid - open_price) >= InpTakeProfitATR * atr;
|
||||
return (open_price - ask) >= InpTakeProfitATR * atr;
|
||||
}
|
||||
|
||||
bool ModelCloseLong(const double p0, const double p1, const double p3)
|
||||
{
|
||||
if(InpExitMode == 0)
|
||||
return (p3 >= InpProbCloseL);
|
||||
// Modes 1/2 (and default): close-long must beat HOLD and stay-long (BUY). Old mode-2 "vs HOLD only" fired almost every bar on softmax.
|
||||
return (p3 > p0 + InpMinCloseBeatHold && p3 > p1);
|
||||
}
|
||||
|
||||
bool ModelCloseShort(const double p0, const double p2, const double p4)
|
||||
{
|
||||
if(InpExitMode == 0)
|
||||
return (p4 >= InpProbCloseS);
|
||||
return (p4 > p0 + InpMinCloseBeatHold && p4 > p2);
|
||||
}
|
||||
|
||||
bool ModelDirFlipExitLong(const double p0, const double p1, const double p2)
|
||||
{
|
||||
if(!InpUseDirFlipExit)
|
||||
return false;
|
||||
const double e = MathMax(0.0, InpFlipExitEdge);
|
||||
return (p2 > p1 + e && p2 > p0 + InpMinBeatHold);
|
||||
}
|
||||
|
||||
bool ModelDirFlipExitShort(const double p0, const double p1, const double p2)
|
||||
{
|
||||
if(!InpUseDirFlipExit)
|
||||
return false;
|
||||
const double e = MathMax(0.0, InpFlipExitEdge);
|
||||
return (p1 > p2 + e && p1 > p0 + InpMinBeatHold);
|
||||
}
|
||||
|
||||
int TrioStrictWinner012(const double p0, const double p1, const double p2)
|
||||
{
|
||||
if(p0 > p1 + REL_EPS && p0 > p2 + REL_EPS)
|
||||
return 0;
|
||||
if(p1 > p0 + REL_EPS && p1 > p2 + REL_EPS)
|
||||
return 1;
|
||||
if(p2 > p0 + REL_EPS && p2 > p1 + REL_EPS)
|
||||
return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int FiveStrictWinner01234(const double p0, const double p1, const double p2, const double p3, const double p4)
|
||||
{
|
||||
const double p[5] = {p0, p1, p2, p3, p4};
|
||||
int best = 0;
|
||||
for(int k = 1; k < 5; k++)
|
||||
if(p[k] > p[best])
|
||||
best = k;
|
||||
const double m = p[best];
|
||||
int cnt = 0;
|
||||
for(int k = 0; k < 5; k++)
|
||||
if(p[k] + REL_EPS >= m)
|
||||
cnt++;
|
||||
if(cnt != 1)
|
||||
return -1;
|
||||
return best;
|
||||
}
|
||||
|
||||
int PositionBarsInTrade()
|
||||
{
|
||||
if(!PositionSelect(_Symbol))
|
||||
return 0;
|
||||
const datetime tOpen = (datetime)PositionGetInteger(POSITION_TIME);
|
||||
const int sh = iBarShift(_Symbol, PERIOD_CURRENT, tOpen, false);
|
||||
if(sh < 0)
|
||||
return 9999;
|
||||
return sh + 1;
|
||||
}
|
||||
|
||||
void ApplyExitCooldown(const bool adverse_stop)
|
||||
{
|
||||
int b = MathMax(0, InpMinBarsAfterExit);
|
||||
if(adverse_stop)
|
||||
b = MathMax(b, MathMax(0, InpCooldownBarsAfterAdverse));
|
||||
if(b > 0)
|
||||
g_entry_cooldown_bars = MathMax(g_entry_cooldown_bars, b);
|
||||
}
|
||||
|
||||
void PushPrediction(const double p0, const double p1, const double p2, const double p3, const double p4, const int maxKeep)
|
||||
{
|
||||
for(int i = PRED_HIST_CAP - 1; i > 0; i--)
|
||||
for(int k = 0; k < 5; k++)
|
||||
g_pred_hist[i][k] = g_pred_hist[i - 1][k];
|
||||
g_pred_hist[0][0] = p0;
|
||||
g_pred_hist[0][1] = p1;
|
||||
g_pred_hist[0][2] = p2;
|
||||
g_pred_hist[0][3] = p3;
|
||||
g_pred_hist[0][4] = p4;
|
||||
int cap = MathMax(1, MathMin(maxKeep, PRED_HIST_CAP));
|
||||
g_pred_hist_len = MathMin(g_pred_hist_len + 1, cap);
|
||||
}
|
||||
|
||||
void RecomputeSmooth(const int aggWindow)
|
||||
{
|
||||
int w = MathMax(1, MathMin(aggWindow, PRED_HIST_CAP));
|
||||
int n = MathMin(w, g_pred_hist_len);
|
||||
if(n < 1)
|
||||
return;
|
||||
for(int k = 0; k < 5; k++)
|
||||
{
|
||||
double s = 0.0;
|
||||
for(int i = 0; i < n; i++)
|
||||
s += g_pred_hist[i][k];
|
||||
g_smooth[k] = s / (double)n;
|
||||
}
|
||||
}
|
||||
|
||||
void ScaleFeatures(const float &raw[], float &out[])
|
||||
{
|
||||
for(int f = 0; f < FEAT_COUNT; f++)
|
||||
{
|
||||
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;
|
||||
|
||||
const bool had_pos = PositionSelect(_Symbol);
|
||||
const bool flat = !had_pos;
|
||||
if(flat && g_entry_cooldown_bars > 0)
|
||||
g_entry_cooldown_bars--;
|
||||
|
||||
g_bar_index++;
|
||||
const bool do_sample = (g_sample_n < 2) || ((g_bar_index % (ulong)g_sample_n) == 0);
|
||||
bool fresh_predict = false;
|
||||
|
||||
if(do_sample)
|
||||
{
|
||||
matrixf Min;
|
||||
if(!PrepareMatrix(Min))
|
||||
{
|
||||
Print("US500 Article EA: PrepareMatrix failed");
|
||||
if(!had_pos)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
vectorf out;
|
||||
out.Resize(5);
|
||||
if(!OnnxRun(g_onnx, ONNX_NO_CONVERSION, Min, out))
|
||||
{
|
||||
Print("OnnxRun failed ", GetLastError());
|
||||
if(!had_pos)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
PushPrediction(out[0], out[1], out[2], out[3], out[4], g_agg_w);
|
||||
RecomputeSmooth(g_agg_w);
|
||||
fresh_predict = true;
|
||||
Print("US500 Article H1 raw HOLD=", out[0], " BUY=", out[1], " SELL=", out[2], " CL=", out[3], " CS=", out[4],
|
||||
" | smooth HOLD=", g_smooth[0], " BUY=", g_smooth[1], " SELL=", g_smooth[2], " CL=", g_smooth[3], " CS=", g_smooth[4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const double p0 = g_smooth[0];
|
||||
const double p1 = g_smooth[1];
|
||||
const double p2 = g_smooth[2];
|
||||
const double p3 = g_smooth[3];
|
||||
const double p4 = g_smooth[4];
|
||||
|
||||
if(flat)
|
||||
{
|
||||
if(!do_sample || !fresh_predict)
|
||||
return;
|
||||
if(g_pred_hist_len < g_min_agg_samples)
|
||||
return;
|
||||
if(g_entry_cooldown_bars > 0)
|
||||
return;
|
||||
|
||||
if(InpEntryMode == 1)
|
||||
{
|
||||
if(InpPureRelative)
|
||||
{
|
||||
const int w3 = TrioStrictWinner012(p0, p1, p2);
|
||||
if(w3 == 1)
|
||||
{
|
||||
if(trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "US500 article BUY"))
|
||||
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
|
||||
}
|
||||
else if(w3 == 2)
|
||||
{
|
||||
if(trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "US500 article SELL"))
|
||||
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
double dir = MathMax(p1, p2);
|
||||
if(dir <= p0 + InpMinBeatHold)
|
||||
return;
|
||||
const double edge = MathMax(0.0, InpMinDirEdge);
|
||||
const bool stay_ok_buy = (!InpRequireStayOverClose) || (p1 > p3);
|
||||
const bool stay_ok_sell = (!InpRequireStayOverClose) || (p2 > p4);
|
||||
if(p1 >= p2 && p1 > p0 + InpMinBeatHold && (p1 - p2) >= edge && stay_ok_buy)
|
||||
{
|
||||
if(trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "US500 article BUY"))
|
||||
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
|
||||
}
|
||||
else if(p2 > p1 && p2 > p0 + InpMinBeatHold && (p2 - p1) >= edge && stay_ok_sell)
|
||||
{
|
||||
if(trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "US500 article SELL"))
|
||||
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(p1 >= InpProbBuy && p1 >= p2)
|
||||
{
|
||||
if(trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "US500 article BUY"))
|
||||
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
|
||||
}
|
||||
else if(p2 >= InpProbSell && p2 > p1)
|
||||
{
|
||||
if(trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "US500 article SELL"))
|
||||
g_entry_cooldown_bars = MathMax(0, InpMinBarsBetweenEntries);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
long typ = (long)PositionGetInteger(POSITION_TYPE);
|
||||
double opn = PositionGetDouble(POSITION_PRICE_OPEN);
|
||||
if(AdverseExit(typ, opn))
|
||||
{
|
||||
if(trade.PositionClose(_Symbol))
|
||||
ApplyExitCooldown(true);
|
||||
return;
|
||||
}
|
||||
if(ProfitExit(typ, opn))
|
||||
{
|
||||
if(trade.PositionClose(_Symbol))
|
||||
ApplyExitCooldown(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const int bars_in = PositionBarsInTrade();
|
||||
const bool allow_model_exit = (InpMinBarsInTradeModelExit <= 0) || (bars_in >= InpMinBarsInTradeModelExit);
|
||||
if(allow_model_exit)
|
||||
{
|
||||
bool want_close = false;
|
||||
if(InpPureRelative)
|
||||
{
|
||||
const int w5 = FiveStrictWinner01234(p0, p1, p2, p3, p4);
|
||||
if(typ == POSITION_TYPE_BUY)
|
||||
want_close = (w5 != -1 && w5 != 1);
|
||||
else
|
||||
want_close = (w5 != -1 && w5 != 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(typ == POSITION_TYPE_BUY)
|
||||
{
|
||||
const bool head = InpUseCloseHeadExit && ModelCloseLong(p0, p1, p3);
|
||||
const bool flip = ModelDirFlipExitLong(p0, p1, p2);
|
||||
want_close = (head || flip);
|
||||
}
|
||||
else
|
||||
{
|
||||
const bool head = InpUseCloseHeadExit && ModelCloseShort(p0, p2, p4);
|
||||
const bool flip = ModelDirFlipExitShort(p0, p1, p2);
|
||||
want_close = (head || flip);
|
||||
}
|
||||
}
|
||||
if(want_close)
|
||||
{
|
||||
if(trade.PositionClose(_Symbol))
|
||||
ApplyExitCooldown(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
; US500_H1_ArticleEA — Strategy Tester preset (fixed inputs, no optimization)
|
||||
; Copy to: MetaQuotes\Terminal\<instance>\MQL5\Profiles\Tester\
|
||||
; In Tester: Inputs tab → right‑click → Load → pick this file
|
||||
;
|
||||
; Notes:
|
||||
; - InpLookback must stay 48 unless you retrain/re‑embed ONNX with another lookback.
|
||||
; - Use micro lot (0.01) for tests; 1.0 lot caused very large exposure on US500.
|
||||
; - InpMinBeatHold / InpMinCloseBeatHold > 0 reduce churn when softmax is flat.
|
||||
;
|
||||
; Model
|
||||
InpLookback=48||48||1||48||N
|
||||
InpEntryMode=1||1||1||1||N
|
||||
InpProbBuy=0.18||0.18||0.02||0.30||N
|
||||
InpProbSell=0.18||0.18||0.02||0.30||N
|
||||
InpMinBeatHold=0.04||0.04||0.01||0.10||N
|
||||
InpExitMode=2||2||1||2||N
|
||||
InpProbCloseL=0.18||0.18||0.02||0.30||N
|
||||
InpProbCloseS=0.18||0.18||0.02||0.30||N
|
||||
InpMinCloseBeatHold=0.03||0.03||0.01||0.08||N
|
||||
; Session (match Python SESSION_HOUR_OFFSET)
|
||||
InpSessionHourOffset=0||0||1||1||N
|
||||
; Scaler override (empty = built‑in scaler from US500_H1_article_split_meta.json)
|
||||
InpFeatMinStr=
|
||||
InpFeatMaxStr=
|
||||
; Risk
|
||||
InpLotSize=0.01||0.01||0.01||0.10||N
|
||||
InpMagic=902503||902503||1||902503||N
|
||||
InpSlippage=30||30||1||300||N
|
||||
InpMaxAdverseATR=2.0||2.0||0.25||4.0||N
|
||||
InpTakeProfitATR=0.0||0.0||0.25||3.0||N
|
||||
@@ -0,0 +1,24 @@
|
||||
; US500_H1_ArticleEA — genetic / slow optimization preset
|
||||
; InpLookback fixed at 48 (must match embedded ONNX). Copy to MQL5\Profiles\Tester\
|
||||
;
|
||||
; Model
|
||||
InpLookback=48||48||1||48||N
|
||||
InpEntryMode=1||0||1||1||Y
|
||||
InpProbBuy=0.18||0.12||0.02||0.28||Y
|
||||
InpProbSell=0.18||0.12||0.02||0.28||Y
|
||||
InpMinBeatHold=0.04||0.0||0.01||0.10||Y
|
||||
InpExitMode=2||0||1||2||Y
|
||||
InpProbCloseL=0.18||0.12||0.02||0.28||Y
|
||||
InpProbCloseS=0.18||0.12||0.02||0.28||Y
|
||||
InpMinCloseBeatHold=0.03||0.0||0.01||0.08||Y
|
||||
; Session (match Python SESSION_HOUR_OFFSET)
|
||||
InpSessionHourOffset=0||-2||1||2||N
|
||||
; Scaler override (leave empty unless you paste new train bounds)
|
||||
InpFeatMinStr=
|
||||
InpFeatMaxStr=
|
||||
; Risk
|
||||
InpLotSize=0.01||0.01||0.01||0.10||N
|
||||
InpMagic=902503||902503||1||902503||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.
@@ -0,0 +1,30 @@
|
||||
# ENKS / clustering article — notes → training in this repo
|
||||
|
||||
Summary of the methodology described in the article (MQL5 / ENKS trader clusters):
|
||||
|
||||
- Models are trained in **Python**, then converted to **ENKS** for the MetaTrader include/bot stack. This repository does **not** ship an ENKS encoder; training here exports **ONNX + JSON meta + scaler** like `ai/xauusd_h1/`. Convert ENKS with the author’s tool or workflow from the article.
|
||||
- **Clustering** (article: Cayley / trade matching): use **forward-return fingerprints** per bar and **KMeans** on the in-sample window only, then optional **per-cluster balancing** of sample weights during training (see `train_article_split.py`).
|
||||
- **Windows**: train **2010-01-01 → 2019-12-31**; out-of-sample / forward **2020-01-01 → 2024-12-31**. Scaler is fit **only** on the train window (no leakage).
|
||||
- **Capital / Capodon-style US H1**: default symbol `US500` on **H1**; override with `YT_SYMBOL`. The article notes models can be attached on other timeframes; EA SL/TP and filters are tuned separately.
|
||||
- **Includes (`tendq`, etc.)**: not present in this repo; wire your ONNX EA to the exported `*_meta.json` and scaler like the existing XAUUSD H1 action EA.
|
||||
|
||||
## Run training
|
||||
|
||||
From `ai/yt` (MetaTrader 5 must be installed and history available for the symbol):
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python train_article_split.py
|
||||
```
|
||||
|
||||
Environment overrides:
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|----------------|----------------|----------------------------------|
|
||||
| `YT_SYMBOL` | `US500` | MT5 symbol |
|
||||
| `YT_LOOKBACK` | `48` | Sequence length (bars) |
|
||||
| `YT_EPOCHS` | `40` | Max epochs |
|
||||
| `YT_BATCH` | `64` | Batch size |
|
||||
| `YT_CLUSTERS` | `12` | KMeans clusters (0 = disable) |
|
||||
|
||||
Outputs: `ai/yt/models/<SYMBOL>_H1_article_split.onnx`, scaler `.pkl`, `*_meta.json`.
|
||||
Binary file not shown.
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"symbol": "US500",
|
||||
"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"
|
||||
],
|
||||
"train_window": [
|
||||
"2010-01-01",
|
||||
"2020-01-01"
|
||||
],
|
||||
"oos_window": [
|
||||
"2020-01-01",
|
||||
"2025-01-01"
|
||||
],
|
||||
"scaler_fit_on": "train_only_rows_before_2020",
|
||||
"clustering": "KMeans n=12 on forward returns (1,2,4,8,16) ATR-norm; sample reweight train",
|
||||
"scaler_feature_min": [
|
||||
1352.5,
|
||||
1352.5999755859375,
|
||||
1347.9000244140625,
|
||||
1352.0999755859375,
|
||||
0.0,
|
||||
0.04497450217604637,
|
||||
-0.030356179922819138,
|
||||
-0.04839427396655083,
|
||||
0.00028562467196024954,
|
||||
-0.047754231840372086,
|
||||
1.0,
|
||||
0.00017100000695791095,
|
||||
0.0,
|
||||
0.01168255414813757,
|
||||
0.0760856345295906,
|
||||
-0.49618232250213623,
|
||||
-1.6348180770874023,
|
||||
-1.731970191001892,
|
||||
6.116794793342706e-06,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"scaler_feature_max": [
|
||||
3250.199951171875,
|
||||
3251.5,
|
||||
3249.5,
|
||||
3250.199951171875,
|
||||
26050000896.0,
|
||||
0.887104868888855,
|
||||
0.09538312256336212,
|
||||
0.10739167034626007,
|
||||
0.02898731827735901,
|
||||
0.036042287945747375,
|
||||
1.0754634141921997,
|
||||
6759499776.0,
|
||||
20.0,
|
||||
0.9637425541877747,
|
||||
0.8267387747764587,
|
||||
0.5573697686195374,
|
||||
1.2618913650512695,
|
||||
1.8784747123718262,
|
||||
0.9100509881973267,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scaler_scale": [
|
||||
0.0005269537214189768,
|
||||
0.0005266206571832299,
|
||||
0.0005258729797787964,
|
||||
0.0005268426612019539,
|
||||
3.8387713147125524e-11,
|
||||
1.1874645948410034,
|
||||
7.952962398529053,
|
||||
6.419064044952393,
|
||||
34.84115219116211,
|
||||
11.933670043945312,
|
||||
13.25145435333252,
|
||||
1.4793993807771244e-10,
|
||||
0.05000000074505806,
|
||||
1.05035400390625,
|
||||
1.332173228263855,
|
||||
0.949169933795929,
|
||||
0.34521928429603577,
|
||||
0.2769741714000702,
|
||||
1.0988469123840332,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"oos_val_accuracy": 0.293920636177063,
|
||||
"oos_val_loss": 1.5438036918640137,
|
||||
"notes": "ONNX for repo EAs; convert to ENKS externally if required."
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
-r ../xauusd_h1/requirements.txt
|
||||
@@ -0,0 +1,315 @@
|
||||
"""
|
||||
Article-style training: chronological train (2010–2019) vs OOS (2020–2024),
|
||||
optional KMeans on forward-return fingerprints (trade-shape clustering),
|
||||
ONNX export compatible with the repo's MT5 ONNX EAs.
|
||||
|
||||
Reuses feature + label definitions from ai/xauusd_h1 (24 features, 5 classes).
|
||||
ENKS conversion is out of scope — use the article's tooling after ONNX if needed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import tensorflow as tf
|
||||
import tf2onnx
|
||||
import onnx
|
||||
from sklearn.cluster import KMeans
|
||||
from sklearn.preprocessing import MinMaxScaler
|
||||
from tensorflow import keras
|
||||
from tensorflow.keras import layers
|
||||
from tqdm import tqdm
|
||||
|
||||
# Reuse XAUUSD H1 stack (same 24 dims / 5 classes as published EAs).
|
||||
_XH1 = Path(__file__).resolve().parent.parent / "xauusd_h1"
|
||||
sys.path.insert(0, str(_XH1))
|
||||
from features import NUM_FEATURES, prepare_features_full # noqa: E402
|
||||
from labeling import atr_series, class_weights, compute_action_labels # noqa: E402
|
||||
|
||||
NUM_CLASSES = 5
|
||||
CLASS_NAMES = ["HOLD", "BUY", "SELL_SHORT", "CLOSE_LONG", "CLOSE_SHORT"]
|
||||
|
||||
TRAIN_START = pd.Timestamp("2010-01-01")
|
||||
TRAIN_END = pd.Timestamp("2020-01-01") # exclusive: train < this
|
||||
OOS_END = pd.Timestamp("2025-01-01") # exclusive: val < this (covers through 2024)
|
||||
|
||||
|
||||
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 symbol history in MT5")
|
||||
|
||||
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 forward_return_fingerprints(
|
||||
df: pd.DataFrame,
|
||||
feat_index: pd.DatetimeIndex,
|
||||
horizons: tuple[int, ...] = (1, 2, 4, 8, 16),
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Normalized forward returns at listed horizons; aligned to feat rows."""
|
||||
close = df["close"].to_numpy(dtype=np.float64)
|
||||
atr = atr_series(df, 14).to_numpy(dtype=np.float64)
|
||||
pos = df.index.get_indexer(feat_index)
|
||||
n = len(feat_index)
|
||||
d = len(horizons)
|
||||
M = np.zeros((n, d), dtype=np.float64)
|
||||
valid = np.ones(n, dtype=bool)
|
||||
max_h = max(horizons)
|
||||
for j, i in enumerate(pos):
|
||||
if i < 0 or i + max_h >= len(close):
|
||||
valid[j] = False
|
||||
continue
|
||||
a = float(atr[i]) if np.isfinite(atr[i]) and atr[i] > 0 else close[i] * 1e-4
|
||||
for k, h in enumerate(horizons):
|
||||
if i + h >= len(close):
|
||||
valid[j] = False
|
||||
break
|
||||
M[j, k] = (close[i + h] - close[i]) / a
|
||||
return M, valid
|
||||
|
||||
|
||||
def create_sequences(
|
||||
X: np.ndarray,
|
||||
y: np.ndarray,
|
||||
times: np.ndarray,
|
||||
lookback: int,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
xs, ys, t_end = [], [], []
|
||||
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])
|
||||
t_end.append(times[i])
|
||||
return (
|
||||
np.asarray(xs, dtype=np.float32),
|
||||
np.asarray(ys, dtype=np.int64),
|
||||
np.asarray(t_end),
|
||||
)
|
||||
|
||||
|
||||
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("YT_SYMBOL", "US500")
|
||||
lookback = int(os.environ.get("YT_LOOKBACK", "48"))
|
||||
epochs = int(os.environ.get("YT_EPOCHS", "40"))
|
||||
batch_size = int(os.environ.get("YT_BATCH", "64"))
|
||||
n_clusters = int(os.environ.get("YT_CLUSTERS", "12"))
|
||||
|
||||
fetch_start = datetime(2009, 6, 1)
|
||||
fetch_end = datetime(2025, 1, 1)
|
||||
|
||||
out_dir = Path(__file__).resolve().parent / "models"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
onnx_path = out_dir / f"{symbol}_H1_article_split.onnx"
|
||||
meta_path = out_dir / f"{symbol}_H1_article_split_meta.json"
|
||||
|
||||
print(f"Symbol={symbol} H1 | train [{TRAIN_START.date()} , {TRAIN_END.date()}) | OOS [{TRAIN_END.date()} , {OOS_END.date()})")
|
||||
print("Fetching MT5 …")
|
||||
try:
|
||||
raw = fetch_mt5_range(symbol, mt5.TIMEFRAME_H1, fetch_start, fetch_end)
|
||||
finally:
|
||||
mt5.shutdown()
|
||||
|
||||
raw = raw.loc[raw.index >= TRAIN_START]
|
||||
if len(raw) < 500:
|
||||
print("ERROR: Not enough H1 bars after 2010 — check symbol and History Center.")
|
||||
return 1
|
||||
|
||||
print(f"Bars: {len(raw)} range: {raw.index[0]} → {raw.index[-1]}")
|
||||
|
||||
feat = prepare_features_full(raw)
|
||||
labels = compute_action_labels(raw).loc[feat.index]
|
||||
y = labels.values.astype(np.int64)
|
||||
X_raw = feat.values.astype(np.float32)
|
||||
times = feat.index.to_numpy()
|
||||
|
||||
valid = np.isfinite(X_raw).all(axis=1) & (y >= 0) & (y < NUM_CLASSES)
|
||||
X_raw = X_raw[valid]
|
||||
y = y[valid]
|
||||
times = times[valid]
|
||||
|
||||
train_row = times < np.datetime64(TRAIN_END)
|
||||
if int(train_row.sum()) < 800:
|
||||
print("ERROR: Too few train rows before 2020 — need deeper history.")
|
||||
return 1
|
||||
|
||||
scaler = MinMaxScaler()
|
||||
scaler.fit(X_raw[train_row])
|
||||
Xn = scaler.transform(X_raw).astype(np.float32)
|
||||
|
||||
X_seq, y_seq, t_end = create_sequences(Xn, y, times, lookback)
|
||||
if len(X_seq) < 500:
|
||||
print("ERROR: Too few sequences.")
|
||||
return 1
|
||||
|
||||
train_m = t_end < np.datetime64(TRAIN_END)
|
||||
val_m = (t_end >= np.datetime64(TRAIN_END)) & (t_end < np.datetime64(OOS_END))
|
||||
X_train, y_train = X_seq[train_m], y_seq[train_m]
|
||||
X_val, y_val = X_seq[val_m], y_seq[val_m]
|
||||
if len(X_val) < 200:
|
||||
print("ERROR: Too few OOS sequences in 2020–2024.")
|
||||
return 1
|
||||
|
||||
print(f"Sequences train={len(X_train)} val(OOS)={len(X_val)} lookback={lookback}")
|
||||
|
||||
cw = class_weights(y_train, NUM_CLASSES)
|
||||
tr_idx = np.flatnonzero(train_m)
|
||||
base_w = np.array([cw[int(y_seq[i])] for i in tr_idx], dtype=np.float32)
|
||||
sample_w = base_w.copy()
|
||||
|
||||
if n_clusters > 1:
|
||||
fp, fp_ok = forward_return_fingerprints(raw, pd.DatetimeIndex(times))
|
||||
fp_seq = fp[lookback - 1 :]
|
||||
ok_seq = fp_ok[lookback - 1 :]
|
||||
fp_tr = fp_seq[tr_idx]
|
||||
ok_tr = ok_seq[tr_idx]
|
||||
fit_mask = ok_tr & np.isfinite(fp_tr).all(axis=1)
|
||||
if int(fit_mask.sum()) >= n_clusters * 5:
|
||||
km = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
|
||||
km.fit(fp_tr[fit_mask])
|
||||
labels_tr = np.full(len(tr_idx), -1, dtype=np.int32)
|
||||
labels_tr[fit_mask] = km.predict(fp_tr[fit_mask])
|
||||
counts = np.zeros(n_clusters, dtype=np.float64)
|
||||
for c in labels_tr:
|
||||
if 0 <= c < n_clusters:
|
||||
counts[c] += 1.0
|
||||
counts = np.maximum(counts, 1.0)
|
||||
total_assigned = max(int((labels_tr >= 0).sum()), 1)
|
||||
w_cl = np.ones(len(tr_idx), dtype=np.float32)
|
||||
for j in range(len(tr_idx)):
|
||||
c = int(labels_tr[j])
|
||||
if c >= 0:
|
||||
w_cl[j] = float(total_assigned / (n_clusters * counts[c]))
|
||||
sample_w = base_w * w_cl
|
||||
sample_w *= len(sample_w) / float(np.sum(sample_w))
|
||||
print(
|
||||
f"KMeans trade-fingerprint clusters={n_clusters} "
|
||||
"(fit on in-sample train only; sample_weight × class balance)"
|
||||
)
|
||||
else:
|
||||
print("Skipping KMeans: not enough valid fingerprint rows in train.")
|
||||
|
||||
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=10, restore_best_weights=True
|
||||
),
|
||||
keras.callbacks.ReduceLROnPlateau(
|
||||
monitor="val_loss", factor=0.5, patience=4, min_lr=1e-6
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
loss, acc = model.evaluate(X_val, y_val, verbose=0)
|
||||
print(f"OOS val_loss={loss:.4f} val_accuracy={acc:.4f}")
|
||||
|
||||
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, str(onnx_path))
|
||||
|
||||
with open(str(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,
|
||||
"train_window": [str(TRAIN_START.date()), str(TRAIN_END.date())],
|
||||
"oos_window": [str(TRAIN_END.date()), str(OOS_END.date())],
|
||||
"scaler_fit_on": "train_only_rows_before_2020",
|
||||
"clustering": f"KMeans n={n_clusters} on forward returns (1,2,4,8,16) ATR-norm; sample reweight train" if n_clusters > 1 else "disabled",
|
||||
"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,
|
||||
"oos_val_accuracy": float(acc),
|
||||
"oos_val_loss": float(loss),
|
||||
"notes": "ONNX for repo EAs; convert to ENKS externally if required.",
|
||||
}
|
||||
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 (%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}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user