This commit is contained in:
zhutoutoutousan
2026-05-02 15:55:04 +02:00
parent 0b4a70b843
commit b5acd37754
198 changed files with 18739 additions and 11364 deletions
+386
View File
@@ -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.
+37
View File
@@ -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 20102020 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.
+316
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
-r ../xauusd_h1/requirements.txt