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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

name: ci
on:
push:
branches: [ quant-init ]
pull_request:
branches: [ main ]
jbs: test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install
run: |
python -m venv .venv
. .venv/bin/activate
pip install -r sol_
21 changes: 0 additions & 21 deletions LICENSE

This file was deleted.

1 change: 0 additions & 1 deletion README.md

This file was deleted.

41 changes: 41 additions & 0 deletions sol_quant_pro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# SOL Quant Pro — Hybrid (Rule‑Based + AI) Starter

A modular, production‑oriented Solana (SOL/USDT) quant framework that supports:
- Rule‑based alphas (trend, mean‑reversion, breakout).
- AI meta‑model (scikit‑learn/XGBoost) to ensemble alphas and features.
- Walk‑forward training and evaluation.
- Vectorized backtester with costs, ATR sizing, and drawdown guard.
- Paper‑trading loop via ccxt; Binance futures testnet ready.

Use this to iterate quickly. Ship signals first (Discord/Telegram), then graduate to small live sizing.

## Quickstart
1) Python 3.10+ recommended.
2) Install deps:
```
pip install -r requirements.txt
```
3) Configure in `config.yaml`. Keep `live.trading: false` while testing.
4) Fetch data:
```
python data.py
```
5) Build features + train AI (optional):
```
python features.py
python meta.py
```
6) Backtest:
```
python backtest.py
```
7) Paper‑trade loop (simulated fills):
```
python live.py
```

## Notes
- Default symbol: SOL/USDT (Binance USDM), timeframe: 5m, lookback: 30 days.
- Targets: classification on forward return sign and regression on next‑k return; strategy uses thresholds.
- AI improves when: (a) enough data; (b) walk‑forward splits; (c) regular re‑training.
- Never deploy live without months of out‑of‑sample. Start tiny.
24 changes: 24 additions & 0 deletions sol_quant_pro/alphas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pandas as pd
import numpy as np

# Basic alphas used by the policy/AI layer

def alpha_trend(df):
sig = np.where(df['ema_fast'] > df['ema_slow'], 1, -1)
return pd.Series(sig, index=df.index, name='alpha_trend')

def alpha_meanrev(df):
sig = np.where(df['bb_pos'] < 0.15, 1, np.where(df['bb_pos'] > 0.85, -1, 0))
return pd.Series(sig, index=df.index, name='alpha_meanrev')

def alpha_breakout(df):
win = 48 # ~4h breakout on 5m bars
hi = df['high'].rolling(win).max()
lo = df['low'].rolling(win).min()
sig = np.where(df['close'] > hi.shift(1), 1, np.where(df['close'] < lo.shift(1), -1, 0))
return pd.Series(sig, index=df.index, name='alpha_breakout')

def stack_alphas(df):
A = pd.concat([alpha_trend(df), alpha_meanrev(df), alpha_breakout(df)], axis=1).fillna(0)
A['alpha_ensemble'] = A.mean(axis=1)
return A
54 changes: 54 additions & 0 deletions sol_quant_pro/backtest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import pandas as pd
import yaml
from alphas import stack_alphas
from features import engineer_5m
from utils import resample_to_4h, ema, atr, pivot_levels_from_daily
from policy.bias_gate import BiasInputs, bias_gate
from policy.derivs_filter import DerivsInputs, derivatives_confirmation
from policy.range_engine import RangeInputs, compute_range
from policy.acceptance import AcceptanceInputs, accepted
from policy.targets import point_target
from policy.guards import sanity_failsafe

from risk import position_size

# Minimal backtest that applies policy constraints and optional AI gating (to be added)

def load_cfg():
with open('config.yaml','r') as f: return yaml.safe_load(f)

def load_data(cfg):
sym = cfg['market']['symbol'].replace('/','_'); tf = cfg['market']['timeframe']
return pd.read_csv(f'data/{sym}_{tf}.csv', parse_dates=['timestamp'])

def backtest():
cfg = load_cfg()
raw = load_data(cfg)
fe = engineer_5m(raw)
# Build 4H frame for policy inputs
h4 = resample_to_4h(raw)
h4['ma50'] = ema(h4['close'], 50)
h4['atr14'] = atr(h4['high'], h4['low'], h4['close'], 14)

