feat: add PyP quant strategy templates

This commit is contained in:
root
2026-04-06 10:59:38 +00:00
commit 6976a0a528
42 changed files with 1017 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
__pycache__/
*.py[cod]
.venv/
venv/
.env
.DS_Store
*.joblib
*.pkl
*.onnx
*.csv
*.parquet
out/
runs/
artifacts/
+9
View File
@@ -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.
+21
View File
@@ -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.
+62
View File
@@ -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.
+26
View File
@@ -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.
+6
View File
@@ -0,0 +1,6 @@
numpy
pandas
scikit-learn
xgboost
lightgbm
statsmodels
+3
View File
@@ -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"]}
+53
View File
@@ -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"}}
+7
View File
@@ -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"]
}
+81
View File
@@ -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)}}
+3
View File
@@ -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"]}
+120
View File
@@ -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",
},
}
+3
View File
@@ -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"]}
+45
View File
@@ -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}}
+5
View File
@@ -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"]
}
+41
View File
@@ -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)}}