Initial commit: Mad Turtle v2.0 ML EA for XAUUSD H1 with Python inference server and MQL5 EA
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
Mad Turtle Inference Server
|
||||
FastAPI service that loads ONNX ensemble models and exposes REST endpoints
|
||||
for MT5 EA to get BUY/SELL/HOLD signals + confidence scores.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("mad_turtle_server")
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MODELS_DIR = ROOT / "models"
|
||||
META_PATH = MODELS_DIR / "metadata.json"
|
||||
|
||||
app = FastAPI(title="Mad Turtle Inference Server", version="2.0.0")
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
models_loaded: int
|
||||
uptime_seconds: float
|
||||
|
||||
class SignalRequest(BaseModel):
|
||||
features: list[float] = Field(..., min_length=14, max_length=14)
|
||||
model: Optional[str] = "ensemble"
|
||||
|
||||
class SignalResponse(BaseModel):
|
||||
signal: str
|
||||
confidence: float
|
||||
buy_prob: float
|
||||
sell_prob: float
|
||||
hold_prob: float
|
||||
model_version: str
|
||||
|
||||
class OHLCVRequest(BaseModel):
|
||||
open: float
|
||||
high: float
|
||||
low: float
|
||||
close: float
|
||||
volume: float
|
||||
|
||||
class Engine:
|
||||
def __init__(self):
|
||||
self.sessions: dict[str, ort.InferenceSession] = {}
|
||||
self.metadata: dict = {}
|
||||
self.feature_names: list[str] = []
|
||||
self.started_at: Optional[str] = None
|
||||
|
||||
def load(self):
|
||||
self.started_at = datetime.now(timezone.utc).isoformat()
|
||||
if not META_PATH.exists():
|
||||
raise FileNotFoundError(f"metadata.json not found at {META_PATH}. Run build_onnx_raw.py first.")
|
||||
with open(META_PATH) as f:
|
||||
self.metadata = json.load(f)
|
||||
self.feature_names = self.metadata["features"]
|
||||
|
||||
for name, info in self.metadata.get("models", {}).items():
|
||||
path = ROOT / info["path"]
|
||||
if not path.exists():
|
||||
logger.warning("Model file missing: %s", path)
|
||||
continue
|
||||
sess = ort.InferenceSession(str(path), providers=["CPUExecutionProvider"])
|
||||
self.sessions[name] = sess
|
||||
logger.info("Loaded model '%s' from %s", name, path)
|
||||
|
||||
def predict(self, features: list[float], model_name: str = "ensemble") -> dict:
|
||||
if model_name not in self.sessions:
|
||||
raise ValueError(f"Model '{model_name}' not loaded. Available: {list(self.sessions.keys())}")
|
||||
if len(features) != len(self.feature_names):
|
||||
raise ValueError(f"Expected {len(self.feature_names)} features, got {len(features)}")
|
||||
|
||||
x = np.array([features], dtype=np.float32)
|
||||
sess = self.sessions[model_name]
|
||||
input_name = sess.get_inputs()[0].name
|
||||
outputs = sess.run(None, {input_name: x})[0]
|
||||
probs = outputs[0]
|
||||
classes = ["SELL", "HOLD", "BUY"]
|
||||
idx = int(np.argmax(probs))
|
||||
return {
|
||||
"signal": classes[idx],
|
||||
"confidence": float(probs[idx]),
|
||||
"buy_prob": float(probs[2]),
|
||||
"sell_prob": float(probs[0]),
|
||||
"hold_prob": float(probs[1]),
|
||||
"model_version": self.metadata.get("built_at", "unknown"),
|
||||
}
|
||||
|
||||
def engineer_features(self, ohlcv: dict) -> list[float]:
|
||||
import pandas as pd
|
||||
df = pd.DataFrame([ohlcv])
|
||||
df["returns_1"] = np.log(df["close"] / df["open"])
|
||||
df["returns_3"] = np.log(df["close"] / df["close"])
|
||||
df["returns_6"] = np.log(df["close"] / df["close"])
|
||||
df["sma_10"] = df["close"]
|
||||
df["sma_20"] = df["close"]
|
||||
df["sma_50"] = df["close"]
|
||||
df["ema_12"] = df["close"]
|
||||
df["ema_26"] = df["close"]
|
||||
df["macd"] = 0.0
|
||||
df["macd_signal"] = 0.0
|
||||
delta = df["close"].diff().fillna(0)
|
||||
gain = delta.clip(lower=0).rolling(14).mean().fillna(0)
|
||||
loss = (-delta.clip(upper=0)).rolling(14).mean().fillna(0)
|
||||
rs = gain / (loss + 1e-9)
|
||||
df["rsi_14"] = (100.0 - (100.0 / (1.0 + rs))).fillna(50.0)
|
||||
df["atr_14"] = (df["high"] - df["low"]).fillna(0.0)
|
||||
df["atr_pct"] = (df["atr_14"] / (df["close"] + 1e-9)).fillna(0.0)
|
||||
df["vol_ratio"] = 1.0
|
||||
df["high_low_range"] = ((df["high"] - df["low"]) / (df["close"] + 1e-9)).fillna(0.0)
|
||||
df["dist_sma20"] = 0.0
|
||||
row = df.iloc[-1]
|
||||
return [float(row[c]) for c in self.feature_names]
|
||||
|
||||
engine = Engine()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
engine.load()
|
||||
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def health():
|
||||
now = datetime.now(timezone.utc)
|
||||
start = datetime.fromisoformat(engine.started_at) if engine.started_at else now
|
||||
return HealthResponse(
|
||||
status="ok" if engine.sessions else "degraded",
|
||||
models_loaded=len(engine.sessions),
|
||||
uptime_seconds=(now - start).total_seconds(),
|
||||
)
|
||||
|
||||
@app.post("/v1/signal", response_model=SignalResponse)
|
||||
async def get_signal(req: SignalRequest):
|
||||
try:
|
||||
res = engine.predict(req.features, req.model or "ensemble")
|
||||
return SignalResponse(**res)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@app.post("/v1/signal/ohlcv", response_model=SignalResponse)
|
||||
async def get_signal_ohlcv(req: OHLCVRequest):
|
||||
try:
|
||||
feats = engine.engineer_features(req.dict())
|
||||
res = engine.predict(feats)
|
||||
return SignalResponse(**res)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Build a minimal ONNX ensemble model using raw ONNX ops (no sklearn needed).
|
||||
Creates a simple linear classifier + softmax as a demo.
|
||||
Replace this with a real trained model later (Python 3.11/3.12 + sklearn + skl2onnx).
|
||||
"""
|
||||
|
||||
import json
|
||||
import numpy as np
|
||||
import onnx
|
||||
from onnx import helper, TensorProto, numpy_helper
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
MODELS_DIR = ROOT / "models"
|
||||
MODELS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
FEATURES = [
|
||||
"returns_1", "returns_3", "returns_6",
|
||||
"sma_10", "sma_20", "sma_50",
|
||||
"macd", "macd_signal",
|
||||
"rsi_14", "atr_14", "atr_pct",
|
||||
"vol_ratio", "high_low_range", "dist_sma20",
|
||||
]
|
||||
|
||||
# Dummy weights for demo — replace with real trained weights
|
||||
rng = np.random.default_rng(123)
|
||||
W = rng.standard_normal((14, 3)).astype(np.float32) * 0.1
|
||||
b = np.zeros(3, dtype=np.float32)
|
||||
|
||||
# Normalize weights roughly
|
||||
W = W / np.maximum(np.abs(W).sum(axis=0, keepdims=True), 1e-6)
|
||||
|
||||
scale = np.array([
|
||||
1000.0, 1000.0, 1000.0,
|
||||
1.0, 1.0, 1.0,
|
||||
10.0, 10.0,
|
||||
1.0, 10.0, 1.0,
|
||||
1.0, 1.0, 1.0,
|
||||
], dtype=np.float32).reshape(1, 14)
|
||||
|
||||
bias = np.zeros((1, 14), dtype=np.float32)
|
||||
|
||||
|
||||
def build_model():
|
||||
# Input
|
||||
x = helper.make_tensor_value_info("float_input", TensorProto.FLOAT, [None, 14])
|
||||
|
||||
# Scale nodes: x_scaled = x * scale + bias
|
||||
scale_tensor = numpy_helper.from_array(scale, name="scale")
|
||||
bias_tensor = numpy_helper.from_array(bias, name="bias")
|
||||
mul_node = helper.make_node("Mul", ["float_input", "scale"], ["x_scaled"])
|
||||
add_node = helper.make_node("Add", ["x_scaled", "bias"], ["x_scaled_centered"])
|
||||
|
||||
# Linear layer: logits = x_scaled @ W + b
|
||||
W_tensor = numpy_helper.from_array(W, name="W")
|
||||
b_tensor = numpy_helper.from_array(b, name="b")
|
||||
matmul_node = helper.make_node("MatMul", ["x_scaled_centered", "W"], ["logits"])
|
||||
add_bias_node = helper.make_node("Add", ["logits", "b"], ["logits_biased"])
|
||||
|
||||
# Softmax
|
||||
softmax_node = helper.make_node("Softmax", ["logits_biased"], ["probs"], axis=1)
|
||||
|
||||
# Output
|
||||
y = helper.make_tensor_value_info("probs", TensorProto.FLOAT, [None, 3])
|
||||
|
||||
graph = helper.make_graph(
|
||||
[mul_node, add_node, matmul_node, add_bias_node, softmax_node],
|
||||
"mad_turtle_ensemble",
|
||||
[x],
|
||||
[y],
|
||||
[scale_tensor, bias_tensor, W_tensor, b_tensor],
|
||||
)
|
||||
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 15)])
|
||||
model.ir_version = 8
|
||||
onnx.checker.check_model(model)
|
||||
out = MODELS_DIR / "xauusd_h1_ensemble.onnx"
|
||||
onnx.save(model, str(out))
|
||||
print(f"Saved demo ONNX model -> {out}")
|
||||
|
||||
|
||||
def save_metadata():
|
||||
meta = {
|
||||
"symbol": "XAUUSD",
|
||||
"timeframe": "H1",
|
||||
"features": FEATURES,
|
||||
"target_horizon": 3,
|
||||
"built_at": __import__('datetime').datetime.utcnow().isoformat(),
|
||||
"models": {
|
||||
"ensemble": {
|
||||
"features": FEATURES,
|
||||
"path": "models/xauusd_h1_ensemble.onnx",
|
||||
"note": "Demo model with random weights. Replace with real trained model.",
|
||||
}
|
||||
},
|
||||
}
|
||||
out = MODELS_DIR / "metadata.json"
|
||||
with open(out, "w") as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
print(f"Saved metadata -> {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_model()
|
||||
save_metadata()
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Fetch real XAUUSD H1 data from Yahoo Finance (GC=F gold futures).
|
||||
Saves to CSV for training pipeline.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
import yfinance as yf
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = ROOT / "data"
|
||||
DATA_DIR.mkdir(exist_ok=True)
|
||||
OUT_CSV = DATA_DIR / "xauusd_h1.csv"
|
||||
|
||||
|
||||
def fetch_xauusd(period: str = "2y", interval: str = "1h") -> pd.DataFrame:
|
||||
ticker = yf.Ticker("GC=F")
|
||||
df = ticker.history(period=period, interval=interval, auto_adjust=True)
|
||||
if df.empty:
|
||||
raise RuntimeError("No data returned from Yahoo Finance for GC=F")
|
||||
df.index = df.index.tz_convert("UTC").tz_localize(None)
|
||||
df.index.name = "datetime"
|
||||
df.reset_index(inplace=True)
|
||||
df.rename(columns={
|
||||
"Open": "open", "High": "high", "Low": "low",
|
||||
"Close": "close", "Volume": "volume"
|
||||
}, inplace=True)
|
||||
df = df[["datetime", "open", "high", "low", "close", "volume"]].copy()
|
||||
df.dropna(subset=["open", "high", "low", "close"], inplace=True)
|
||||
df["volume"] = df["volume"].fillna(0)
|
||||
return df
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--period", default="2y", help="yfinance period (1y, 2y, max)")
|
||||
parser.add_argument("--interval", default="1h", help="yfinance interval (1h, 4h, 1d)")
|
||||
parser.add_argument("--out", default=str(OUT_CSV))
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Fetching XAUUSD (GC=F) {args.interval} for {args.period}...")
|
||||
df = fetch_xauusd(args.period, args.interval)
|
||||
df.to_csv(args.out, index=False)
|
||||
print(f"Saved {len(df)} rows -> {args.out}")
|
||||
print(df.tail(3).to_string(index=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Mad Turtle ML Pipeline
|
||||
- Generates/loads OHLCV features for XAUUSD H1
|
||||
- Trains ensemble models (BUY/SELL sub-models)
|
||||
- Exports to ONNX for MT5 inference via REST bridge
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
import onnx
|
||||
import onnxruntime as ort
|
||||
|
||||
try:
|
||||
from skl2onnx import convert_sklearn
|
||||
from skl2onnx.common.data_types import FloatTensorType
|
||||
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier, VotingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import classification_report, accuracy_score
|
||||
import joblib
|
||||
HAS_SKLEARN = True
|
||||
except Exception as e:
|
||||
HAS_SKLEARN = False
|
||||
SKLEARN_IMPORT_ERROR = str(e)
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = ROOT / "data"
|
||||
MODELS_DIR = ROOT / "models"
|
||||
DATA_DIR.mkdir(exist_ok=True)
|
||||
MODELS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def generate_synthetic_gold_data(days: int = 2000, seed: int = 42) -> pd.DataFrame:
|
||||
"""Generate realistic synthetic XAUUSD H1 data when no real feed is available."""
|
||||
rng = np.random.default_rng(seed)
|
||||
n = days * 24
|
||||
base = 1800.0
|
||||
returns = rng.normal(loc=0.00002, scale=0.0008, size=n)
|
||||
prices = base * np.exp(np.cumsum(returns))
|
||||
df = pd.DataFrame({"close": prices})
|
||||
df["open"] = df["close"].shift(1).fillna(base)
|
||||
df["high"] = df[["open", "close"]].max(axis=1) * (1 + np.abs(rng.normal(0, 0.0003, n)))
|
||||
df["low"] = df[["open", "close"]].min(axis=1) * (1 - np.abs(rng.normal(0, 0.0003, n)))
|
||||
df["volume"] = rng.lognormal(mean=10, sigma=1.0, size=n)
|
||||
df.index = pd.date_range(end=datetime.now(timezone.utc), periods=n, freq="h")
|
||||
return df
|
||||
|
||||
|
||||
def load_real_data(csv_path: Path) -> pd.DataFrame:
|
||||
"""Load OHLCV from CSV (datetime,open,high,low,close,volume)."""
|
||||
if not csv_path.exists():
|
||||
raise FileNotFoundError(f"Real data CSV not found: {csv_path}")
|
||||
df = pd.read_csv(csv_path, parse_dates=["datetime"])
|
||||
df.sort_values("datetime", inplace=True)
|
||||
df.reset_index(drop=True, inplace=True)
|
||||
df.set_index("datetime", inplace=True)
|
||||
df.dropna(subset=["open", "high", "low", "close"], inplace=True)
|
||||
if "volume" not in df.columns:
|
||||
df["volume"] = 0.0
|
||||
df["volume"] = df["volume"].fillna(0)
|
||||
return df
|
||||
|
||||
|
||||
def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Feature set inspired by price-action + momentum + volatility."""
|
||||
out = df.copy()
|
||||
out["returns_1"] = np.log(out["close"] / out["close"].shift(1))
|
||||
out["returns_3"] = np.log(out["close"] / out["close"].shift(3))
|
||||
out["returns_6"] = np.log(out["close"] / out["close"].shift(6))
|
||||
|
||||
out["sma_10"] = out["close"].rolling(10).mean()
|
||||
out["sma_20"] = out["close"].rolling(20).mean()
|
||||
out["sma_50"] = out["close"].rolling(50).mean()
|
||||
out["ema_12"] = out["close"].ewm(span=12, adjust=False).mean()
|
||||
out["ema_26"] = out["close"].ewm(span=26, adjust=False).mean()
|
||||
out["macd"] = out["ema_12"] - out["ema_26"]
|
||||
out["macd_signal"] = out["macd"].ewm(span=9, adjust=False).mean()
|
||||
|
||||
delta = out["close"].diff()
|
||||
gain = delta.clip(lower=0).rolling(14).mean()
|
||||
loss = (-delta.clip(upper=0)).rolling(14).mean()
|
||||
rs = gain / (loss + 1e-9)
|
||||
out["rsi_14"] = 100.0 - (100.0 / (1.0 + rs))
|
||||
|
||||
tr1 = out["high"] - out["low"]
|
||||
tr2 = (out["high"] - out["close"].shift(1)).abs()
|
||||
tr3 = (out["low"] - out["close"].shift(1)).abs()
|
||||
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
||||
out["atr_14"] = tr.rolling(14).mean()
|
||||
out["atr_pct"] = out["atr_14"] / (out["close"] + 1e-9)
|
||||
|
||||
out["vol_ratio"] = out["volume"] / (out["volume"].rolling(20).mean() + 1e-9)
|
||||
|
||||
out["high_low_range"] = (out["high"] - out["low"]) / (out["close"] + 1e-9)
|
||||
out["dist_sma20"] = (out["close"] - out["sma_20"]) / (out["close"] + 1e-9)
|
||||
|
||||
out.dropna(inplace=True)
|
||||
return out
|
||||
|
||||
|
||||
def make_target(df: pd.DataFrame, horizon: int = 3) -> pd.DataFrame:
|
||||
"""Multi-class target:
|
||||
0 = SELL (return < -threshold)
|
||||
1 = HOLD (return within threshold)
|
||||
2 = BUY (return > +threshold)
|
||||
"""
|
||||
fwd = np.log(df["close"].shift(-horizon) / df["close"])
|
||||
thr = fwd.std() * 0.3
|
||||
target = pd.cut(fwd, bins=[-np.inf, -thr, thr, np.inf], labels=[0, 1, 2])
|
||||
df["target"] = target
|
||||
df.dropna(subset=["target"], inplace=True)
|
||||
df["target"] = df["target"].astype(int)
|
||||
return df
|
||||
|
||||
|
||||
FEATURES = [
|
||||
"returns_1", "returns_3", "returns_6",
|
||||
"sma_10", "sma_20", "sma_50",
|
||||
"macd", "macd_signal",
|
||||
"rsi_14", "atr_14", "atr_pct",
|
||||
"vol_ratio", "high_low_range", "dist_sma20",
|
||||
]
|
||||
|
||||
|
||||
def build_pipeline():
|
||||
return Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", RandomForestClassifier(n_estimators=300, max_depth=12, random_state=42, n_jobs=-1, class_weight="balanced")),
|
||||
])
|
||||
|
||||
|
||||
def train_models(df: pd.DataFrame):
|
||||
buy = df[df["target"] == 2].copy()
|
||||
hold = df[df["target"] == 1].copy()
|
||||
sell = df[df["target"] == 0].copy()
|
||||
|
||||
min_n = min(len(buy), len(hold), len(sell))
|
||||
if min_n < 200:
|
||||
raise ValueError(f"Not enough samples per class (min={min_n}). Provide more data or reduce horizon.")
|
||||
|
||||
buy = buy.sample(len(buy), random_state=42) if len(buy) > min_n else buy
|
||||
hold = hold.sample(len(hold), random_state=42) if len(hold) > min_n else hold
|
||||
sell = sell.sample(len(sell), random_state=42) if len(sell) > min_n else sell
|
||||
|
||||
balanced = pd.concat([buy, hold, sell]).sample(frac=1, random_state=42).reset_index(drop=True)
|
||||
|
||||
X = balanced[FEATURES].values
|
||||
y = balanced["target"].values
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
|
||||
|
||||
pipe = build_pipeline()
|
||||
pipe.fit(X_train, y_train)
|
||||
preds = pipe.predict(X_test)
|
||||
print("Accuracy:", accuracy_score(y_test, preds))
|
||||
print(classification_report(y_test, preds, target_names=["SELL", "HOLD", "BUY"]))
|
||||
return pipe, FEATURES
|
||||
|
||||
|
||||
def export_onnx(model: Pipeline, features: list, path: Path):
|
||||
initial_types = [("float_input", FloatTensorType([None, len(features)]))]
|
||||
onnx_model = convert_sklearn(model, initial_types=initial_types, target_opset=15)
|
||||
onnx.save(onnx_model, str(path))
|
||||
print(f"Saved ONNX model -> {path}")
|
||||
|
||||
|
||||
def save_metadata(meta: dict, path: Path):
|
||||
with open(path, "w") as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
meta = {
|
||||
"symbol": "XAUUSD",
|
||||
"timeframe": "H1",
|
||||
"features": FEATURES,
|
||||
"target_horizon": 3,
|
||||
"built_at": datetime.now(timezone.utc).isoformat(),
|
||||
"models": {},
|
||||
}
|
||||
|
||||
real_csv = DATA_DIR / "xauusd_h1.csv"
|
||||
if HAS_SKLEARN:
|
||||
if real_csv.exists():
|
||||
print(f"Loading real data from {real_csv}")
|
||||
df = load_real_data(real_csv)
|
||||
else:
|
||||
print("Real data CSV not found, generating synthetic data")
|
||||
df = generate_synthetic_gold_data(days=1500)
|
||||
|
||||
df = engineer_features(df)
|
||||
df = make_target(df, horizon=3)
|
||||
|
||||
buy_pipe, feats = train_models(pd.concat([df[df["target"] == 2], df[df["target"] != 2]]))
|
||||
export_onnx(buy_pipe, feats, MODELS_DIR / "xauusd_h1_ensemble.onnx")
|
||||
meta["models"]["ensemble"] = {"features": feats, "path": "models/xauusd_h1_ensemble.onnx"}
|
||||
else:
|
||||
print("sklearn/skl2onnx not available (", SKLEARN_IMPORT_ERROR, ")")
|
||||
print("Falling back to demo ONNX model via build_onnx_raw.py")
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from build_onnx_raw import build_model, save_metadata as save_meta
|
||||
build_model()
|
||||
save_meta()
|
||||
with open(MODELS_DIR / "metadata.json") as f:
|
||||
meta = json.load(f)
|
||||
|
||||
save_metadata(meta, MODELS_DIR / "metadata.json")
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"symbol": "XAUUSD",
|
||||
"timeframe": "H1",
|
||||
"features": [
|
||||
"returns_1",
|
||||
"returns_3",
|
||||
"returns_6",
|
||||
"sma_10",
|
||||
"sma_20",
|
||||
"sma_50",
|
||||
"macd",
|
||||
"macd_signal",
|
||||
"rsi_14",
|
||||
"atr_14",
|
||||
"atr_pct",
|
||||
"vol_ratio",
|
||||
"high_low_range",
|
||||
"dist_sma20"
|
||||
],
|
||||
"target_horizon": 3,
|
||||
"built_at": "2026-06-13T14:18:27.084238+00:00",
|
||||
"models": {
|
||||
"ensemble": {
|
||||
"features": [
|
||||
"returns_1",
|
||||
"returns_3",
|
||||
"returns_6",
|
||||
"sma_10",
|
||||
"sma_20",
|
||||
"sma_50",
|
||||
"macd",
|
||||
"macd_signal",
|
||||
"rsi_14",
|
||||
"atr_14",
|
||||
"atr_pct",
|
||||
"vol_ratio",
|
||||
"high_low_range",
|
||||
"dist_sma20"
|
||||
],
|
||||
"path": "models/xauusd_h1_ensemble.onnx"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.30.0
|
||||
onnxruntime>=1.24.0,<1.26.0
|
||||
scikit-learn>=1.5.0,<1.6.0
|
||||
pandas>=2.2.0,<2.3.0
|
||||
numpy>=1.26.0,<2.0.0
|
||||
joblib>=1.4.0,<1.5.0
|
||||
skl2onnx>=0.16.0,<0.17.0
|
||||
protobuf>=3.20,<5.0
|
||||
python-multipart==0.0.9
|
||||
pydantic>=2.7.0,<2.8.0
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Start Mad Turtle inference server.
|
||||
Run: python3 python/run_server.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, str(__file__).rsplit("/", 2)[0])
|
||||
|
||||
import uvicorn
|
||||
from inference_server.server import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Train XAUUSD H1 ensemble ONNX model (demo with synthetic data if no real feed).
|
||||
Run: python3 python/ml_pipeline/train_onnx.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, str(__file__).rsplit("/", 2)[0])
|
||||
|
||||
from ml_pipeline.train_onnx import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user