mirror of
https://github.com/PyP-Quant/quant-trading-strategy-templates.git
synced 2026-08-17 12:38:05 +00:00
feat: add PyP quant strategy templates
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
.env
|
||||
.DS_Store
|
||||
*.joblib
|
||||
*.pkl
|
||||
*.onnx
|
||||
*.csv
|
||||
*.parquet
|
||||
out/
|
||||
runs/
|
||||
artifacts/
|
||||
@@ -0,0 +1,9 @@
|
||||
# Disclaimer
|
||||
|
||||
This repository is for educational and software-development purposes only.
|
||||
|
||||
The strategies here are starter templates, not investment advice, trading recommendations, or financial promotions. They are not verified profitable strategies. You are responsible for testing, validating, and understanding every strategy before using it.
|
||||
|
||||
Markets involve risk. You can lose money. Simulated results can differ materially from live execution because of spreads, slippage, liquidity, broker behavior, fees, data quality, and latency.
|
||||
|
||||
Do not deploy any strategy live until you have validated it with out-of-sample data, realistic execution assumptions, and appropriate risk limits.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 PyP Quant
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,62 @@
|
||||
# PyP Quant Trading Strategy Templates
|
||||
|
||||
Open-source Python quant trading strategy templates for PyP Quant Mode.
|
||||
|
||||
This repository is a public, educational starter library for traders who want to build quantitative trading strategies with Python, validate them with PyP PPE simulation, and deploy live signals through the PyP platform.
|
||||
|
||||
These examples are intentionally safe starter projects. They are not financial advice, not live performance claims, and not recommendations to trade any instrument.
|
||||
|
||||
## What Is Inside
|
||||
|
||||
Each template follows the PyP Quant Mode contract:
|
||||
|
||||
```python
|
||||
def train(data, config):
|
||||
return model, metrics
|
||||
|
||||
def predict(model, market_data, config):
|
||||
return {"signal": "UP|DOWN|HOLD", "confidence": 0.0, "metadata": {}}
|
||||
```
|
||||
|
||||
Every project includes:
|
||||
|
||||
- `strategy.py`
|
||||
- `quant.config.json`
|
||||
- `README.md`
|
||||
|
||||
## Templates
|
||||
|
||||
| Template | Pair | Timeframe | Model family | Use case |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `eurusd-logistic-15m` | EURUSD | 15m | sklearn | Baseline directional classifier |
|
||||
| `eurusd-xgboost-1h` | EURUSD | 1h | XGBoost | Feature-rich trend classifier |
|
||||
| `gbpusd-breakout-rf` | GBPUSD | 30m | sklearn RandomForest | Range breakout classifier |
|
||||
| `usdjpy-mean-reversion` | USDJPY | 1h | sklearn | Mean reversion baseline |
|
||||
| `xauusd-regime-xgboost-v11` | XAUUSD | 1h | XGBoost | Less restrictive Au-79-style gold model |
|
||||
| `xauusd-atr-breakout` | XAUUSD | 15m | custom Python | ATR breakout rules |
|
||||
| `btcusdt-lightgbm-1h` | BTCUSDT | 1h | LightGBM | Crypto trend classifier |
|
||||
| `ethusdt-volatility-classifier` | ETHUSDT | 30m | sklearn | Volatility regime classifier |
|
||||
| `solusdt-scalp-baseline` | SOLUSDT | 1m | custom Python | High-volatility scalp baseline |
|
||||
| `onnx-export-sklearn-starter` | EURUSD | 1h | sklearn to ONNX | ONNX export starter |
|
||||
| `statsmodels-arima-direction` | EURUSD | 1h | statsmodels | Statistical direction baseline |
|
||||
| `lightgbm-fx-multifeature` | EURUSD | 30m | LightGBM | Multi-feature FX classifier |
|
||||
|
||||
## Use With PyP
|
||||
|
||||
1. Open PyP Quant Mode:
|
||||
https://pyp.stanlink.online/projects/quant/new
|
||||
2. Create a new quant project.
|
||||
3. Copy a template's `strategy.py` and `quant.config.json`.
|
||||
4. Run a training job.
|
||||
5. Validate with PPE simulation.
|
||||
6. Deploy only after the strategy produces acceptable out-of-sample behavior.
|
||||
|
||||
## PyP Links
|
||||
|
||||
- Quant landing page: https://pyp.stanl.ink/for-quant-traders
|
||||
- Quant docs: https://pyp.stanl.ink/docs/quant/what-is-quant-mode
|
||||
- Create a quant project: https://pyp.stanlink.online/projects/quant/new
|
||||
|
||||
## Risk Disclaimer
|
||||
|
||||
Trading foreign exchange, CFDs, crypto, and leveraged products involves substantial risk. These templates are educational examples only. Past performance and simulated results do not guarantee future performance.
|
||||
@@ -0,0 +1,26 @@
|
||||
# How To Use These Templates
|
||||
|
||||
Each folder is a PyP Quant project skeleton. Copy the files into a new PyP Quant project and run training from the dashboard.
|
||||
|
||||
## Required Files
|
||||
|
||||
- `strategy.py` contains `train()` and `predict()`.
|
||||
- `quant.config.json` declares pair, timeframe, model family, artifact format, and requirements.
|
||||
- `README.md` explains the project intent and tuning knobs.
|
||||
|
||||
## Recommended Workflow
|
||||
|
||||
1. Train the model.
|
||||
2. Run PPE simulation.
|
||||
3. Inspect trade count, drawdown, profit factor, win rate, and session behavior.
|
||||
4. Adjust label thresholds or signal gates.
|
||||
5. Train again.
|
||||
6. Deploy only after out-of-sample validation.
|
||||
|
||||
## Common Tuning Knobs
|
||||
|
||||
- `threshold`: minimum forward move for UP/DOWN labels.
|
||||
- `horizon`: bars ahead used for training labels.
|
||||
- `min_confidence`: inference confidence gate.
|
||||
- `lookback`: bars required before prediction.
|
||||
- `sl_percent` and `tp_percent`: stop-loss and take-profit defaults for simulations.
|
||||
@@ -0,0 +1,6 @@
|
||||
numpy
|
||||
pandas
|
||||
scikit-learn
|
||||
xgboost
|
||||
lightgbm
|
||||
statsmodels
|
||||
@@ -0,0 +1,3 @@
|
||||
# BTCUSDT LightGBM 1h
|
||||
|
||||
Crypto trend classifier for BTCUSDT on 1 hour candles using LightGBM.
|
||||
@@ -0,0 +1 @@
|
||||
{"pair":"BTCUSDT","timeframe":"1h","model_family":"lightgbm","runtime_target":"modal","artifact_format":"joblib_bundle","parameters":{"lookback":120,"horizon":3,"threshold":0.006,"min_confidence":0.46},"training_requirements":["numpy","pandas","scikit-learn","lightgbm","joblib"],"inference_requirements":["numpy","pandas","scikit-learn","lightgbm","joblib"]}
|
||||
@@ -0,0 +1,53 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from lightgbm import LGBMClassifier
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
|
||||
def _prep(data):
|
||||
df = data.copy()
|
||||
df.columns = [str(c).lower() for c in df.columns]
|
||||
if "volume" not in df.columns:
|
||||
df["volume"] = 1.0
|
||||
for col in ["open", "high", "low", "close", "volume"]:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
return df.dropna().reset_index(drop=True)
|
||||
|
||||
|
||||
def _feat(df):
|
||||
c, v = df["close"], df["volume"]
|
||||
f = pd.DataFrame(index=df.index)
|
||||
for n in [1, 3, 6, 12, 24]:
|
||||
f[f"ret{n}"] = c.pct_change(n)
|
||||
f["volatility"] = c.pct_change().rolling(24).std()
|
||||
f["volume_z"] = (v - v.rolling(24).mean()) / (v.rolling(24).std() + 1e-9)
|
||||
f["ema_fast"] = (c.ewm(span=12, adjust=False).mean() - c.ewm(span=48, adjust=False).mean()) / c
|
||||
return f.replace([np.inf, -np.inf], np.nan).dropna()
|
||||
|
||||
|
||||
def train(data, config):
|
||||
p = config.get("parameters", {})
|
||||
df = _prep(data)
|
||||
x = _feat(df)
|
||||
fwd = df["close"].pct_change(int(p.get("horizon", 3))).shift(-int(p.get("horizon", 3)))
|
||||
threshold = float(p.get("threshold", 0.006))
|
||||
y = pd.Series(1, index=df.index)
|
||||
y[fwd > threshold] = 2
|
||||
y[fwd < -threshold] = 0
|
||||
y = y.reindex(x.index).fillna(1).astype(int)
|
||||
model = Pipeline([("scaler", StandardScaler()), ("clf", LGBMClassifier(n_estimators=300, learning_rate=0.04, max_depth=5, random_state=42, verbose=-1))])
|
||||
model.fit(x.values, y.values)
|
||||
return {"model": model, "features": list(x.columns)}, {"training_bars": int(len(x)), "class_dist": {"SELL": int((y == 0).sum()), "HOLD": int((y == 1).sum()), "BUY": int((y == 2).sum())}}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
candles = market_data.get("candles", [])
|
||||
if len(candles) < int(config.get("parameters", {}).get("lookback", 120)):
|
||||
return {"signal": "HOLD", "confidence": 0, "metadata": {"reason": "not_enough_candles"}}
|
||||
df = _prep(pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"]))
|
||||
row = _feat(df).tail(1)[model["features"]]
|
||||
prob = model["model"].predict_proba(row.values)[0]
|
||||
k, conf = int(np.argmax(prob)), float(np.max(prob))
|
||||
signal = {0: "DOWN", 1: "HOLD", 2: "UP"}[k] if conf >= float(config.get("parameters", {}).get("min_confidence", 0.46)) else "HOLD"
|
||||
return {"signal": signal, "confidence": round(conf, 4), "metadata": {"p_sell": round(float(prob[0]), 4), "p_hold": round(float(prob[1]), 4), "p_buy": round(float(prob[2]), 4)}}
|
||||
@@ -0,0 +1,3 @@
|
||||
# ETHUSDT Volatility Classifier
|
||||
|
||||
Volatility-aware ETHUSDT directional classifier.
|
||||
@@ -0,0 +1 @@
|
||||
{"pair":"ETHUSDT","timeframe":"30m","model_family":"sklearn","runtime_target":"edge","artifact_format":"weights_bundle","parameters":{"lookback":120,"horizon":4,"threshold":0.005,"min_confidence":0.5},"training_requirements":["numpy","pandas","scikit-learn","joblib"],"inference_requirements":["numpy","pandas","scikit-learn","joblib"]}
|
||||
@@ -0,0 +1,94 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
|
||||
def _prep(data):
|
||||
df = data.copy()
|
||||
df.columns = [str(c).strip().lower() for c in df.columns]
|
||||
if "volume" not in df.columns:
|
||||
df["volume"] = 1.0
|
||||
for col in ["open", "high", "low", "close", "volume"]:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
return df.dropna(subset=["open", "high", "low", "close"]).reset_index(drop=True)
|
||||
|
||||
|
||||
def _atr(df, n=14):
|
||||
h, l, c = df["high"], df["low"], df["close"]
|
||||
tr = pd.concat([(h - l), (h - c.shift()).abs(), (l - c.shift()).abs()], axis=1).max(axis=1)
|
||||
return tr.ewm(span=n, adjust=False).mean()
|
||||
|
||||
|
||||
def _features(df):
|
||||
c = df["close"]
|
||||
v = df["volume"]
|
||||
ret = c.pct_change()
|
||||
f = pd.DataFrame(index=df.index)
|
||||
for n in [1, 2, 4, 8, 16, 32]:
|
||||
f[f"ret{n}"] = c.pct_change(n)
|
||||
f["atr_pct"] = _atr(df, 14) / c
|
||||
f["atr_expansion"] = (_atr(df, 8) / (_atr(df, 50) + 1e-9)).clip(0, 5)
|
||||
f["rv_12"] = ret.rolling(12).std()
|
||||
f["rv_48"] = ret.rolling(48).std()
|
||||
f["vol_regime"] = f["rv_12"] / (f["rv_48"] + 1e-9)
|
||||
f["volume_z"] = (v - v.rolling(48).mean()) / (v.rolling(48).std() + 1e-9)
|
||||
f["ema_9_34"] = (c.ewm(span=9, adjust=False).mean() - c.ewm(span=34, adjust=False).mean()) / c
|
||||
f["ema_21_89"] = (c.ewm(span=21, adjust=False).mean() - c.ewm(span=89, adjust=False).mean()) / c
|
||||
return f.replace([np.inf, -np.inf], np.nan).dropna()
|
||||
|
||||
|
||||
def train(data, config):
|
||||
p = config.get("parameters", {})
|
||||
df = _prep(data)
|
||||
x = _features(df)
|
||||
horizon = int(p.get("horizon", 3))
|
||||
threshold = float(p.get("threshold", 0.004))
|
||||
fwd = df["close"].pct_change(horizon).shift(-horizon)
|
||||
y = pd.Series(1, index=df.index)
|
||||
y[fwd > threshold] = 2
|
||||
y[fwd < -threshold] = 0
|
||||
y = y.reindex(x.index).fillna(1).astype(int)
|
||||
model = Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", RandomForestClassifier(
|
||||
n_estimators=int(p.get("n_estimators", 300)),
|
||||
min_samples_leaf=int(p.get("min_samples_leaf", 8)),
|
||||
max_depth=int(p.get("max_depth", 8)),
|
||||
class_weight="balanced_subsample",
|
||||
random_state=42,
|
||||
n_jobs=-1,
|
||||
)),
|
||||
])
|
||||
model.fit(x.values, y.values)
|
||||
pred = model.predict(x.values)
|
||||
return {
|
||||
"model": model,
|
||||
"features": list(x.columns),
|
||||
}, {
|
||||
"training_bars": int(len(x)),
|
||||
"feature_count": int(x.shape[1]),
|
||||
"class_dist": {"SELL": int((y == 0).sum()), "HOLD": int((y == 1).sum()), "BUY": int((y == 2).sum())},
|
||||
"buy_signals": int((pred == 2).sum()),
|
||||
"sell_signals": int((pred == 0).sum()),
|
||||
"hold_signals": int((pred == 1).sum()),
|
||||
}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
p = config.get("parameters", {})
|
||||
candles = market_data.get("candles", [])
|
||||
if len(candles) < int(p.get("lookback", 120)):
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
|
||||
df = _prep(pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"]))
|
||||
row = _features(df).tail(1)
|
||||
if row.empty:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "no_features"}}
|
||||
prob = model["model"].predict_proba(row[model["features"]].values)[0]
|
||||
klass = int(np.argmax(prob))
|
||||
conf = float(np.max(prob))
|
||||
signal = {0: "DOWN", 1: "HOLD", 2: "UP"}[klass]
|
||||
if conf < float(p.get("min_confidence", 0.47)):
|
||||
signal = "HOLD"
|
||||
return {"signal": signal, "confidence": round(conf, 4), "metadata": {"p_sell": round(float(prob[0]), 4), "p_hold": round(float(prob[1]), 4), "p_buy": round(float(prob[2]), 4), "model": "ethusdt-volatility-classifier"}}
|
||||
@@ -0,0 +1,7 @@
|
||||
# EURUSD Logistic 15m
|
||||
|
||||
Baseline directional classifier for EURUSD on 15 minute candles.
|
||||
|
||||
This is a clean first quant project: engineered candle features, three-class labels, and a simple `LogisticRegression` model.
|
||||
|
||||
Use it to validate the full PyP pipeline before moving to heavier models.
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"pair": "EURUSD",
|
||||
"timeframe": "15m",
|
||||
"model_family": "sklearn",
|
||||
"runtime_target": "edge",
|
||||
"artifact_format": "weights_bundle",
|
||||
"parameters": {
|
||||
"lookback": 80,
|
||||
"horizon": 4,
|
||||
"threshold": 0.0008,
|
||||
"min_confidence": 0.48
|
||||
},
|
||||
"training_requirements": ["numpy", "pandas", "scikit-learn", "joblib"],
|
||||
"inference_requirements": ["numpy", "pandas", "scikit-learn", "joblib"]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
|
||||
def _normalise(data):
|
||||
df = data.copy()
|
||||
df.columns = [str(c).lower() for c in df.columns]
|
||||
if "volume" not in df.columns:
|
||||
df["volume"] = 1.0
|
||||
for col in ["open", "high", "low", "close", "volume"]:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
return df.dropna(subset=["open", "high", "low", "close"]).reset_index(drop=True)
|
||||
|
||||
|
||||
def _features(df):
|
||||
c = df["close"]
|
||||
rng = (df["high"] - df["low"]).replace(0, np.nan)
|
||||
out = pd.DataFrame(index=df.index)
|
||||
out["ret1"] = c.pct_change()
|
||||
out["ret4"] = c.pct_change(4)
|
||||
out["ret12"] = c.pct_change(12)
|
||||
out["range_pct"] = rng / c
|
||||
out["body_pct"] = (c - df["open"]) / rng
|
||||
out["close_pos"] = (c - df["low"]) / rng
|
||||
out["volatility"] = out["ret1"].rolling(20).std()
|
||||
out["ma_fast"] = (c.rolling(8).mean() - c.rolling(21).mean()) / c
|
||||
out["ma_slow"] = (c.rolling(21).mean() - c.rolling(55).mean()) / c
|
||||
return out.replace([np.inf, -np.inf], np.nan).dropna()
|
||||
|
||||
|
||||
def _labels(close, index, horizon, threshold):
|
||||
fwd = close.pct_change(horizon).shift(-horizon)
|
||||
y = pd.Series(1, index=close.index)
|
||||
y[fwd > threshold] = 2
|
||||
y[fwd < -threshold] = 0
|
||||
return y.reindex(index).fillna(1).astype(int)
|
||||
|
||||
|
||||
def train(data, config):
|
||||
params = config.get("parameters", {})
|
||||
horizon = int(params.get("horizon", 4))
|
||||
threshold = float(params.get("threshold", 0.0008))
|
||||
df = _normalise(data)
|
||||
feat = _features(df)
|
||||
y = _labels(df["close"], feat.index, horizon, threshold)
|
||||
model = Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", LogisticRegression(max_iter=1000, class_weight="balanced", multi_class="auto")),
|
||||
])
|
||||
model.fit(feat.values, y.values)
|
||||
preds = model.predict(feat.values)
|
||||
metrics = {
|
||||
"training_bars": int(len(feat)),
|
||||
"feature_count": int(feat.shape[1]),
|
||||
"buy_signals": int((preds == 2).sum()),
|
||||
"sell_signals": int((preds == 0).sum()),
|
||||
"hold_signals": int((preds == 1).sum()),
|
||||
}
|
||||
return {"model": model, "features": list(feat.columns)}, metrics
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
candles = market_data.get("candles", [])
|
||||
lookback = int(config.get("parameters", {}).get("lookback", 80))
|
||||
min_conf = float(config.get("parameters", {}).get("min_confidence", 0.48))
|
||||
if len(candles) < lookback:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
|
||||
df = _normalise(pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"]))
|
||||
feat = _features(df).tail(1)
|
||||
if feat.empty:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "no_features"}}
|
||||
proba = model["model"].predict_proba(feat.values)[0]
|
||||
klass = int(np.argmax(proba))
|
||||
conf = float(proba[klass])
|
||||
signal = {0: "DOWN", 1: "HOLD", 2: "UP"}[klass]
|
||||
if conf < min_conf:
|
||||
signal = "HOLD"
|
||||
return {"signal": signal, "confidence": round(conf, 4), "metadata": {"p_sell": round(float(proba[0]), 4), "p_hold": round(float(proba[1]), 4), "p_buy": round(float(proba[2]), 4)}}
|
||||
@@ -0,0 +1,3 @@
|
||||
# EURUSD XGBoost 1h
|
||||
|
||||
Feature-rich EURUSD trend classifier using XGBoost.
|
||||
@@ -0,0 +1 @@
|
||||
{"pair":"EURUSD","timeframe":"1h","model_family":"xgboost","runtime_target":"modal","artifact_format":"joblib_bundle","parameters":{"lookback":160,"horizon":3,"threshold":0.001,"min_confidence":0.48},"training_requirements":["numpy","pandas","scikit-learn","xgboost","joblib"],"inference_requirements":["numpy","pandas","scikit-learn","xgboost","joblib"]}
|
||||
@@ -0,0 +1,120 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from xgboost import XGBClassifier
|
||||
|
||||
|
||||
def _normalise(data):
|
||||
df = data.copy()
|
||||
df.columns = [str(c).strip().lower() for c in df.columns]
|
||||
aliases = {"o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"}
|
||||
df.rename(columns=aliases, inplace=True)
|
||||
if "volume" not in df.columns:
|
||||
df["volume"] = 1.0
|
||||
for col in ["open", "high", "low", "close", "volume"]:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
return df.dropna(subset=["open", "high", "low", "close"]).reset_index(drop=True)
|
||||
|
||||
|
||||
def _rsi(close, n=14):
|
||||
delta = close.diff()
|
||||
gain = delta.clip(lower=0).ewm(alpha=1 / n, adjust=False).mean()
|
||||
loss = (-delta.clip(upper=0)).ewm(alpha=1 / n, adjust=False).mean()
|
||||
return 100 - 100 / (1 + gain / (loss + 1e-9))
|
||||
|
||||
|
||||
def _features(df):
|
||||
c = df["close"]
|
||||
h = df["high"]
|
||||
l = df["low"]
|
||||
o = df["open"]
|
||||
v = df["volume"]
|
||||
rng = (h - l).replace(0, np.nan)
|
||||
f = pd.DataFrame(index=df.index)
|
||||
for n in [1, 3, 6, 12, 24]:
|
||||
f[f"ret{n}"] = c.pct_change(n)
|
||||
f["range_pct"] = rng / c
|
||||
f["body_pct"] = (c - o) / rng
|
||||
f["close_pos"] = (c - l) / (rng + 1e-9)
|
||||
f["volatility_24"] = c.pct_change().rolling(24).std()
|
||||
f["volatility_72"] = c.pct_change().rolling(72).std()
|
||||
f["ema_12_48"] = (c.ewm(span=12, adjust=False).mean() - c.ewm(span=48, adjust=False).mean()) / c
|
||||
f["ema_24_96"] = (c.ewm(span=24, adjust=False).mean() - c.ewm(span=96, adjust=False).mean()) / c
|
||||
f["rsi14"] = (_rsi(c, 14) - 50) / 50
|
||||
f["volume_z"] = (v - v.rolling(48).mean()) / (v.rolling(48).std() + 1e-9)
|
||||
return f.replace([np.inf, -np.inf], np.nan).dropna()
|
||||
|
||||
|
||||
def _labels(close, index, horizon, threshold):
|
||||
fwd = close.pct_change(horizon).shift(-horizon)
|
||||
y = pd.Series(1, index=close.index)
|
||||
y[fwd > threshold] = 2
|
||||
y[fwd < -threshold] = 0
|
||||
return y.reindex(index).fillna(1).astype(int)
|
||||
|
||||
|
||||
def train(data, config):
|
||||
params = config.get("parameters", {})
|
||||
df = _normalise(data)
|
||||
feat = _features(df)
|
||||
horizon = int(params.get("horizon", 4))
|
||||
threshold = float(params.get("threshold", 0.0012))
|
||||
y = _labels(df["close"], feat.index, horizon, threshold)
|
||||
scaler = StandardScaler()
|
||||
x = scaler.fit_transform(feat.values.astype(np.float32))
|
||||
clf = XGBClassifier(
|
||||
n_estimators=int(params.get("n_estimators", 250)),
|
||||
max_depth=int(params.get("max_depth", 4)),
|
||||
learning_rate=float(params.get("learning_rate", 0.04)),
|
||||
subsample=float(params.get("subsample", 0.8)),
|
||||
colsample_bytree=float(params.get("colsample_bytree", 0.85)),
|
||||
objective="multi:softprob",
|
||||
num_class=3,
|
||||
eval_metric="mlogloss",
|
||||
tree_method="hist",
|
||||
random_state=42,
|
||||
)
|
||||
clf.fit(x, y.values)
|
||||
preds = clf.predict(x)
|
||||
metrics = {
|
||||
"training_bars": int(len(feat)),
|
||||
"feature_count": int(feat.shape[1]),
|
||||
"class_dist": {
|
||||
"SELL": int((y == 0).sum()),
|
||||
"HOLD": int((y == 1).sum()),
|
||||
"BUY": int((y == 2).sum()),
|
||||
},
|
||||
"buy_signals": int((preds == 2).sum()),
|
||||
"sell_signals": int((preds == 0).sum()),
|
||||
"hold_signals": int((preds == 1).sum()),
|
||||
}
|
||||
return {"model": clf, "scaler": scaler, "features": list(feat.columns)}, metrics
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
params = config.get("parameters", {})
|
||||
candles = market_data.get("candles", [])
|
||||
if len(candles) < int(params.get("lookback", 140)):
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
|
||||
df = _normalise(pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"]))
|
||||
row = _features(df).tail(1)
|
||||
if row.empty:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "no_features"}}
|
||||
row = row[model["features"]]
|
||||
x = model["scaler"].transform(row.values.astype(np.float32))
|
||||
prob = model["model"].predict_proba(x)[0]
|
||||
klass = int(np.argmax(prob))
|
||||
conf = float(np.max(prob))
|
||||
signal = {0: "DOWN", 1: "HOLD", 2: "UP"}[klass]
|
||||
if conf < float(params.get("min_confidence", 0.48)):
|
||||
signal = "HOLD"
|
||||
return {
|
||||
"signal": signal,
|
||||
"confidence": round(conf, 4),
|
||||
"metadata": {
|
||||
"p_sell": round(float(prob[0]), 4),
|
||||
"p_hold": round(float(prob[1]), 4),
|
||||
"p_buy": round(float(prob[2]), 4),
|
||||
"model": "eurusd-xgboost-1h",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# GBPUSD Breakout RandomForest
|
||||
|
||||
Range-breakout classifier for GBPUSD.
|
||||
@@ -0,0 +1 @@
|
||||
{"pair":"GBPUSD","timeframe":"30m","model_family":"sklearn","runtime_target":"edge","artifact_format":"weights_bundle","parameters":{"lookback":120,"horizon":4,"threshold":0.0012,"min_confidence":0.5},"training_requirements":["numpy","pandas","scikit-learn","joblib"],"inference_requirements":["numpy","pandas","scikit-learn","joblib"]}
|
||||
@@ -0,0 +1,45 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
|
||||
|
||||
def _dataset(data, horizon=4, threshold=0.0012):
|
||||
df = data.copy()
|
||||
df.columns = [str(c).lower() for c in df.columns]
|
||||
if "volume" not in df.columns:
|
||||
df["volume"] = 1
|
||||
for col in ["open", "high", "low", "close", "volume"]:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
c = df["close"]
|
||||
x = pd.DataFrame(index=df.index)
|
||||
x["range_break"] = (c - df["high"].rolling(24).max().shift()) / c
|
||||
x["range_floor"] = (c - df["low"].rolling(24).min().shift()) / c
|
||||
x["ret4"] = c.pct_change(4)
|
||||
x["ret12"] = c.pct_change(12)
|
||||
x["vol"] = c.pct_change().rolling(20).std()
|
||||
x = x.replace([np.inf, -np.inf], np.nan).dropna()
|
||||
fwd = c.pct_change(horizon).shift(-horizon)
|
||||
y = pd.Series(1, index=df.index)
|
||||
y[fwd > threshold] = 2
|
||||
y[fwd < -threshold] = 0
|
||||
return x, y.reindex(x.index).fillna(1).astype(int)
|
||||
|
||||
|
||||
def train(data, config):
|
||||
p = config.get("parameters", {})
|
||||
x, y = _dataset(data, int(p.get("horizon", 4)), float(p.get("threshold", 0.0012)))
|
||||
clf = RandomForestClassifier(n_estimators=300, max_depth=7, class_weight="balanced_subsample", random_state=42)
|
||||
clf.fit(x.values, y.values)
|
||||
return {"model": clf, "features": list(x.columns)}, {"training_bars": int(len(x)), "feature_count": int(x.shape[1])}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
candles = market_data.get("candles", [])
|
||||
if len(candles) < int(config.get("parameters", {}).get("lookback", 120)):
|
||||
return {"signal": "HOLD", "confidence": 0, "metadata": {"reason": "not_enough_candles"}}
|
||||
x, _ = _dataset(pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"]))
|
||||
row = x.tail(1)[model["features"]]
|
||||
prob = model["model"].predict_proba(row.values)[0]
|
||||
k = int(np.argmax(prob))
|
||||
conf = float(np.max(prob))
|
||||
return {"signal": {0: "DOWN", 1: "HOLD", 2: "UP"}[k] if conf >= float(config.get("parameters", {}).get("min_confidence", 0.5)) else "HOLD", "confidence": round(conf, 4), "metadata": {"proba": [round(float(v), 4) for v in prob]}}
|
||||
@@ -0,0 +1,3 @@
|
||||
# LightGBM FX Multi Feature
|
||||
|
||||
LightGBM starter for multi-feature FX classification.
|
||||
@@ -0,0 +1 @@
|
||||
{"pair":"EURUSD","timeframe":"30m","model_family":"lightgbm","runtime_target":"modal","artifact_format":"joblib_bundle","parameters":{"lookback":140,"horizon":4,"threshold":0.001,"min_confidence":0.48},"training_requirements":["numpy","pandas","scikit-learn","lightgbm","joblib"],"inference_requirements":["numpy","pandas","scikit-learn","lightgbm","joblib"]}
|
||||
@@ -0,0 +1,92 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from lightgbm import LGBMClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
|
||||
def _prep(data):
|
||||
df = data.copy()
|
||||
df.columns = [str(c).strip().lower() for c in df.columns]
|
||||
if "volume" not in df.columns:
|
||||
df["volume"] = 1.0
|
||||
for col in ["open", "high", "low", "close", "volume"]:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
return df.dropna(subset=["open", "high", "low", "close"]).reset_index(drop=True)
|
||||
|
||||
|
||||
def _rsi(close, n=14):
|
||||
d = close.diff()
|
||||
g = d.clip(lower=0).ewm(alpha=1 / n, adjust=False).mean()
|
||||
l = (-d.clip(upper=0)).ewm(alpha=1 / n, adjust=False).mean()
|
||||
return 100 - 100 / (1 + g / (l + 1e-9))
|
||||
|
||||
|
||||
def _features(df):
|
||||
c = df["close"]
|
||||
v = df["volume"]
|
||||
f = pd.DataFrame(index=df.index)
|
||||
for n in [1, 2, 4, 8, 16, 32, 64]:
|
||||
f[f"ret{n}"] = c.pct_change(n)
|
||||
f["ema_8_21"] = (c.ewm(span=8, adjust=False).mean() - c.ewm(span=21, adjust=False).mean()) / c
|
||||
f["ema_21_55"] = (c.ewm(span=21, adjust=False).mean() - c.ewm(span=55, adjust=False).mean()) / c
|
||||
f["ema_55_144"] = (c.ewm(span=55, adjust=False).mean() - c.ewm(span=144, adjust=False).mean()) / c
|
||||
f["rsi14"] = (_rsi(c, 14) - 50) / 50
|
||||
f["rsi5"] = (_rsi(c, 5) - 50) / 50
|
||||
f["volatility_32"] = c.pct_change().rolling(32).std()
|
||||
f["volatility_96"] = c.pct_change().rolling(96).std()
|
||||
f["volume_z"] = (v - v.rolling(48).mean()) / (v.rolling(48).std() + 1e-9)
|
||||
return f.replace([np.inf, -np.inf], np.nan).dropna()
|
||||
|
||||
|
||||
def train(data, config):
|
||||
p = config.get("parameters", {})
|
||||
df = _prep(data)
|
||||
x = _features(df)
|
||||
horizon = int(p.get("horizon", 6))
|
||||
threshold = float(p.get("threshold", 0.0015))
|
||||
fwd = df["close"].pct_change(horizon).shift(-horizon)
|
||||
y = pd.Series(1, index=df.index)
|
||||
y[fwd > threshold] = 2
|
||||
y[fwd < -threshold] = 0
|
||||
y = y.reindex(x.index).fillna(1).astype(int)
|
||||
scaler = StandardScaler()
|
||||
x_scaled = scaler.fit_transform(x.values.astype(np.float32))
|
||||
clf = LGBMClassifier(
|
||||
n_estimators=int(p.get("n_estimators", 350)),
|
||||
learning_rate=float(p.get("learning_rate", 0.035)),
|
||||
num_leaves=int(p.get("num_leaves", 31)),
|
||||
max_depth=int(p.get("max_depth", -1)),
|
||||
subsample=float(p.get("subsample", 0.85)),
|
||||
colsample_bytree=float(p.get("colsample_bytree", 0.85)),
|
||||
random_state=42,
|
||||
verbose=-1,
|
||||
)
|
||||
clf.fit(x_scaled, y.values)
|
||||
pred = clf.predict(x_scaled)
|
||||
return {"model": clf, "scaler": scaler, "features": list(x.columns)}, {
|
||||
"training_bars": int(len(x)),
|
||||
"feature_count": int(x.shape[1]),
|
||||
"class_dist": {"SELL": int((y == 0).sum()), "HOLD": int((y == 1).sum()), "BUY": int((y == 2).sum())},
|
||||
"buy_signals": int((pred == 2).sum()),
|
||||
"sell_signals": int((pred == 0).sum()),
|
||||
"hold_signals": int((pred == 1).sum()),
|
||||
}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
p = config.get("parameters", {})
|
||||
candles = market_data.get("candles", [])
|
||||
if len(candles) < int(p.get("lookback", 180)):
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
|
||||
df = _prep(pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"]))
|
||||
row = _features(df).tail(1)
|
||||
if row.empty:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "no_features"}}
|
||||
x = model["scaler"].transform(row[model["features"]].values.astype(np.float32))
|
||||
prob = model["model"].predict_proba(x)[0]
|
||||
klass = int(np.argmax(prob))
|
||||
conf = float(np.max(prob))
|
||||
signal = {0: "DOWN", 1: "HOLD", 2: "UP"}[klass]
|
||||
if conf < float(p.get("min_confidence", 0.48)):
|
||||
signal = "HOLD"
|
||||
return {"signal": signal, "confidence": round(conf, 4), "metadata": {"p_sell": round(float(prob[0]), 4), "p_hold": round(float(prob[1]), 4), "p_buy": round(float(prob[2]), 4), "model": "lightgbm-fx-multifeature"}}
|
||||
@@ -0,0 +1,5 @@
|
||||
# ONNX Export Sklearn Starter
|
||||
|
||||
Starter project for training a small sklearn classifier and exporting it to ONNX in a follow-up packaging step.
|
||||
|
||||
The included `strategy.py` remains standard sklearn so it can be trained first, inspected, and then converted.
|
||||
@@ -0,0 +1 @@
|
||||
{"pair":"EURUSD","timeframe":"1h","model_family":"sklearn","runtime_target":"container","artifact_format":"onnx","parameters":{"lookback":120,"horizon":3,"threshold":0.001,"min_confidence":0.5},"training_requirements":["numpy","pandas","scikit-learn","joblib","skl2onnx","onnx"],"inference_requirements":["numpy","onnxruntime"]}
|
||||
@@ -0,0 +1,74 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
|
||||
def _prep(data):
|
||||
df = data.copy()
|
||||
df.columns = [str(c).strip().lower() for c in df.columns]
|
||||
if "volume" not in df.columns:
|
||||
df["volume"] = 1.0
|
||||
for col in ["open", "high", "low", "close", "volume"]:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
return df.dropna(subset=["open", "high", "low", "close"]).reset_index(drop=True)
|
||||
|
||||
|
||||
def _features(df):
|
||||
c = df["close"]
|
||||
rng = (df["high"] - df["low"]).replace(0, np.nan)
|
||||
f = pd.DataFrame(index=df.index)
|
||||
f["ret1"] = c.pct_change(1)
|
||||
f["ret5"] = c.pct_change(5)
|
||||
f["ret20"] = c.pct_change(20)
|
||||
f["range_pct"] = rng / c
|
||||
f["body_pct"] = (c - df["open"]) / (rng + 1e-9)
|
||||
f["ema_10_40"] = (c.ewm(span=10, adjust=False).mean() - c.ewm(span=40, adjust=False).mean()) / c
|
||||
f["volatility_20"] = c.pct_change().rolling(20).std()
|
||||
f["volume_ratio"] = df["volume"] / (df["volume"].rolling(20).mean() + 1e-9)
|
||||
return f.replace([np.inf, -np.inf], np.nan).dropna()
|
||||
|
||||
|
||||
def train(data, config):
|
||||
p = config.get("parameters", {})
|
||||
df = _prep(data)
|
||||
x = _features(df)
|
||||
horizon = int(p.get("horizon", 4))
|
||||
threshold = float(p.get("threshold", 0.001))
|
||||
fwd = df["close"].pct_change(horizon).shift(-horizon)
|
||||
y = pd.Series(1, index=df.index)
|
||||
y[fwd > threshold] = 2
|
||||
y[fwd < -threshold] = 0
|
||||
y = y.reindex(x.index).fillna(1).astype(int)
|
||||
model = Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", LogisticRegression(max_iter=1000, class_weight="balanced")),
|
||||
])
|
||||
model.fit(x.values.astype(np.float32), y.values)
|
||||
pred = model.predict(x.values.astype(np.float32))
|
||||
return {"model": model, "features": list(x.columns), "onnx_export_hint": "Convert this sklearn Pipeline with skl2onnx after training."}, {
|
||||
"training_bars": int(len(x)),
|
||||
"feature_count": int(x.shape[1]),
|
||||
"buy_signals": int((pred == 2).sum()),
|
||||
"sell_signals": int((pred == 0).sum()),
|
||||
"hold_signals": int((pred == 1).sum()),
|
||||
}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
p = config.get("parameters", {})
|
||||
candles = market_data.get("candles", [])
|
||||
if len(candles) < int(p.get("lookback", 80)):
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
|
||||
df = _prep(pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"]))
|
||||
row = _features(df).tail(1)
|
||||
if row.empty:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "no_features"}}
|
||||
prob = model["model"].predict_proba(row[model["features"]].values.astype(np.float32))[0]
|
||||
klass = int(np.argmax(prob))
|
||||
conf = float(np.max(prob))
|
||||
signal = {0: "DOWN", 1: "HOLD", 2: "UP"}[klass]
|
||||
if conf < float(p.get("min_confidence", 0.48)):
|
||||
signal = "HOLD"
|
||||
return {"signal": signal, "confidence": round(conf, 4), "metadata": {"p_sell": round(float(prob[0]), 4), "p_hold": round(float(prob[1]), 4), "p_buy": round(float(prob[2]), 4), "model": "onnx-export-sklearn-starter"}}
|
||||
@@ -0,0 +1,5 @@
|
||||
# SOLUSDT Scalp Baseline
|
||||
|
||||
High-volatility 1 minute baseline for SOLUSDT.
|
||||
|
||||
This is intentionally simple and transparent. It is useful for stress-testing PyP's live signal loop on fast crypto candles.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"pair": "SOLUSDT",
|
||||
"timeframe": "1m",
|
||||
"model_family": "custom_python",
|
||||
"runtime_target": "edge",
|
||||
"artifact_format": "python_bundle",
|
||||
"parameters": {
|
||||
"lookback": 90,
|
||||
"fast": 8,
|
||||
"slow": 34,
|
||||
"vol_window": 20,
|
||||
"min_move": 0.0006
|
||||
},
|
||||
"training_requirements": ["numpy", "pandas"],
|
||||
"inference_requirements": ["numpy", "pandas"]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def train(data, config):
|
||||
return {"params": config.get("parameters", {}), "name": "solusdt_scalp_baseline"}, {"training_bars": int(len(data)), "model": "rule_baseline"}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
p = {**model.get("params", {}), **config.get("parameters", {})}
|
||||
candles = market_data.get("candles", [])
|
||||
lookback = int(p.get("lookback", 90))
|
||||
if len(candles) < lookback:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
|
||||
df = pd.DataFrame(candles[-lookback:], columns=["open", "high", "low", "close", "volume"]).astype(float)
|
||||
close = df["close"]
|
||||
fast = close.ewm(span=int(p.get("fast", 8)), adjust=False).mean()
|
||||
slow = close.ewm(span=int(p.get("slow", 34)), adjust=False).mean()
|
||||
ret = close.pct_change()
|
||||
vol = ret.rolling(int(p.get("vol_window", 20))).std().iloc[-1]
|
||||
slope = (fast.iloc[-1] - slow.iloc[-1]) / close.iloc[-1]
|
||||
min_move = float(p.get("min_move", 0.0006))
|
||||
confidence = min(0.9, abs(slope) / max(float(vol or 1e-6), 1e-6))
|
||||
if slope > min_move:
|
||||
return {"signal": "UP", "confidence": round(float(confidence), 4), "metadata": {"slope": float(slope), "vol": float(vol)}}
|
||||
if slope < -min_move:
|
||||
return {"signal": "DOWN", "confidence": round(float(confidence), 4), "metadata": {"slope": float(slope), "vol": float(vol)}}
|
||||
return {"signal": "HOLD", "confidence": 0.2, "metadata": {"slope": float(slope), "vol": float(vol)}}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Statsmodels ARIMA Direction
|
||||
|
||||
Statistical baseline for directional prediction using recent returns.
|
||||
@@ -0,0 +1 @@
|
||||
{"pair":"EURUSD","timeframe":"1h","model_family":"statsmodels","runtime_target":"modal","artifact_format":"joblib_bundle","parameters":{"lookback":96,"entry_threshold":0.0004},"training_requirements":["numpy","pandas","statsmodels","joblib"],"inference_requirements":["numpy","pandas","statsmodels","joblib"]}
|
||||
@@ -0,0 +1,21 @@
|
||||
import pandas as pd
|
||||
from statsmodels.tsa.arima.model import ARIMA
|
||||
|
||||
|
||||
def train(data, config):
|
||||
df = data.copy()
|
||||
df.columns = [str(c).lower() for c in df.columns]
|
||||
close = pd.to_numeric(df["close"], errors="coerce").dropna()
|
||||
ret = close.pct_change().dropna().tail(1500)
|
||||
fit = ARIMA(ret, order=(1, 0, 1)).fit()
|
||||
return {"fit": fit, "threshold": config.get("parameters", {}).get("entry_threshold", 0.0004)}, {"training_bars": int(len(ret)), "aic": float(fit.aic)}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
threshold = float(config.get("parameters", {}).get("entry_threshold", model.get("threshold", 0.0004)))
|
||||
forecast = float(model["fit"].forecast(1).iloc[0])
|
||||
if forecast > threshold:
|
||||
return {"signal": "UP", "confidence": min(0.8, abs(forecast) / threshold / 3), "metadata": {"forecast_return": forecast}}
|
||||
if forecast < -threshold:
|
||||
return {"signal": "DOWN", "confidence": min(0.8, abs(forecast) / threshold / 3), "metadata": {"forecast_return": forecast}}
|
||||
return {"signal": "HOLD", "confidence": 0.25, "metadata": {"forecast_return": forecast}}
|
||||
@@ -0,0 +1,3 @@
|
||||
# USDJPY Mean Reversion
|
||||
|
||||
Mean-reversion baseline for USDJPY on 1 hour candles.
|
||||
@@ -0,0 +1 @@
|
||||
{"pair":"USDJPY","timeframe":"1h","model_family":"custom_python","runtime_target":"edge","artifact_format":"python_bundle","parameters":{"lookback":100,"z_window":40,"entry_z":1.4},"training_requirements":["numpy","pandas"],"inference_requirements":["numpy","pandas"]}
|
||||
@@ -0,0 +1,24 @@
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def train(data, config):
|
||||
return {"params": config.get("parameters", {}), "name": "usdjpy_mean_reversion"}, {"training_bars": int(len(data)), "model": "rule_baseline"}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
p = {**model.get("params", {}), **config.get("parameters", {})}
|
||||
candles = market_data.get("candles", [])
|
||||
lookback = int(p.get("lookback", 100))
|
||||
if len(candles) < lookback:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
|
||||
close = pd.Series([float(c[3]) for c in candles[-lookback:]])
|
||||
n = int(p.get("z_window", 40))
|
||||
mean = close.rolling(n).mean().iloc[-1]
|
||||
std = close.rolling(n).std().iloc[-1] or 1e-9
|
||||
z = float((close.iloc[-1] - mean) / std)
|
||||
entry = float(p.get("entry_z", 1.4))
|
||||
if z <= -entry:
|
||||
return {"signal": "UP", "confidence": min(0.88, abs(z) / 3), "metadata": {"zscore": z}}
|
||||
if z >= entry:
|
||||
return {"signal": "DOWN", "confidence": min(0.88, abs(z) / 3), "metadata": {"zscore": z}}
|
||||
return {"signal": "HOLD", "confidence": 0.25, "metadata": {"zscore": z}}
|
||||
@@ -0,0 +1,5 @@
|
||||
# XAUUSD ATR Breakout
|
||||
|
||||
Custom Python breakout baseline for XAUUSD.
|
||||
|
||||
This project avoids ML on purpose. It is useful as a transparent baseline to compare against heavier gold models like XGBoost or LightGBM.
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"pair": "XAUUSD",
|
||||
"timeframe": "15m",
|
||||
"model_family": "custom_python",
|
||||
"runtime_target": "edge",
|
||||
"artifact_format": "python_bundle",
|
||||
"parameters": {
|
||||
"lookback": 64,
|
||||
"atr_window": 14,
|
||||
"breakout_window": 24,
|
||||
"atr_mult": 0.25
|
||||
},
|
||||
"training_requirements": ["numpy", "pandas"],
|
||||
"inference_requirements": ["numpy", "pandas"]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def _df(candles):
|
||||
df = pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"])
|
||||
for col in df.columns:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
return df.dropna().reset_index(drop=True)
|
||||
|
||||
|
||||
def _atr(df, n):
|
||||
prev = df["close"].shift()
|
||||
tr = pd.concat([(df["high"] - df["low"]), (df["high"] - prev).abs(), (df["low"] - prev).abs()], axis=1).max(axis=1)
|
||||
return tr.ewm(span=n, adjust=False).mean()
|
||||
|
||||
|
||||
def train(data, config):
|
||||
params = config.get("parameters", {})
|
||||
return {"params": params, "name": "xauusd_atr_breakout"}, {"training_bars": int(len(data)), "model": "rule_baseline"}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
params = {**model.get("params", {}), **config.get("parameters", {})}
|
||||
lookback = int(params.get("lookback", 64))
|
||||
atr_window = int(params.get("atr_window", 14))
|
||||
breakout_window = int(params.get("breakout_window", 24))
|
||||
atr_mult = float(params.get("atr_mult", 0.25))
|
||||
candles = market_data.get("candles", [])
|
||||
if len(candles) < lookback:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
|
||||
df = _df(candles[-lookback:])
|
||||
atr = float(_atr(df, atr_window).iloc[-1])
|
||||
close = float(df["close"].iloc[-1])
|
||||
high = float(df["high"].iloc[-breakout_window:-1].max())
|
||||
low = float(df["low"].iloc[-breakout_window:-1].min())
|
||||
if close > high + atr * atr_mult:
|
||||
return {"signal": "UP", "confidence": 0.64, "metadata": {"breakout": "high", "atr": atr}}
|
||||
if close < low - atr * atr_mult:
|
||||
return {"signal": "DOWN", "confidence": 0.64, "metadata": {"breakout": "low", "atr": atr}}
|
||||
return {"signal": "HOLD", "confidence": 0.2, "metadata": {"high": high, "low": low, "atr": atr}}
|
||||
@@ -0,0 +1,5 @@
|
||||
# XAUUSD Regime XGBoost v1.1
|
||||
|
||||
Gold directional classifier inspired by Au-79, but less restrictive.
|
||||
|
||||
The goal is to avoid the common no-trade failure mode by using a lower label threshold and a softer live gate.
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"pair": "XAUUSD",
|
||||
"timeframe": "1h",
|
||||
"model_family": "xgboost",
|
||||
"runtime_target": "modal",
|
||||
"artifact_format": "joblib_bundle",
|
||||
"parameters": {
|
||||
"lookback": 220,
|
||||
"horizon": 6,
|
||||
"threshold": 0.0015,
|
||||
"atr_mult": 1.0,
|
||||
"min_direction_prob": 0.38,
|
||||
"min_edge": 0.03
|
||||
},
|
||||
"training_requirements": ["numpy", "pandas", "scikit-learn", "xgboost", "joblib"],
|
||||
"inference_requirements": ["numpy", "pandas", "scikit-learn", "xgboost", "joblib"]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from xgboost import XGBClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.pipeline import Pipeline
|
||||
|
||||
|
||||
def _normalise(data):
|
||||
df = data.copy()
|
||||
df.columns = [str(c).lower() for c in df.columns]
|
||||
if "volume" not in df.columns:
|
||||
df["volume"] = 1.0
|
||||
for col in ["open", "high", "low", "close", "volume"]:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
return df.dropna(subset=["open", "high", "low", "close"]).reset_index(drop=True)
|
||||
|
||||
|
||||
def _atr(df, n):
|
||||
prev = df["close"].shift()
|
||||
tr = pd.concat([(df["high"] - df["low"]), (df["high"] - prev).abs(), (df["low"] - prev).abs()], axis=1).max(axis=1)
|
||||
return tr.ewm(span=n, adjust=False).mean()
|
||||
|
||||
|
||||
def _features(df):
|
||||
c = df["close"]
|
||||
atr8 = _atr(df, 8)
|
||||
atr50 = _atr(df, 50)
|
||||
f = pd.DataFrame(index=df.index)
|
||||
f["hurst_proxy"] = atr8 / (atr50 + 1e-10)
|
||||
f["atr_norm"] = _atr(df, 14) / (c + 1e-10)
|
||||
f["ret3"] = c.pct_change(3)
|
||||
f["ret6"] = c.pct_change(6)
|
||||
f["ret20"] = c.pct_change(20)
|
||||
f["ema8_21"] = (c.ewm(span=8, adjust=False).mean() - c.ewm(span=21, adjust=False).mean()) / c
|
||||
f["ema21_55"] = (c.ewm(span=21, adjust=False).mean() - c.ewm(span=55, adjust=False).mean()) / c
|
||||
f["price_vs_200"] = (c - c.ewm(span=200, adjust=False).mean()) / c
|
||||
rng = (df["high"] - df["low"]).replace(0, np.nan)
|
||||
f["body_pct"] = (c - df["open"]) / rng
|
||||
f["close_position"] = (c - df["low"]) / rng
|
||||
return f.iloc[200:].replace([np.inf, -np.inf], np.nan).dropna()
|
||||
|
||||
|
||||
def _labels(df, index, horizon, threshold, atr_mult):
|
||||
close = df["close"]
|
||||
fwd = close.pct_change(horizon).shift(-horizon)
|
||||
dyn = ((_atr(df, 14) / close) * atr_mult).clip(lower=threshold)
|
||||
y = pd.Series(1, index=df.index)
|
||||
y[fwd > dyn] = 2
|
||||
y[fwd < -dyn] = 0
|
||||
return y.reindex(index).fillna(1).astype(int)
|
||||
|
||||
|
||||
def train(data, config):
|
||||
params = config.get("parameters", {})
|
||||
df = _normalise(data)
|
||||
feat = _features(df)
|
||||
y = _labels(df, feat.index, int(params.get("horizon", 6)), float(params.get("threshold", 0.0015)), float(params.get("atr_mult", 1.0)))
|
||||
model = Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", XGBClassifier(n_estimators=350, max_depth=4, learning_rate=0.05, subsample=0.85, colsample_bytree=0.85, objective="multi:softprob", num_class=3, eval_metric="mlogloss", tree_method="hist", random_state=79)),
|
||||
])
|
||||
model.fit(feat.values.astype(np.float32), y.values)
|
||||
preds = model.predict(feat.values.astype(np.float32))
|
||||
return {"model": model, "features": list(feat.columns)}, {"training_bars": int(len(feat)), "buy_signals": int((preds == 2).sum()), "sell_signals": int((preds == 0).sum()), "hold_signals": int((preds == 1).sum())}
|
||||
|
||||
|
||||
def predict(model, market_data, config):
|
||||
p = config.get("parameters", {})
|
||||
candles = market_data.get("candles", [])
|
||||
if len(candles) < int(p.get("lookback", 220)):
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
|
||||
df = _normalise(pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"]))
|
||||
row = _features(df).tail(1)
|
||||
if row.empty:
|
||||
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "no_features"}}
|
||||
row = row[model["features"]]
|
||||
prob = model["model"].predict_proba(row.values.astype(np.float32))[0]
|
||||
p_sell, p_hold, p_buy = map(float, prob)
|
||||
edge = max(p_buy, p_sell) - p_hold
|
||||
min_prob = float(p.get("min_direction_prob", 0.38))
|
||||
min_edge = float(p.get("min_edge", 0.03))
|
||||
if p_buy >= min_prob and edge >= min_edge and p_buy > p_sell:
|
||||
signal, confidence = "UP", p_buy
|
||||
elif p_sell >= min_prob and edge >= min_edge and p_sell > p_buy:
|
||||
signal, confidence = "DOWN", p_sell
|
||||
else:
|
||||
signal, confidence = "HOLD", p_hold
|
||||
return {"signal": signal, "confidence": round(confidence, 4), "metadata": {"p_buy": round(p_buy, 4), "p_sell": round(p_sell, 4), "p_hold": round(p_hold, 4), "edge": round(edge, 4), "hurst_proxy": round(float(row["hurst_proxy"].iloc[0]), 4)}}
|
||||
Reference in New Issue
Block a user