# Daily pivots from 4H aggregated to daily
dly = raw.set_index('timestamp').resample('1D').agg({'open':'first','high':'max','low':'min','close':'last','volume':'sum'}).dropna().reset_index()
piv = pivot_levels_from_daily(dly)

last4 = h4.iloc[-1]
gate = bias_gate(BiasInputs(last4['close'], piv['P'], last4['ma50']))

# Derivs filter placeholders (user to wire real feeds)
derivs = derivatives_confirmation(DerivsInputs(price=float(last4['close']), pivot=float(piv['P']), oi_24h_change=0.0, funding_rate=0.0))
skew_sign = 1 if derivs == 'Neutral_TacticalBullish' else (-1 if derivs == 'Neutral_TacticalBearish' else 0)

rng, base = compute_range(RangeInputs(midpoint=piv['P'], atr_4h=float(last4['atr14']), regime_broken=False, skew_sign=skew_sign))

favored_half = 'upper' if skew_sign>0 else ('lower' if skew_sign<0 else 'none')
tgt = point_target(piv['P'], favored_half if favored_half!='none' else 'upper', piv['P'], piv['R1'], piv['S1'])
gate_adj, half_adj = sanity_failsafe(gate, favored_half if favored_half!='none' else 'upper')

print('Bias Gate:', gate, 'Derivs:', derivs)
print('Range:', rng, 'Target:', tgt, 'GateAdj:', gate_adj, 'HalfAdj:', half_adj)

if __name__ == '__main__':
backtest()
47 changes: 47 additions & 0 deletions sol_quant_pro/broker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os
import ccxt
import yaml

def load_cfg(path: str = "config.yaml"):
with open(path, "r") as f:
return yaml.safe_load(f)

def make_exchange(cfg):
name = cfg["exchange"]["name"].lower()
testnet = cfg["exchange"]["testnet"]
key = os.getenv("BINANCE_API_KEY","")
sec = os.getenv("BINANCE_SECRET","")
if name == "binance":
ex = ccxt.binanceusdm({"apiKey": key, "secret": sec, "enableRateLimit": True})
ex.set_sandbox_mode(testnet)
return ex
raise ValueError(f"Unsupported exchange: {name}")

class PaperBroker:
def __init__(self, fee=0.0006):
self.equity = None
self.pos = 0
self.entry = 0.0
self.qty = 0.0
self.fee = fee

def reset(self, equity):
self.equity = equity
self.pos = 0
self.entry = 0.0
self.qty = 0.0

def enter(self, side, price, qty):
self.pos = 1 if side.upper()=="LONG" else -1
self.entry = price
self.qty = qty

def exit(self, price):
if self.pos == 0: return 0.0
pnl = (price - self.entry) * self.qty * self.pos
fees = self.fee * abs(self.qty) * (self.entry + price)
self.equity += pnl - fees
self.pos = 0
self.entry = 0.0
self.qty = 0.0
return pnl - fees
41 changes: 41 additions & 0 deletions sol_quant_pro/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
exchange:
name: binance
testnet: true

market:
symbol: "SOL/USDT"
timeframe: "5m"
since_days: 30

fees:
taker: 0.0006
maker: 0.0002

slippage:
ticks: 1

risk:
account_equity: 10000.0
risk_per_trade: 0.01
max_daily_loss_pct: 0.03
kelly_cap: 0.25

strategy:
use_ai: true
ai_model: "xgboost"
horizon_bars: 6
cls_threshold: 0.55
reg_threshold: 0.0008
ema_fast: 50
ema_slow: 200
rsi_period: 14
bb_window: 20
bb_std: 2.0
atr_period: 14
atr_mult_stop: 2.0
atr_mult_tp: 2.5

live:
trading: false
poll_seconds: 15
max_candles: 1000
37 changes: 37 additions & 0 deletions sol_quant_pro/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os, time
import pandas as pd
from datetime import datetime, timedelta, timezone
from broker import load_cfg, make_exchange

def ms_since(days: int) -> int:
dt = datetime.now(timezone.utc) - timedelta(days=days)
return int(dt.timestamp() * 1000)

