Files
zhutoutoutousan b50b430d1a Update
2026-04-09 11:47:56 +02:00

211 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
XAUUSD M15 — ONNX action model (buy / sell short / close long / close short / hold).
Features: 24 dims — base 13 + RSI/frontline stack (see features.py, FRONTLINE_RSI_INTEGRATION.md).
Row order matches XAUUSD_M15_ActionEA.mq5 (row 0 = newest bar).
Data: MT5, 20082026 (limited by downloaded history).
"""
from __future__ import annotations
import json
import os
import pickle
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from tensorflow import keras
from tensorflow.keras import layers
from tqdm import tqdm
import tf2onnx
import onnx
from labeling import class_weights, compute_action_labels
from features import NUM_FEATURES, prepare_features_full
NUM_CLASSES = 5
CLASS_NAMES = ["HOLD", "BUY", "SELL_SHORT", "CLOSE_LONG", "CLOSE_SHORT"]
def fetch_mt5_range(
symbol: str,
timeframe: int,
start_date: datetime,
end_date: datetime,
) -> pd.DataFrame:
if not mt5.initialize():
raise RuntimeError(f"MT5 init failed: {mt5.last_error()}")
info = mt5.symbol_info(symbol)
if info is None:
mt5.shutdown()
raise ValueError(f"Symbol {symbol} not found")
if not info.visible and not mt5.symbol_select(symbol, True):
mt5.shutdown()
raise ValueError(f"Cannot select {symbol}")
all_rows: list[dict] = []
chunk_days = 30
cur = start_date
while cur < end_date:
chunk_end = min(cur + timedelta(days=chunk_days), end_date)
rates = mt5.copy_rates_range(symbol, timeframe, cur, chunk_end)
if rates is not None and len(rates) > 1:
for row in rates:
all_rows.append({n: row[n] for n in rates.dtype.names})
cur = chunk_end
if not all_rows:
mt5.shutdown()
raise ValueError("No rates returned — download XAUUSD M15 in MT5 History Center")
df = pd.DataFrame(all_rows)
df["time"] = pd.to_datetime(df["time"], unit="s")
df = df.set_index("time").sort_index()
df = df[~df.index.duplicated(keep="first")]
return df
def create_sequences(
X: np.ndarray, y: np.ndarray, lookback: int
) -> tuple[np.ndarray, np.ndarray]:
"""
Window ends at bar i (chronological). Rows: newest-first inside each window
(matches MT5 series arrays in EA).
"""
xs, ys = [], []
for i in tqdm(range(lookback - 1, len(X)), desc="sequences"):
window = X[i - lookback + 1 : i + 1].copy()
window = window[::-1] # newest bar first → same as EA matrix row 0
xs.append(window)
ys.append(y[i])
return np.asarray(xs, dtype=np.float32), np.asarray(ys, dtype=np.int64)
def build_model(lookback: int, n_feat: int) -> keras.Model:
inp = layers.Input(shape=(lookback, n_feat))
x = layers.LSTM(96, return_sequences=True)(inp)
x = layers.Dropout(0.25)(x)
x = layers.LSTM(48)(x)
x = layers.Dropout(0.25)(x)
x = layers.Dense(32, activation="relu")(x)
out = layers.Dense(NUM_CLASSES, activation="softmax", name="action_probs")(x)
model = keras.Model(inp, out)
model.compile(
optimizer=keras.optimizers.Adam(1e-3),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
return model
def main() -> int:
symbol = os.environ.get("XAU_SYMBOL", "XAUUSD")
lookback = int(os.environ.get("XAU_LOOKBACK", "64"))
epochs = int(os.environ.get("XAU_EPOCHS", "40"))
batch_size = int(os.environ.get("XAU_BATCH", "64"))
start_date = datetime(2008, 1, 1)
end_date = datetime(2026, 12, 31)
out_dir = os.path.join(os.path.dirname(__file__), "models")
os.makedirs(out_dir, exist_ok=True)
onnx_path = os.path.join(out_dir, f"{symbol}_M15_action.onnx")
meta_path = os.path.join(out_dir, f"{symbol}_M15_action_meta.json")
print("Fetching MT5 data …")
try:
raw = fetch_mt5_range(symbol, mt5.TIMEFRAME_M15, start_date, end_date)
finally:
mt5.shutdown()
print(f"Bars: {len(raw)} range: {raw.index[0]}{raw.index[-1]}")
feat = prepare_features_full(raw)
labels_full = compute_action_labels(raw)
labels = labels_full.loc[feat.index]
y = labels.loc[feat.index].values.astype(np.int64)
X_raw = feat.values.astype(np.float32)
valid = np.isfinite(X_raw).all(axis=1) & (y >= 0) & (y < NUM_CLASSES)
X_raw = X_raw[valid]
y = y[valid]
print("Label counts:", {CLASS_NAMES[i]: int((y == i).sum()) for i in range(NUM_CLASSES)})
scaler = MinMaxScaler()
Xn = scaler.fit_transform(X_raw).astype(np.float32)
X_seq, y_seq = create_sequences(Xn, y, lookback)
if len(X_seq) < 500:
print("ERROR: Too few sequences — need more M15 history in MT5.")
return 1
X_train, X_val, y_train, y_val = train_test_split(
X_seq, y_seq, test_size=0.15, shuffle=False
)
cw = class_weights(y_train, NUM_CLASSES)
sample_w = np.array([cw[int(c)] for c in y_train], dtype=np.float32)
model = build_model(lookback, NUM_FEATURES)
model.summary()
model.fit(
X_train,
y_train,
sample_weight=sample_w,
validation_data=(X_val, y_val),
epochs=epochs,
batch_size=batch_size,
verbose=1,
callbacks=[
keras.callbacks.EarlyStopping(
monitor="val_loss", patience=8, restore_best_weights=True
),
keras.callbacks.ReduceLROnPlateau(
monitor="val_loss", factor=0.5, patience=4, min_lr=1e-6
),
],
)
spec = (tf.TensorSpec((None, lookback, NUM_FEATURES), tf.float32, name="input"),)
onnx_m, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=13)
onnx.save_model(onnx_m, onnx_path)
with open(onnx_path.replace(".onnx", "_scaler.pkl"), "wb") as f:
pickle.dump(scaler, f)
meta = {
"symbol": symbol,
"timeframe": "M15",
"lookback": lookback,
"num_features": int(NUM_FEATURES),
"feature_columns": feat.columns.tolist(),
"num_classes": NUM_CLASSES,
"class_names": CLASS_NAMES,
"scaler_feature_min": scaler.data_min_.tolist(),
"scaler_feature_max": scaler.data_max_.tolist(),
"scaler_scale": scaler.scale_.tolist() if hasattr(scaler, "scale_") else None,
"notes": "MinMax in EA; row0=newest. See FRONTLINE_RSI_INTEGRATION.md.",
}
with open(meta_path, "w", encoding="utf-8") as f:
json.dump(meta, f, indent=2)
print(f"Saved: {onnx_path}")
print(f"Meta: {meta_path}")
print("\n--- Paste into EA InpFeatMinStr / InpFeatMaxStr (comma-separated, %d floats each) ---" % NUM_FEATURES)
print(",".join(f"{x:.8g}" for x in scaler.data_min_))
print(",".join(f"{x:.8g}" for x in scaler.data_max_))
return 0
if __name__ == "__main__":
sys.exit(main())