Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALPACA_API_KEY=
ALPACA_API_SECRET=
ENABLE_LIVE_TRADING=false
KILL_SWITCH=false
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.PHONY: test train run backtest

test:
pytest -q

train:
python -m trader train --config configs/config.yaml

run:
python -m trader run --config configs/config.yaml --mode paper

backtest:
python -m trader backtest --config configs/config.yaml
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# AI Prediction Trader

Production-minded AI auto-trading scaffold for BTC-USD, ETH-USD, and SPY with strict safeguards and paper trading by default.

## Quickstart

1. Create env and install:
- `pip install -r requirements.txt`
2. Copy env file:
- `cp .env.example .env`
3. Train models:
- `python -m trader train --config configs/config.yaml`
4. Run paper trading:
- `python -m trader run --config configs/config.yaml --mode paper`
5. Run walk-forward backtest:
- `python -m trader backtest --config configs/config.yaml`

## What training now does

- Performs per-asset walk-forward parameter search over several model candidates.
- Selects the best candidate by walk-forward AUC (with logloss tie-break).
- Calibrates each asset's `p_long` cutoff from walk-forward predictions (balanced accuracy objective).
- Stores tuned settings in `models/<asset>_meta.json` and uses calibrated `p_long` during `run`.

## Backtest output

`python -m trader backtest` writes `backtest_results.csv` with per-split:

- `accuracy`
- `auc`
- `logloss`
- `n_test`

## Scheduling

Use cron/systemd to call the run command once daily after market close.

## Safety

- Live mode requires both `--mode live` and `ENABLE_LIVE_TRADING=true`.
- `KILL_SWITCH=true` disables all order placement.
- Drawdown kill switch disables trading if portfolio drawdown exceeds 20%.
35 changes: 35 additions & 0 deletions configs/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
assets:
- BTC-USD
- ETH-USD
- SPY
thresholds:
BTC-USD: 0.005
ETH-USD: 0.005
SPY: 0.002
p_long:
BTC-USD: 0.60
ETH-USD: 0.60
SPY: 0.58
vol_target:
BTC-USD: 0.010
ETH-USD: 0.010
SPY: 0.006
caps:
per_asset:
BTC-USD: 0.30
ETH-USD: 0.30
SPY: 1.0
total_crypto: 0.40
walkforward:
train_days: 756
test_days: 63
step_days: 63
embargo_days: 1
data:
crypto_source: coinbase
equity_source: alpaca_or_fallback
paper_costs:
fee_bps: 1.0
slippage_bps: 2.0
timezone: America/New_York
include_crypto_for_spy: false
Empty file added logs/.gitkeep
Empty file.
Empty file added models/.gitkeep
Empty file.
12 changes: 12 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "ai-prediction-trader"
version = "0.1.0"
requires-python = ">=3.11"

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
8 changes: 8 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
numpy
pandas
scikit-learn
xgboost
pyyaml
requests
python-dotenv
pytest
6 changes: 6 additions & 0 deletions tests/test_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from trader.data.adapters import _to_coinbase_product


def test_coinbase_product_format() -> None:
assert _to_coinbase_product("BTCUSD") == "BTC-USD"
assert _to_coinbase_product("BTC-USD") == "BTC-USD"
12 changes: 12 additions & 0 deletions tests/test_features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pandas as pd

from trader.data.synthetic import generate_synthetic_ohlcv
from trader.features.compute import compute_features


def test_feature_generation_has_expected_columns() -> None:
df = generate_synthetic_ohlcv("BTC-USD", periods=300)
feats = compute_features(df)
expected = {"r1", "r5", "sma20_dist", "slope20", "atr_pct", "distance_from_high_20"}
assert expected.issubset(set(feats.columns))
assert feats.dropna().shape[0] > 0
41 changes: 41 additions & 0 deletions tests/test_regime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import numpy as np
import pandas as pd

from trader.features.compute import compute_features
from trader.risk.regime import regime_is_bad, regime_status


def test_regime_flag_returns_bool() -> None:
idx = pd.date_range("2020-01-01", periods=300, freq="D")
close = pd.Series(np.linspace(100, 130, len(idx)), index=idx)
df = pd.DataFrame(
{
"open": close,
"high": close * 1.01,
"low": close * 0.99,
"close": close,
"volume": 1000.0,
}
)
feats = compute_features(df)
bad = regime_is_bad("SPY", feats)
assert isinstance(bad, bool)