def fetch_ohlcv():
cfg = load_cfg()
ex = make_exchange(cfg)
sym = cfg['market']['symbol']
tf = cfg['market']['timeframe']
since = ms_since(cfg['market']['since_days'])
limit = 1500
all_rows = []
ptr = since
while True:
batch = ex.fetch_ohlcv(sym, timeframe=tf, since=ptr, limit=limit)
if not batch:
break
all_rows += batch
ptr = batch[-1][0] + 1
if len(batch) < limit:
break
time.sleep(ex.rateLimit/1000)
df = pd.DataFrame(all_rows, columns=['timestamp','open','high','low','close','volume'])
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)
os.makedirs('data', exist_ok=True)
out = 'data/' + sym.replace('/', '_') + '_' + tf + '.csv'
df.to_csv(out, index=False)
print('Wrote ' + out + ' rows=' + str(len(df)))
return out

if __name__ == '__main__':
fetch_ohlcv()
19 changes: 19 additions & 0 deletions sol_quant_pro/data_sources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import time
import math
import requests
import pandas as pd
from datetime import datetime, timedelta, timezone
from typing import Optional, Tuple
import ccxt

BINANCE_FAPI = 'https://fapi.binance.com'

def _usdm_symbol(symbol: str) => str:
# 'SOL/USDT' -> 'SOLUSDT'
return symbol.replace('/', '')

def fetch_binance_funding(symbol: str, timeout: float = 5.0) => Optional[float]:
try:
s = _usdm_symbol(symbol)
url = f({BINANCE_FAPI})/fapi/v1/fundingRate"\n r = requests.get(url, params={'symbol': s
59 changes: 59 additions & 0 deletions sol_quant_pro/features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import pandas as pd
import numpy as np
from ta.momentum import RSIIndicator, StochasticOscillator
from ta.trend import EMAIndicator, MACD
from ta.volatility import AverageTrueRange, BollingerBands

# Core feature engineering for 5m data.
# Expects columns: timestamp, open, high, low, close, volume (timestamp tz-aware UTC).

def engineer_5m(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df['ret1'] = df['close'].pct_change()
df['ret5'] = df['close'].pct_change(5)
df['ret20'] = df['close'].pct_change(20)

df['roll_std_20'] = df['ret1'].rolling(20).std()
df['roll_std_100'] = df['ret1'].rolling(100).std()

ema50 = EMAIndicator(close=df['close'], window=50).ema_indicator()
ema200 = EMAIndicator(close=df['close'], window=200).ema_indicator()
df['ema_fast'] = ema50
df['ema_slow'] = ema200
df['ema_diff'] = (ema50 - ema200) / df['close']

macd = MACD(close=df['close'])
df['macd'] = macd.macd()
df['macd_sig'] = macd.macd_signal()
df['macd_diff'] = macd.macd_diff()

rsi = RSIIndicator(close=df['close'], window=14).rsi()
df['rsi'] = rsi

stoch = StochasticOscillator(high=df['high'], low=df['low'], close=df['close'])
df['stoch_k'] = stoch.stoch()
df['stoch_d'] = stoch.stoch_signal()

bb = BollingerBands(close=df['close'], window=20, window_dev=2.0)
df['bb_high'] = bb.bollinger_hband()
df['bb_low'] = bb.bollinger_lband()
df['bb_pos'] = (df['close'] - df['bb_low']) / (df['bb_high'] - df['bb_low'] + 1e-9)

atr = AverageTrueRange(high=df['high'], low=df['low'], close=df['close'], window=14).average_true_range()
df['atr'] = atr
df['atr_norm'] = atr / df['close']

df['hl_range'] = (df['high'] - df['low']) / df['close']
df['close_pos'] = (df['close'] - df['low']) / (df['high'] - df['low'] + 1e-9)
return df

# Utility to compute previous UTC day H/L/C and classic pivots.

def prior_day_pivots(df_d: pd.DataFrame):
# Expects daily candles with columns high, low, close and timestamp.
last = df_d.iloc[-1]
H, L, C = float(last['high']), float(last['low']), float(last['close'])
P = (H + L + C) / 3.0
S1 = 2*P - H
R1 = 2*P - L
return {'H': H, 'L': L, 'C': C, 'P': P, 'S1': S1, 'R1': R1}
Loading