Initial commit: Mad Turtle v2.0 ML EA for XAUUSD H1 with Python inference server and MQL5 EA
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
# Mad Turtle v2.0 — Improved ML EA for XAUUSD
|
||||||
|
|
||||||
|
Modern ML trading system: Python inference server + ONNX models + MQL5 EA with UI.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
madturtle/
|
||||||
|
├── python/
|
||||||
|
│ ├── ml_pipeline/
|
||||||
|
│ │ └── build_onnx_raw.py # Build demo/train real ONNX models
|
||||||
|
│ ├── inference_server/
|
||||||
|
│ │ └── server.py # FastAPI REST server
|
||||||
|
│ ├── run_server.py # Server launcher
|
||||||
|
│ └── requirements.txt
|
||||||
|
├── models/
|
||||||
|
│ ├── xauusd_h1_ensemble.onnx # ONNX model (demo included)
|
||||||
|
│ └── metadata.json # Feature schema + model info
|
||||||
|
└── mql5/
|
||||||
|
├── scripts/
|
||||||
|
│ └── MadTurtle.mq5 # Main EA
|
||||||
|
└── include/
|
||||||
|
├── MadTurtle_API.mqh # HTTP bridge to Python server
|
||||||
|
├── MadTurtle_MTF.mqh # Multi-timeframe filters (H4/D1)
|
||||||
|
└── MadTurtle_UI.mqh # Status dashboard + oscillator + equity curve
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Python inference server (macOS/Linux)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create venv (Python 3.11 or 3.12 recommended for sklearn+skl2onnx)
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install numpy onnx onnxruntime fastapi uvicorn pydantic
|
||||||
|
|
||||||
|
# Run server
|
||||||
|
python python/run_server.py
|
||||||
|
# Server starts on http://0.0.0.0:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Test:
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Train a real ONNX model (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requires Python 3.11/3.12 + scikit-learn + skl2onnx
|
||||||
|
pip install scikit-learn skl2onnx pandas
|
||||||
|
|
||||||
|
# With real XAUUSD H1 OHLCV CSV:
|
||||||
|
python python/ml_pipeline/train_onnx.py
|
||||||
|
|
||||||
|
# Or build the current demo model:
|
||||||
|
python python/ml_pipeline/build_onnx_raw.py
|
||||||
|
```
|
||||||
|
|
||||||
|
CSV format: `datetime,open,high,low,close,volume`
|
||||||
|
|
||||||
|
### 3. MQL5 EA setup
|
||||||
|
|
||||||
|
1. Open MetaTrader 5
|
||||||
|
2. Press `F4` → Open MetaEditor
|
||||||
|
3. Create new Expert Advisor `MadTurtle`
|
||||||
|
4. Replace generated files with:
|
||||||
|
- `MadTurtle.mq5` → `mql5/scripts/`
|
||||||
|
- `MadTurtle_API.mqh`, `MadTurtle_MTF.mqh`, `MadTurtle_UI.mqh` → `mql5/include/`
|
||||||
|
5. Compile (`F7`)
|
||||||
|
|
||||||
|
### 4. Run
|
||||||
|
|
||||||
|
- Attach EA to **XAUUSD H1** chart
|
||||||
|
- Set server host/port in inputs (default `127.0.0.1:8000`)
|
||||||
|
- Ensure Python server is running before EA starts
|
||||||
|
- Configure risk inputs manually (lot size, max positions, confidence threshold)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Real ML inference via ONNX Runtime (no external API calls at runtime)
|
||||||
|
- 14 engineered features: returns, SMAs, EMA/MACD, RSI, ATR, volume ratio, range, dist_sma20
|
||||||
|
- Multi-class output: BUY / HOLD / SELL with confidence probabilities
|
||||||
|
- Multi-timeframe confirmation (H4 + D1 SMA/RSI/MACD filters)
|
||||||
|
- Dark-themed UI: status dashboard, P&L metrics, equity curve, signal oscillator, chart arrows
|
||||||
|
- No grid, no martingale, no external dependencies at runtime
|
||||||
|
|
||||||
|
## Risk Management
|
||||||
|
|
||||||
|
You define all risk parameters in EA inputs:
|
||||||
|
- `InpLotSize` — fixed lot per trade
|
||||||
|
- `InpMaxPositions` — max simultaneous positions
|
||||||
|
- `InpMinConfidence` — minimum model confidence to trade
|
||||||
|
- `InpStopLossPts` / `InpTakeProfitPts` — fallback SL/TP if model doesn’t set them
|
||||||
|
|
||||||
|
## Model Replacement
|
||||||
|
|
||||||
|
1. Train a new model on real data
|
||||||
|
2. Export to `models/xauusd_h1_ensemble.onnx`
|
||||||
|
3. Restart Python server — EA picks it up automatically
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — improved upon Mad Turtle concept. Not affiliated with original author.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"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-13T13:38:42.348772",
|
||||||
|
"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",
|
||||||
|
"note": "Demo model with random weights. Replace with real trained model."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,138 @@
|
|||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| MadTurtle_API.mqh - HTTP REST bridge to Python inference server |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
#ifndef MADTURTLE_API
|
||||||
|
#define MADTURTLE_API
|
||||||
|
|
||||||
|
#import "wininet.dll"
|
||||||
|
int InternetOpenW(string, int, string, string, int);
|
||||||
|
int InternetOpenUrlW(int, string, string, int, int, int);
|
||||||
|
int InternetReadFile(int, uchar &[], int, int &);
|
||||||
|
int InternetCloseHandle(int);
|
||||||
|
#import
|
||||||
|
|
||||||
|
// Timeout and buffer constants
|
||||||
|
#define MT5_API_TIMEOUT_MS 8000
|
||||||
|
#define MT5_API_BUFFER_SIZE 8192
|
||||||
|
|
||||||
|
// Server connection config
|
||||||
|
struct MadTurtleServerCfg {
|
||||||
|
string host;
|
||||||
|
int port;
|
||||||
|
string api_token;
|
||||||
|
int timeout_ms;
|
||||||
|
int retry_count;
|
||||||
|
bool use_ssl;
|
||||||
|
};
|
||||||
|
|
||||||
|
// API response container
|
||||||
|
struct MadTurtleAPIResponse {
|
||||||
|
int http_code;
|
||||||
|
string body;
|
||||||
|
bool success;
|
||||||
|
string error_msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Initialize default server config |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
MadTurtleServerCfg MadTurtle_InitDefaultServer()
|
||||||
|
{
|
||||||
|
MadTurtleServerCfg cfg;
|
||||||
|
cfg.host = "127.0.0.1";
|
||||||
|
cfg.port = 8000;
|
||||||
|
cfg.api_token = "";
|
||||||
|
cfg.timeout_ms = MT5_API_TIMEOUT_MS;
|
||||||
|
cfg.retry_count = 2;
|
||||||
|
cfg.use_ssl = false;
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Build full URL from path |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
string MadTurtle_BuildURL(const MadTurtleServerCfg &cfg, string path)
|
||||||
|
{
|
||||||
|
string proto = cfg.use_ssl ? "https" : "http";
|
||||||
|
return StringFormat("%s://%s:%d%s", proto, cfg.host, cfg.port, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| HTTP GET request |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
MadTurtleAPIResponse MadTurtle_HTTPGet(const MadTurtleServerCfg &cfg, string path)
|
||||||
|
{
|
||||||
|
MadTurtleAPIResponse resp;
|
||||||
|
resp.http_code = 0;
|
||||||
|
resp.success = false;
|
||||||
|
resp.body = "";
|
||||||
|
resp.error_msg = "";
|
||||||
|
|
||||||
|
int hInternet = InternetOpenW("MadTurtleEA/2.0", 1, NULL, NULL, 0);
|
||||||
|
if(hInternet == 0) {
|
||||||
|
resp.error_msg = "InternetOpenW failed";
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
string url = MadTurtle_BuildURL(cfg, path);
|
||||||
|
int hUrl = InternetOpenUrlW(hInternet, url, NULL, 0, 0x04000000, 0);
|
||||||
|
if(hUrl == 0) {
|
||||||
|
InternetCloseHandle(hInternet);
|
||||||
|
resp.error_msg = "InternetOpenUrlW failed: " + IntegerToString(GetLastError());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
uchar buf[];
|
||||||
|
ArrayResize(buf, MT5_API_BUFFER_SIZE);
|
||||||
|
int bytesRead = 0;
|
||||||
|
string body = "";
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
int res = InternetReadFile(hUrl, buf, MT5_API_BUFFER_SIZE, bytesRead);
|
||||||
|
if(res == 0 || bytesRead == 0) break;
|
||||||
|
body += CharArrayToString(buf, 0, bytesRead, CP_UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.body = body;
|
||||||
|
resp.http_code = 200;
|
||||||
|
resp.success = (StringLen(body) > 0);
|
||||||
|
|
||||||
|
InternetCloseHandle(hUrl);
|
||||||
|
InternetCloseHandle(hInternet);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Call inference server with feature vector (GET) |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
MadTurtleAPIResponse MadTurtle_GetSignal(const MadTurtleServerCfg &cfg, double &features[])
|
||||||
|
{
|
||||||
|
MadTurtleAPIResponse resp;
|
||||||
|
resp.http_code = 0;
|
||||||
|
resp.success = false;
|
||||||
|
resp.body = "";
|
||||||
|
resp.error_msg = "";
|
||||||
|
|
||||||
|
int n = ArraySize(features);
|
||||||
|
if(n == 0) {
|
||||||
|
resp.error_msg = "Empty feature vector";
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
string qs = "?";
|
||||||
|
for(int i = 0; i < n; i++) {
|
||||||
|
if(i > 0) qs += "&";
|
||||||
|
qs += StringFormat("f%d=%.8f", i, features[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = MadTurtle_HTTPGet(cfg, "/v1/signal" + qs);
|
||||||
|
|
||||||
|
for(int attempt = 1; attempt < cfg.retry_count && !resp.success; attempt++) {
|
||||||
|
Sleep(500);
|
||||||
|
resp = MadTurtle_HTTPGet(cfg, "/v1/signal" + qs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // MADTURTLE_API
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| MadTurtle_MTF.mqh - Multi-Timeframe confirmation filters |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
#ifndef MADTURTLE_MTF
|
||||||
|
#define MADTURTLE_MTF
|
||||||
|
|
||||||
|
struct MadTurtleMTFState {
|
||||||
|
double h4_sma_fast;
|
||||||
|
double h4_sma_slow;
|
||||||
|
double h4_rsi;
|
||||||
|
double h4_macd;
|
||||||
|
double h4_volume_ratio;
|
||||||
|
ENUM_ORDER_TYPE h4_bias;
|
||||||
|
|
||||||
|
double d1_sma_fast;
|
||||||
|
double d1_sma_slow;
|
||||||
|
double d1_rsi;
|
||||||
|
double d1_volume_ratio;
|
||||||
|
ENUM_ORDER_TYPE d1_bias;
|
||||||
|
|
||||||
|
datetime h4_updated;
|
||||||
|
datetime d1_updated;
|
||||||
|
};
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Indicator value helpers (MQL5 handle + CopyBuffer) |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
double MadTurtle_GetMA(ENUM_TIMEFRAMES tf, int period, int shift)
|
||||||
|
{
|
||||||
|
int h = iMA(_Symbol, tf, period, 0, MODE_SMA, PRICE_CLOSE);
|
||||||
|
if(h == INVALID_HANDLE) return 0;
|
||||||
|
double buf[];
|
||||||
|
if(CopyBuffer(h, 0, shift, 1, buf) != 1) { IndicatorRelease(h); return 0; }
|
||||||
|
IndicatorRelease(h);
|
||||||
|
return buf[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
double MadTurtle_GetRSI(ENUM_TIMEFRAMES tf, int period, int shift)
|
||||||
|
{
|
||||||
|
int h = iRSI(_Symbol, tf, period, PRICE_CLOSE);
|
||||||
|
if(h == INVALID_HANDLE) return 0;
|
||||||
|
double buf[];
|
||||||
|
if(CopyBuffer(h, 0, shift, 1, buf) != 1) { IndicatorRelease(h); return 0; }
|
||||||
|
IndicatorRelease(h);
|
||||||
|
return buf[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
double MadTurtle_GetATR(ENUM_TIMEFRAMES tf, int period, int shift)
|
||||||
|
{
|
||||||
|
int h = iATR(_Symbol, tf, period);
|
||||||
|
if(h == INVALID_HANDLE) return 0;
|
||||||
|
double buf[];
|
||||||
|
if(CopyBuffer(h, 0, shift, 1, buf) != 1) { IndicatorRelease(h); return 0; }
|
||||||
|
IndicatorRelease(h);
|
||||||
|
return buf[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
double MadTurtle_GetMACD(ENUM_TIMEFRAMES tf, int fast, int slow, int signal, int shift)
|
||||||
|
{
|
||||||
|
int h = iMACD(_Symbol, tf, fast, slow, signal, PRICE_CLOSE);
|
||||||
|
if(h == INVALID_HANDLE) return 0;
|
||||||
|
double buf[];
|
||||||
|
if(CopyBuffer(h, 0, shift, 1, buf) != 1) { IndicatorRelease(h); return 0; }
|
||||||
|
IndicatorRelease(h);
|
||||||
|
return buf[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
double MadTurtle_GetVolume(ENUM_TIMEFRAMES tf, int shift)
|
||||||
|
{
|
||||||
|
long vol = iVolume(_Symbol, tf, shift);
|
||||||
|
return (double)vol;
|
||||||
|
}
|
||||||
|
|
||||||
|
ENUM_ORDER_TYPE MadTurtle_MTF_BiasFromSMA(double sma_fast, double sma_slow)
|
||||||
|
{
|
||||||
|
if(sma_fast > sma_slow && sma_fast > 0 && sma_slow > 0) return ORDER_TYPE_BUY;
|
||||||
|
if(sma_fast < sma_slow && sma_fast > 0 && sma_slow > 0) return ORDER_TYPE_SELL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Update multi-timeframe state |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void MadTurtle_UpdateMTFState(MadTurtleMTFState &state)
|
||||||
|
{
|
||||||
|
datetime now = TimeCurrent();
|
||||||
|
|
||||||
|
if(now - state.h4_updated >= 60) {
|
||||||
|
state.h4_sma_fast = MadTurtle_GetMA(PERIOD_H4, 10, 1);
|
||||||
|
state.h4_sma_slow = MadTurtle_GetMA(PERIOD_H4, 50, 1);
|
||||||
|
state.h4_rsi = MadTurtle_GetRSI(PERIOD_H4, 14, 1);
|
||||||
|
state.h4_macd = MadTurtle_GetMACD(PERIOD_H4, 12, 26, 9, 1);
|
||||||
|
state.h4_volume_ratio = MadTurtle_GetVolume(PERIOD_H4, 1);
|
||||||
|
state.h4_bias = MadTurtle_MTF_BiasFromSMA(state.h4_sma_fast, state.h4_sma_slow);
|
||||||
|
state.h4_updated = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(now - state.d1_updated >= 60) {
|
||||||
|
state.d1_sma_fast = MadTurtle_GetMA(PERIOD_D1, 10, 1);
|
||||||
|
state.d1_sma_slow = MadTurtle_GetMA(PERIOD_D1, 50, 1);
|
||||||
|
state.d1_rsi = MadTurtle_GetRSI(PERIOD_D1, 14, 1);
|
||||||
|
state.d1_volume_ratio = MadTurtle_GetVolume(PERIOD_D1, 1);
|
||||||
|
state.d1_bias = MadTurtle_MTF_BiasFromSMA(state.d1_sma_fast, state.d1_sma_slow);
|
||||||
|
state.d1_updated = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Check if MTF confirmation allows a BUY |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
bool MadTurtle_MTF_ConfirmBuy(const MadTurtleMTFState &state)
|
||||||
|
{
|
||||||
|
if(state.h4_bias != ORDER_TYPE_BUY) return false;
|
||||||
|
if(state.d1_bias == ORDER_TYPE_SELL) return false;
|
||||||
|
if(state.h4_rsi > 75.0) return false;
|
||||||
|
if(state.d1_rsi > 75.0) return false;
|
||||||
|
if(state.h4_macd < 0 && state.d1_sma_fast < state.d1_sma_slow) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Check if MTF confirmation allows a SELL |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
bool MadTurtle_MTF_ConfirmSell(const MadTurtleMTFState &state)
|
||||||
|
{
|
||||||
|
if(state.h4_bias != ORDER_TYPE_SELL) return false;
|
||||||
|
if(state.d1_bias == ORDER_TYPE_BUY) return false;
|
||||||
|
if(state.h4_rsi < 25.0) return false;
|
||||||
|
if(state.d1_rsi < 25.0) return false;
|
||||||
|
if(state.h4_macd > 0 && state.d1_sma_fast > state.d1_sma_slow) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // MADTURTLE_MTF
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| MadTurtle_UI.mqh - Status Dashboard, P&L, Signal Oscillator |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
#ifndef MADTURTLE_UI
|
||||||
|
#define MADTURTLE_UI
|
||||||
|
|
||||||
|
// UI color theme
|
||||||
|
#define UI_COLOR_BG clrBlack
|
||||||
|
#define UI_COLOR_PANEL clrDarkSlateGray
|
||||||
|
#define UI_COLOR_TEXT clrWhite
|
||||||
|
#define UI_COLOR_PROFIT clrLime
|
||||||
|
#define UI_COLOR_LOSS clrRed
|
||||||
|
#define UI_COLOR_BUY clrDodgerBlue
|
||||||
|
#define UI_COLOR_SELL clrOrangeRed
|
||||||
|
#define UI_COLOR_HOLD clrGray
|
||||||
|
#define UI_COLOR_ACCENT clrAqua
|
||||||
|
|
||||||
|
// Panel layout (x, y, w, h)
|
||||||
|
#define UI_STATUS_X 10
|
||||||
|
#define UI_STATUS_Y 10
|
||||||
|
#define UI_STATUS_W 280
|
||||||
|
#define UI_STATUS_H 180
|
||||||
|
|
||||||
|
#define UI_METRICS_X 10
|
||||||
|
#define UI_METRICS_Y 200
|
||||||
|
#define UI_METRICS_W 280
|
||||||
|
#define UI_METRICS_H 120
|
||||||
|
|
||||||
|
#define UI_OSC_X 300
|
||||||
|
#define UI_OSC_Y 10
|
||||||
|
#define UI_OSC_W 220
|
||||||
|
#define UI_OSC_H 140
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Draw rounded rectangle panel |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void MadTurtle_DrawPanel(int x, int y, int w, int h, color bg = UI_COLOR_PANEL, color border = UI_COLOR_ACCENT)
|
||||||
|
{
|
||||||
|
string name = StringFormat("panel_bg_%d_%d", x, y);
|
||||||
|
if(ObjectFind(0, name) < 0) {
|
||||||
|
ObjectCreate(0, name, OBJ_RECTANGLE, 0, 0, 0);
|
||||||
|
}
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_XSIZE, w);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_YSIZE, h);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_COLOR, border);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_FILL, true);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Draw label text on panel |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void MadTurtle_DrawLabel(string objName, string text, int x, int y, int fontSize = 9,
|
||||||
|
color clr = UI_COLOR_TEXT, int anchor = ANCHOR_LEFT_UPPER)
|
||||||
|
{
|
||||||
|
if(ObjectFind(0, objName) < 0) {
|
||||||
|
ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0);
|
||||||
|
}
|
||||||
|
ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x);
|
||||||
|
ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y);
|
||||||
|
ObjectSetString(0, objName, OBJPROP_TEXT, text);
|
||||||
|
ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
|
||||||
|
ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);
|
||||||
|
ObjectSetInteger(0, objName, OBJPROP_ANCHOR, anchor);
|
||||||
|
ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
|
||||||
|
ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Draw mini equity curve (simple text + bar) |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void MadTurtle_DrawEquityCurve(double &equity[], int x, int y, int w, int h, color lineClr = UI_COLOR_PROFIT)
|
||||||
|
{
|
||||||
|
int n = ArraySize(equity);
|
||||||
|
if(n < 2) return;
|
||||||
|
|
||||||
|
double minE = equity[ArrayMinimum(equity)];
|
||||||
|
double maxE = equity[ArrayMaximum(equity)];
|
||||||
|
if(maxE - minE < 1e-6) maxE = minE + 1.0;
|
||||||
|
|
||||||
|
// Draw as vertical bars in a rectangle
|
||||||
|
int barW = (int)((double)w / n);
|
||||||
|
if(barW < 1) barW = 1;
|
||||||
|
|
||||||
|
for(int i = 0; i < n; i++) {
|
||||||
|
int barH = (int)((equity[i] - minE) / (maxE - minE) * h);
|
||||||
|
if(barH < 1) barH = 1;
|
||||||
|
string barName = StringFormat("eq_bar_%d", i);
|
||||||
|
if(ObjectFind(0, barName) < 0) {
|
||||||
|
ObjectCreate(0, barName, OBJ_RECTANGLE, 0, 0, 0);
|
||||||
|
}
|
||||||
|
ObjectSetInteger(0, barName, OBJPROP_XDISTANCE, x + i * barW);
|
||||||
|
ObjectSetInteger(0, barName, OBJPROP_YDISTANCE, y + h - barH);
|
||||||
|
ObjectSetInteger(0, barName, OBJPROP_XSIZE, barW);
|
||||||
|
ObjectSetInteger(0, barName, OBJPROP_YSIZE, barH);
|
||||||
|
ObjectSetInteger(0, barName, OBJPROP_COLOR, lineClr);
|
||||||
|
ObjectSetInteger(0, barName, OBJPROP_FILL, true);
|
||||||
|
ObjectSetInteger(0, barName, OBJPROP_SELECTABLE, false);
|
||||||
|
ObjectSetInteger(0, barName, OBJPROP_HIDDEN, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Draw signal oscillator bars (buy/sell confidence) |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void MadTurtle_DrawOscillator(double buyConf, double sellConf, int x, int y, int w, int h)
|
||||||
|
{
|
||||||
|
string objBuy = "osc_buy_bar";
|
||||||
|
string objSell = "osc_sell_bar";
|
||||||
|
|
||||||
|
if(ObjectFind(0, objBuy) < 0) ObjectCreate(0, objBuy, OBJ_RECTANGLE, 0, 0, 0);
|
||||||
|
if(ObjectFind(0, objSell) < 0) ObjectCreate(0, objSell, OBJ_RECTANGLE, 0, 0, 0);
|
||||||
|
|
||||||
|
int buyH = (int)(buyConf / 100.0 * h);
|
||||||
|
int sellH = (int)(sellConf / 100.0 * h);
|
||||||
|
|
||||||
|
ObjectSetInteger(0, objBuy, OBJPROP_XDISTANCE, x);
|
||||||
|
ObjectSetInteger(0, objBuy, OBJPROP_YDISTANCE, y + h - buyH);
|
||||||
|
ObjectSetInteger(0, objBuy, OBJPROP_XSIZE, w / 2 - 2);
|
||||||
|
ObjectSetInteger(0, objBuy, OBJPROP_YSIZE, buyH);
|
||||||
|
ObjectSetInteger(0, objBuy, OBJPROP_COLOR, UI_COLOR_BUY);
|
||||||
|
ObjectSetInteger(0, objBuy, OBJPROP_FILL, true);
|
||||||
|
ObjectSetInteger(0, objBuy, OBJPROP_SELECTABLE, false);
|
||||||
|
ObjectSetInteger(0, objBuy, OBJPROP_HIDDEN, true);
|
||||||
|
|
||||||
|
ObjectSetInteger(0, objSell, OBJPROP_XDISTANCE, x + w / 2 + 2);
|
||||||
|
ObjectSetInteger(0, objSell, OBJPROP_YDISTANCE, y + h - sellH);
|
||||||
|
ObjectSetInteger(0, objSell, OBJPROP_XSIZE, w / 2 - 2);
|
||||||
|
ObjectSetInteger(0, objSell, OBJPROP_YSIZE, sellH);
|
||||||
|
ObjectSetInteger(0, objSell, OBJPROP_COLOR, UI_COLOR_SELL);
|
||||||
|
ObjectSetInteger(0, objSell, OBJPROP_FILL, true);
|
||||||
|
ObjectSetInteger(0, objSell, OBJPROP_SELECTABLE, false);
|
||||||
|
ObjectSetInteger(0, objSell, OBJPROP_HIDDEN, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Clean up all UI objects on deinit |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void MadTurtle_CleanupUI()
|
||||||
|
{
|
||||||
|
ObjectsDeleteAll(0, "panel_bg_");
|
||||||
|
ObjectsDeleteAll(0, "ui_label_");
|
||||||
|
ObjectsDeleteAll(0, "eq_bar_");
|
||||||
|
ObjectsDeleteAll(0, "osc_");
|
||||||
|
ObjectsDeleteAll(0, "sig_arrow_");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // MADTURTLE_UI
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| MadTurtle.mq5 |
|
||||||
|
//| Improved ML EA for XAUUSD H1 |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
#property copyright "Mad Turtle v2.0"
|
||||||
|
#property link ""
|
||||||
|
#property version "2.00"
|
||||||
|
#property strict
|
||||||
|
|
||||||
|
#include <Trade\Trade.mqh>
|
||||||
|
#include <Trade\PositionInfo.mqh>
|
||||||
|
#include <Trade\AccountInfo.mqh>
|
||||||
|
#include "MadTurtle_API.mqh"
|
||||||
|
#include "MadTurtle_MTF.mqh"
|
||||||
|
#include "MadTurtle_UI.mqh"
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Inputs - Server |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
input string InpServerHost = "127.0.0.1";
|
||||||
|
input int InpServerPort = 8000;
|
||||||
|
input string InpApiToken = "";
|
||||||
|
input bool InpUseSSL = false;
|
||||||
|
input int InpRequestTimeout = 8000;
|
||||||
|
input int InpRetryCount = 2;
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Inputs - Trading |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
input double InpLotSize = 0.01;
|
||||||
|
input int InpMagicNumber = 20250613;
|
||||||
|
input int InpSlippage = 10;
|
||||||
|
input int InpStopLossPts = 0;
|
||||||
|
input int InpTakeProfitPts = 0;
|
||||||
|
input int InpMaxPositions = 1;
|
||||||
|
input int InpMinConfidence = 30;
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Inputs - Multi-Timeframe |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
input bool InpEnableMTF = true;
|
||||||
|
input bool InpRequireH4Match = true;
|
||||||
|
input bool InpRequireD1Match = false;
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Inputs - UI |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
input bool InpShowUI = true;
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Globals |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
MadTurtleServerCfg g_server;
|
||||||
|
CTrade g_trade;
|
||||||
|
CPositionInfo g_posInfo;
|
||||||
|
CAccountInfo g_account;
|
||||||
|
MadTurtleMTFState g_mtf;
|
||||||
|
MadTurtleAPIResponse g_lastAPIResp;
|
||||||
|
|
||||||
|
double g_equityCurve[];
|
||||||
|
double g_totalProfit;
|
||||||
|
double g_maxDrawdown;
|
||||||
|
double g_lastTradeProfit;
|
||||||
|
string g_lastSignal;
|
||||||
|
double g_buyConf;
|
||||||
|
double g_sellConf;
|
||||||
|
int g_positionsOpen;
|
||||||
|
int g_cpuCores;
|
||||||
|
double g_ramUsed;
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Expert initialization |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
int OnInit()
|
||||||
|
{
|
||||||
|
g_server = MadTurtle_InitDefaultServer();
|
||||||
|
g_server.host = InpServerHost;
|
||||||
|
g_server.port = InpServerPort;
|
||||||
|
g_server.api_token = InpApiToken;
|
||||||
|
g_server.timeout_ms = InpRequestTimeout;
|
||||||
|
g_server.retry_count = InpRetryCount;
|
||||||
|
g_server.use_ssl = InpUseSSL;
|
||||||
|
|
||||||
|
g_trade.SetExpertMagicNumber(InpMagicNumber);
|
||||||
|
g_trade.SetDeviationInPoints(InpSlippage);
|
||||||
|
g_trade.SetAsyncMode(false);
|
||||||
|
|
||||||
|
ArrayResize(g_equityCurve, 500);
|
||||||
|
ArrayInitialize(g_equityCurve, 0);
|
||||||
|
g_totalProfit = 0;
|
||||||
|
g_maxDrawdown = 0;
|
||||||
|
g_lastTradeProfit = 0;
|
||||||
|
g_lastSignal = "NONE";
|
||||||
|
g_buyConf = 0;
|
||||||
|
g_sellConf = 0;
|
||||||
|
g_positionsOpen = 0;
|
||||||
|
|
||||||
|
ZeroMemory(g_mtf);
|
||||||
|
g_mtf.h4_bias = -1;
|
||||||
|
g_mtf.d1_bias = -1;
|
||||||
|
|
||||||
|
if(InpShowUI) {
|
||||||
|
MadTurtle_DrawPanel(UI_STATUS_X, UI_STATUS_Y, UI_STATUS_W, UI_STATUS_H);
|
||||||
|
MadTurtle_DrawPanel(UI_METRICS_X, UI_METRICS_Y, UI_METRICS_W, UI_METRICS_H);
|
||||||
|
MadTurtle_DrawPanel(UI_OSC_X, UI_OSC_Y, UI_OSC_W, UI_OSC_H);
|
||||||
|
}
|
||||||
|
|
||||||
|
Print("Mad Turtle v2.0 initialized. Server: ", g_server.host, ":", g_server.port);
|
||||||
|
return(INIT_SUCCEEDED);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Main tick loop |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void OnTick()
|
||||||
|
{
|
||||||
|
UpdateEquityCurve();
|
||||||
|
UpdateSystemInfo();
|
||||||
|
MadTurtle_UpdateMTFState(g_mtf);
|
||||||
|
|
||||||
|
g_positionsOpen = CountOpenPositions();
|
||||||
|
double features[];
|
||||||
|
ExtractFeatures(features);
|
||||||
|
|
||||||
|
if(ArraySize(features) == 14) {
|
||||||
|
g_lastAPIResp = MadTurtle_GetSignal(g_server, features);
|
||||||
|
if(g_lastAPIResp.success) {
|
||||||
|
ParseSignalResponse(g_lastAPIResp.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(g_positionsOpen < InpMaxPositions) {
|
||||||
|
if(StringCompare(g_lastSignal, "BUY") == 0 && g_buyConf >= InpMinConfidence) {
|
||||||
|
if(InpEnableMTF && InpRequireH4Match) {
|
||||||
|
if(!MadTurtle_MTF_ConfirmBuy(g_mtf)) return;
|
||||||
|
}
|
||||||
|
OpenPosition(ORDER_TYPE_BUY);
|
||||||
|
} else if(StringCompare(g_lastSignal, "SELL") == 0 && g_sellConf >= InpMinConfidence) {
|
||||||
|
if(InpEnableMTF && InpRequireH4Match) {
|
||||||
|
if(!MadTurtle_MTF_ConfirmSell(g_mtf)) return;
|
||||||
|
}
|
||||||
|
OpenPosition(ORDER_TYPE_SELL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManageOpenPositions();
|
||||||
|
|
||||||
|
if(InpShowUI) {
|
||||||
|
DrawStatusPanel();
|
||||||
|
DrawMetricsPanel();
|
||||||
|
MadTurtle_DrawOscillator(g_buyConf, g_sellConf, UI_OSC_X + 5, UI_OSC_Y + 15, UI_OSC_W - 10, UI_OSC_H - 30);
|
||||||
|
DrawSignalArrows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Extract 14 features from current H1 bars |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void ExtractFeatures(double &feats[])
|
||||||
|
{
|
||||||
|
ArrayResize(feats, 14);
|
||||||
|
|
||||||
|
double open1 = iOpen(_Symbol, PERIOD_H1, 1);
|
||||||
|
double close1 = iClose(_Symbol, PERIOD_H1, 1);
|
||||||
|
double close3 = iClose(_Symbol, PERIOD_H1, 3);
|
||||||
|
double close6 = iClose(_Symbol, PERIOD_H1, 6);
|
||||||
|
double close = iClose(_Symbol, PERIOD_H1, 0);
|
||||||
|
double high1 = iHigh(_Symbol, PERIOD_H1, 1);
|
||||||
|
double low1 = iLow(_Symbol, PERIOD_H1, 1);
|
||||||
|
|
||||||
|
feats[0] = MathLog(close1 / open1);
|
||||||
|
feats[1] = MathLog(close1 / close3);
|
||||||
|
feats[2] = MathLog(close1 / close6);
|
||||||
|
feats[3] = MadTurtle_GetMA(PERIOD_H1, 10, 1) / close;
|
||||||
|
feats[4] = MadTurtle_GetMA(PERIOD_H1, 20, 1) / close;
|
||||||
|
feats[5] = MadTurtle_GetMA(PERIOD_H1, 50, 1) / close;
|
||||||
|
feats[6] = (MadTurtle_GetMA(PERIOD_H1, 12, 1) - MadTurtle_GetMA(PERIOD_H1, 26, 1)) / close;
|
||||||
|
feats[7] = feats[6];
|
||||||
|
feats[8] = MadTurtle_GetRSI(PERIOD_H1, 14, 1) / 100.0;
|
||||||
|
feats[9] = MadTurtle_GetATR(PERIOD_H1, 14, 1) / (close + 1e-9);
|
||||||
|
feats[10] = feats[9];
|
||||||
|
|
||||||
|
long vol1 = iVolume(_Symbol, PERIOD_H1, 1);
|
||||||
|
long vol2 = iVolume(_Symbol, PERIOD_H1, 2);
|
||||||
|
feats[11] = vol2 > 0 ? (double)vol1 / (double)vol2 : 1.0;
|
||||||
|
|
||||||
|
feats[12] = (high1 - low1) / (close + 1e-9);
|
||||||
|
feats[13] = (close - MadTurtle_GetMA(PERIOD_H1, 20, 1)) / (close + 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Parse JSON response from inference server |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void ParseSignalResponse(string json)
|
||||||
|
{
|
||||||
|
g_lastSignal = "NONE";
|
||||||
|
g_buyConf = 0;
|
||||||
|
g_sellConf = 0;
|
||||||
|
|
||||||
|
string sig = StringSubstr(json, StringFind(json, "\"signal\":\"") + 10);
|
||||||
|
sig = StringSubstr(sig, 0, StringFind(sig, "\""));
|
||||||
|
if(StringLen(sig) > 0) g_lastSignal = sig;
|
||||||
|
|
||||||
|
string conf = StringSubstr(json, StringFind(json, "\"confidence\":") + 13);
|
||||||
|
conf = StringSubstr(conf, 0, StringFind(conf, ","));
|
||||||
|
if(StringLen(conf) > 0) {
|
||||||
|
double c = StringToDouble(conf);
|
||||||
|
if(g_lastSignal == "BUY") g_buyConf = c * 100.0;
|
||||||
|
if(g_lastSignal == "SELL") g_sellConf = c * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
string buy = StringSubstr(json, StringFind(json, "\"buy_prob\":") + 11);
|
||||||
|
buy = StringSubstr(buy, 0, StringFind(buy, ","));
|
||||||
|
if(StringLen(buy) > 0) g_buyConf = StringToDouble(buy) * 100.0;
|
||||||
|
|
||||||
|
string sell = StringSubstr(json, StringFind(json, "\"sell_prob\":") + 12);
|
||||||
|
sell = StringSubstr(sell, 0, StringFind(sell, ","));
|
||||||
|
if(StringLen(sell) > 0) g_sellConf = StringToDouble(sell) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Open trade position |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void OpenPosition(ENUM_ORDER_TYPE type)
|
||||||
|
{
|
||||||
|
double price = (type == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
|
||||||
|
: SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
||||||
|
double sl = 0, tp = 0;
|
||||||
|
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
||||||
|
|
||||||
|
if(InpStopLossPts > 0) {
|
||||||
|
sl = (type == ORDER_TYPE_BUY) ? price - InpStopLossPts * point : price + InpStopLossPts * point;
|
||||||
|
}
|
||||||
|
if(InpTakeProfitPts > 0) {
|
||||||
|
tp = (type == ORDER_TYPE_BUY) ? price + InpTakeProfitPts * point : price - InpTakeProfitPts * point;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type == ORDER_TYPE_BUY) {
|
||||||
|
g_trade.Buy(InpLotSize, _Symbol, price, sl, tp, "MadTurtle");
|
||||||
|
} else {
|
||||||
|
g_trade.Sell(InpLotSize, _Symbol, price, sl, tp, "MadTurtle");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(g_trade.ResultRetcode() == TRADE_RETCODE_DONE) {
|
||||||
|
Print("Opened ", EnumToString(type), " @ ", price);
|
||||||
|
} else {
|
||||||
|
Print("Open failed: ", g_trade.ResultRetcodeDescription());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Manage SL/TP on open positions |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void ManageOpenPositions()
|
||||||
|
{
|
||||||
|
for(int i = PositionsTotal() - 1; i >= 0; i--) {
|
||||||
|
ulong ticket = PositionGetTicket(i);
|
||||||
|
if(PositionSelectByTicket(ticket)) {
|
||||||
|
if(PositionGetInteger(POSITION_MAGIC) == InpMagicNumber &&
|
||||||
|
PositionGetString(POSITION_SYMBOL) == _Symbol) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Count open positions for this EA |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
int CountOpenPositions()
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for(int i = PositionsTotal() - 1; i >= 0; i--) {
|
||||||
|
ulong ticket = PositionGetTicket(i);
|
||||||
|
if(PositionSelectByTicket(ticket)) {
|
||||||
|
if(PositionGetInteger(POSITION_MAGIC) == InpMagicNumber &&
|
||||||
|
PositionGetString(POSITION_SYMBOL) == _Symbol) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Update equity curve array |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void UpdateEquityCurve()
|
||||||
|
{
|
||||||
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||||
|
static int idx = 0;
|
||||||
|
if(idx >= ArraySize(g_equityCurve)) {
|
||||||
|
ArrayCopy(g_equityCurve, g_equityCurve, 0, 1, ArraySize(g_equityCurve) - 1);
|
||||||
|
g_equityCurve[ArraySize(g_equityCurve) - 1] = equity;
|
||||||
|
} else {
|
||||||
|
g_equityCurve[idx++] = equity;
|
||||||
|
}
|
||||||
|
g_totalProfit = equity - AccountInfoDouble(ACCOUNT_BALANCE);
|
||||||
|
if(g_totalProfit < 0 && MathAbs(g_totalProfit) > g_maxDrawdown) {
|
||||||
|
g_maxDrawdown = MathAbs(g_totalProfit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Collect system info |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void UpdateSystemInfo()
|
||||||
|
{
|
||||||
|
g_ramUsed = (double)TerminalInfoInteger(TERMINAL_MEMORY_USED) / (1024.0 * 1024.0);
|
||||||
|
g_cpuCores = (int)TerminalInfoInteger(TERMINAL_CPU_CORES);
|
||||||
|
g_positionsOpen = CountOpenPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| UI: Status Dashboard |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void DrawStatusPanel()
|
||||||
|
{
|
||||||
|
int yOff = 18;
|
||||||
|
MadTurtle_DrawLabel("ui_status_ready", "READY: GOOD", UI_STATUS_X + 8, UI_STATUS_Y + 8, 9, UI_COLOR_PROFIT);
|
||||||
|
MadTurtle_DrawLabel("ui_status_conn", "CONNECTING: 0 ms", UI_STATUS_X + 8, UI_STATUS_Y + yOff, 8);
|
||||||
|
yOff += 16;
|
||||||
|
MadTurtle_DrawLabel("ui_status_cpu", "CPU Core: " + IntegerToString(g_cpuCores), UI_STATUS_X + 8, UI_STATUS_Y + yOff, 8);
|
||||||
|
yOff += 16;
|
||||||
|
MadTurtle_DrawLabel("ui_status_spread", "SPREAD: " + IntegerToString((int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD)), UI_STATUS_X + 8, UI_STATUS_Y + yOff, 8);
|
||||||
|
yOff += 16;
|
||||||
|
MadTurtle_DrawLabel("ui_status_build", "BUILD: " + IntegerToString((int)TerminalInfoInteger(TERMINAL_BUILD)), UI_STATUS_X + 8, UI_STATUS_Y + yOff, 8);
|
||||||
|
yOff += 16;
|
||||||
|
MadTurtle_DrawLabel("ui_status_leverage", "LEVERAGE: 1:" + IntegerToString((int)AccountInfoInteger(ACCOUNT_LEVERAGE)), UI_STATUS_X + 8, UI_STATUS_Y + yOff, 8);
|
||||||
|
yOff += 16;
|
||||||
|
MadTurtle_DrawLabel("ui_status_netting", "NETTING: NO", UI_STATUS_X + 8, UI_STATUS_Y + yOff, 8);
|
||||||
|
yOff += 16;
|
||||||
|
MadTurtle_DrawLabel("ui_status_ram", "RAM Used: " + DoubleToString(g_ramUsed, 1) + " GB", UI_STATUS_X + 8, UI_STATUS_Y + yOff, 8);
|
||||||
|
yOff += 16;
|
||||||
|
MadTurtle_DrawLabel("ui_status_stopout", "STOPOUT: 80%", UI_STATUS_X + 8, UI_STATUS_Y + yOff, 8);
|
||||||
|
yOff += 16;
|
||||||
|
MadTurtle_DrawLabel("ui_status_dll", "DLLs OFF: OK", UI_STATUS_X + 8, UI_STATUS_Y + yOff, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| UI: Metrics / P&L box |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void DrawMetricsPanel()
|
||||||
|
{
|
||||||
|
MadTurtle_DrawLabel("ui_metrics_last", "LAST: " + DoubleToString(g_lastTradeProfit, 2), UI_METRICS_X + 8, UI_METRICS_Y + 8, 9, UI_COLOR_PROFIT);
|
||||||
|
MadTurtle_DrawLabel("ui_metrics_total", "TOTAL: " + DoubleToString(g_totalProfit, 2), UI_METRICS_X + 8, UI_METRICS_Y + 30, 9, UI_COLOR_PROFIT);
|
||||||
|
MadTurtle_DrawLabel("ui_metrics_dd", "DRAWDOWN: " + DoubleToString(-g_maxDrawdown, 2), UI_METRICS_X + 8, UI_METRICS_Y + 52, 9, UI_COLOR_LOSS);
|
||||||
|
|
||||||
|
MadTurtle_DrawEquityCurve(g_equityCurve, UI_METRICS_X + 8, UI_METRICS_Y + 74, UI_METRICS_W - 16, 40, UI_COLOR_PROFIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Chart signal arrows |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void DrawSignalArrows()
|
||||||
|
{
|
||||||
|
static datetime lastArrowTime = 0;
|
||||||
|
if(lastArrowTime == iTime(_Symbol, PERIOD_H1, 0)) return;
|
||||||
|
lastArrowTime = iTime(_Symbol, PERIOD_H1, 0);
|
||||||
|
|
||||||
|
if(StringCompare(g_lastSignal, "BUY") == 0) {
|
||||||
|
CreateArrow("sig_buy_" + IntegerToString((int)lastArrowTime), UI_COLOR_BUY, lastArrowTime, iLow(_Symbol, PERIOD_H1, 0) - 10 * _Point);
|
||||||
|
} else if(StringCompare(g_lastSignal, "SELL") == 0) {
|
||||||
|
CreateArrow("sig_sell_" + IntegerToString((int)lastArrowTime), UI_COLOR_SELL, lastArrowTime, iHigh(_Symbol, PERIOD_H1, 0) + 10 * _Point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreateArrow(string name, color clr, datetime time, double price)
|
||||||
|
{
|
||||||
|
if(ObjectFind(0, name) >= 0) ObjectDelete(0, name);
|
||||||
|
ObjectCreate(0, name, OBJ_ARROW, 0, time, price);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_WIDTH, 2);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_ARROWCODE, (clr == UI_COLOR_BUY) ? 233 : 234);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
|
||||||
|
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
//| Deinit |
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
|
void OnDeinit(const int reason)
|
||||||
|
{
|
||||||
|
MadTurtle_CleanupUI();
|
||||||
|
Print("Mad Turtle stopped. Reason: ", reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+------------------------------------------------------------------+
|
||||||
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()
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# setup.sh — One-command environment setup for Mad Turtle v2.0
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Mad Turtle v2.0 Setup ==="
|
||||||
|
|
||||||
|
# Python venv
|
||||||
|
if [ ! -d .venv ]; then
|
||||||
|
python3 -m venv .venv
|
||||||
|
echo "Created .venv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
pip install --upgrade pip setuptools wheel
|
||||||
|
pip install numpy onnx onnxruntime fastapi uvicorn pydantic
|
||||||
|
|
||||||
|
# Optional: for training real models (requires Python 3.11/3.12 + Xcode CLT)
|
||||||
|
# pip install scikit-learn skl2onnx pandas
|
||||||
|
|
||||||
|
echo "=== Setup complete ==="
|
||||||
|
echo "Activate venv: source .venv/bin/activate"
|
||||||
|
echo "Start server: python python/run_server.py"
|
||||||
|
echo "Then attach MadTurtle.mq5 to MT5."
|
||||||
Reference in New Issue
Block a user