def test_regime_status_exposes_trigger_details() -> None:
idx = pd.date_range("2020-01-01", periods=300, freq="D")
close = pd.Series(np.linspace(100, 60, len(idx)), index=idx)
df = pd.DataFrame(
{
"open": close,
"high": close * 1.01,
"low": close * 0.99,
"close": close,
"volume": 1000.0,
}
)
feats = compute_features(df)
status = regime_status("SPY", feats)
assert status["bad"] is True
assert status["drawdown_breach"] is True
assert status["drawdown"] > status["drawdown_limit"]
14 changes: 14 additions & 0 deletions tests/test_sizing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from trader.risk.caps import apply_crypto_cap
from trader.risk.sizing import confidence_fraction, target_weight


def test_confidence_ladder_and_weight() -> None:
assert confidence_fraction(0.57, 0.58) == 0.0
assert confidence_fraction(0.60, 0.58) == 0.25
w = target_weight(prob=0.75, p_long=0.60, sigma20=0.02, vol_target=0.01, cap=0.30)
assert 0 <= w <= 0.30


def test_crypto_cap_scaling() -> None:
out = apply_crypto_cap({"BTC-USD": 0.3, "ETH-USD": 0.3, "SPY": 0.5}, total_cap=0.4)
assert abs(out["BTC-USD"] + out["ETH-USD"] - 0.4) < 1e-8
13 changes: 13 additions & 0 deletions tests/test_splitter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pandas as pd

from trader.model.splitter import WalkForwardSplitter


def test_walkforward_splitter_embargo() -> None:
idx = pd.date_range("2020-01-01", periods=1000, freq="B")
splitter = WalkForwardSplitter(train_days=200, test_days=50, step_days=50, embargo_days=1)
splits = splitter.split(idx)
assert len(splits) > 0
tr, te = splits[0]
assert te.min() > tr.max()
assert (te.min() - tr.max()).days >= 2
24 changes: 24 additions & 0 deletions tests/test_train.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pandas as pd

from trader.data.synthetic import generate_synthetic_ohlcv
from trader.features.compute import make_dataset
from trader.model.splitter import WalkForwardSplitter
from trader.model.train import _find_best_params, _optimize_cutoff


def test_training_search_and_cutoff_optimization_ranges() -> None:
prices = {
"BTC-USD": generate_synthetic_ohlcv("BTC-USD", periods=420),
"ETH-USD": generate_synthetic_ohlcv("ETH-USD", periods=420),
"SPY": generate_synthetic_ohlcv("SPY", periods=420),
}
X, y = make_dataset("BTC-USD", prices, threshold=0.005)
splitter = WalkForwardSplitter(train_days=220, test_days=50, step_days=50, embargo_days=1)

params, auc, ll = _find_best_params(X, y, splitter)
cutoff = _optimize_cutoff(X, y, splitter, params, default_cutoff=0.60)

assert isinstance(params, dict)
assert 0.0 <= auc <= 1.0
assert ll > 0.0
assert 0.50 <= cutoff <= 0.70
4 changes: 4 additions & 0 deletions trader/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""AI trading package."""

__all__ = ["__version__"]
__version__ = "0.1.0"
4 changes: 4 additions & 0 deletions trader/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from trader.cli import main

if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions trader/backtest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Backtesting utilities."""
33 changes: 33 additions & 0 deletions trader/backtest/walkforward.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from typing import Any

import numpy as np
import pandas as pd
from sklearn.metrics import log_loss, roc_auc_score

from trader.features.compute import make_dataset
from trader.model.splitter import WalkForwardSplitter
from trader.model.train import build_model


def run_walkforward(prices: dict[str, pd.DataFrame], cfg: dict[str, Any]) -> pd.DataFrame:
rows: list[dict[str, Any]] = []
for asset in cfg["assets"]:
X, y = make_dataset(asset, prices, cfg["thresholds"][asset])
splitter = WalkForwardSplitter(**cfg["walkforward"])
for i, (tr, te) in enumerate(splitter.split(X.index)):
y_tr = y.loc[tr]
if y_tr.nunique() < 2:
continue
scale_pos_weight = float((y_tr == 0).sum() / max((y_tr == 1).sum(), 1))
model = build_model({"scale_pos_weight": scale_pos_weight})
model.fit(X.loc[tr], y_tr)
p = model.predict_proba(X.loc[te])[:, 1]
pred = (p >= cfg["p_long"][asset]).astype(int)
y_te = y.loc[te].values
acc = float((pred == y_te).mean())
auc = float(roc_auc_score(y_te, p)) if len(np.unique(y_te)) > 1 else 0.5
ll = float(log_loss(y_te, np.clip(p, 1e-6, 1 - 1e-6)))
rows.append({"asset": asset, "split": i, "accuracy": acc, "auc": auc, "logloss": ll, "n_test": len(te)})
return pd.DataFrame(rows)
Loading