From 816bf7c6a0572d55fd6091eb681f1d02483590ab Mon Sep 17 00:00:00 2001 From: lotbones1-code Date: Sun, 19 Oct 2025 21:27:57 -0600 Subject: [PATCH 1/5] =?UTF-8?q?Add=20SOL=20Quant=20Pro=20=E2=80=94=20base?= =?UTF-8?q?=20files=20(README)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 --------------------- README.md | 1 - sol_quant_pro/README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 22 deletions(-) delete mode 100644 LICENSE delete mode 100644 README.md create mode 100644 sol_quant_pro/README.md diff --git a/LICENSE b/LICENSE deleted file mode 100644 index fcd8206..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 lotbones1-code - -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. diff --git a/README.md b/README.md deleted file mode 100644 index 3f62d57..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# fun \ No newline at end of file diff --git a/sol_quant_pro/README.md b/sol_quant_pro/README.md new file mode 100644 index 0000000..b09e2f2 --- /dev/null +++ b/sol_quant_pro/README.md @@ -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. From 5b77571792fca276e062162b3aefc34de68f226f Mon Sep 17 00:00:00 2001 From: lotbones1-code Date: Sun, 19 Oct 2025 21:28:42 -0600 Subject: [PATCH 2/5] Add config and requirements --- sol_quant_pro/config.yaml | 41 ++++++++++++++++++++++++++++++++++ sol_quant_pro/requirements.txt | 8 +++++++ 2 files changed, 49 insertions(+) create mode 100644 sol_quant_pro/config.yaml create mode 100644 sol_quant_pro/requirements.txt diff --git a/sol_quant_pro/config.yaml b/sol_quant_pro/config.yaml new file mode 100644 index 0000000..3ba4741 --- /dev/null +++ b/sol_quant_pro/config.yaml @@ -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 diff --git a/sol_quant_pro/requirements.txt b/sol_quant_pro/requirements.txt new file mode 100644 index 0000000..0a6cc3d --- /dev/null +++ b/sol_quant_pro/requirements.txt @@ -0,0 +1,8 @@ +ccxt==4.3.78 +pandas==2.2.2 +numpy==1.26.4 +ta==0.11.0 +pyyaml==6.0.2 +scikit-learn==1.4.2 +xgboost==2.0.3 +joblib==1.4.2 From 551955f451144d82f2d7e8f78a5febbd9338a052 Mon Sep 17 00:00:00 2001 From: lotbones1-code Date: Sun, 19 Oct 2025 21:29:25 -0600 Subject: [PATCH 3/5] Add broker module --- sol_quant_pro/broker.py | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 sol_quant_pro/broker.py diff --git a/sol_quant_pro/broker.py b/sol_quant_pro/broker.py new file mode 100644 index 0000000..b60f2ad --- /dev/null +++ b/sol_quant_pro/broker.py @@ -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 From 972e1636da1bf087594ceeef9e80b0fba76a3812 Mon Sep 17 00:00:00 2001 From: lotbones1-code Date: Sun, 19 Oct 2025 21:34:46 -0600 Subject: [PATCH 4/5] Add features, utils, policy modules, reporter, data fetcher, backtest, live, risk, and tests --- sol_quant_pro/alphas.py | 24 +++++++++++ sol_quant_pro/backtest.py | 54 ++++++++++++++++++++++++ sol_quant_pro/data.py | 37 +++++++++++++++++ sol_quant_pro/features.py | 59 +++++++++++++++++++++++++++ sol_quant_pro/live.py | 50 +++++++++++++++++++++++ sol_quant_pro/meta.py | 41 +++++++++++++++++++ sol_quant_pro/policy/acceptance.py | 10 +++++ sol_quant_pro/policy/bias_gate.py | 14 +++++++ sol_quant_pro/policy/btc_coupling.py | 4 ++ sol_quant_pro/policy/derivs_filter.py | 15 +++++++ sol_quant_pro/policy/guards.py | 12 ++++++ sol_quant_pro/policy/range_engine.py | 17 ++++++++ sol_quant_pro/policy/targets.py | 8 ++++ sol_quant_pro/policy/volatility.py | 5 +++ sol_quant_pro/reporter_absolute.py | 18 ++++++++ sol_quant_pro/risk.py | 5 +++ sol_quant_pro/tests/test_policy.py | 23 +++++++++++ sol_quant_pro/utils.py | 29 +++++++++++++ 18 files changed, 425 insertions(+) create mode 100644 sol_quant_pro/alphas.py create mode 100644 sol_quant_pro/backtest.py create mode 100644 sol_quant_pro/data.py create mode 100644 sol_quant_pro/features.py create mode 100644 sol_quant_pro/live.py create mode 100644 sol_quant_pro/meta.py create mode 100644 sol_quant_pro/policy/acceptance.py create mode 100644 sol_quant_pro/policy/bias_gate.py create mode 100644 sol_quant_pro/policy/btc_coupling.py create mode 100644 sol_quant_pro/policy/derivs_filter.py create mode 100644 sol_quant_pro/policy/guards.py create mode 100644 sol_quant_pro/policy/range_engine.py create mode 100644 sol_quant_pro/policy/targets.py create mode 100644 sol_quant_pro/policy/volatility.py create mode 100644 sol_quant_pro/reporter_absolute.py create mode 100644 sol_quant_pro/risk.py create mode 100644 sol_quant_pro/tests/test_policy.py create mode 100644 sol_quant_pro/utils.py diff --git a/sol_quant_pro/alphas.py b/sol_quant_pro/alphas.py new file mode 100644 index 0000000..a7041cd --- /dev/null +++ b/sol_quant_pro/alphas.py @@ -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 diff --git a/sol_quant_pro/backtest.py b/sol_quant_pro/backtest.py new file mode 100644 index 0000000..4f6e526 --- /dev/null +++ b/sol_quant_pro/backtest.py @@ -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() diff --git a/sol_quant_pro/data.py b/sol_quant_pro/data.py new file mode 100644 index 0000000..8add672 --- /dev/null +++ b/sol_quant_pro/data.py @@ -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() diff --git a/sol_quant_pro/features.py b/sol_quant_pro/features.py new file mode 100644 index 0000000..4e91541 --- /dev/null +++ b/sol_quant_pro/features.py @@ -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} diff --git a/sol_quant_pro/live.py b/sol_quant_pro/live.py new file mode 100644 index 0000000..0139bb5 --- /dev/null +++ b/sol_quant_pro/live.py @@ -0,0 +1,50 @@ +import time +import pandas as pd +from broker import load_cfg, make_exchange, PaperBroker +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.targets import point_target +from policy.guards import sanity_failsafe + +from risk import position_size + +def last_n(ex, symbol, timeframe, n): + ohlcv = ex.fetch_ohlcv(symbol, timeframe=timeframe, limit=n) + df = pd.DataFrame(ohlcv, columns=['timestamp','open','high','low','close','volume']) + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True) + return df + +def main(): + cfg = load_cfg() + ex = make_exchange(cfg) + sym = cfg['market']['symbol']; tf = cfg['market']['timeframe'] + fee = cfg['fees']['taker'] + pb = PaperBroker(fee=fee); pb.reset(cfg['risk']['account_equity']) + + while True: + try: + raw = last_n(ex, sym, tf, n=min(1000, cfg['live']['max_candles'])) + fe = engineer_5m(raw) + h4 = resample_to_4h(raw) + h4['ma50'] = ema(h4['close'], 50) + h4['atr14'] = atr(h4['high'], h4['low'], h4['close'], 14) + 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(float(last4['close']), float(piv['P']), float(last4['ma50']))) + 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=float(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(float(piv['P']), favored_half if favored_half!='none' else 'upper', float(piv['P']), float(piv['R1']), float(piv['S1'])) + gate_adj, half_adj = sanity_failsafe(gate, favored_half if favored_half!='none' else 'upper') + print(str(last4['timestamp']), 'gate=', gate, 'derivs=', derivs, 'range=', rng, 'target=', tgt, 'pos=', pb.pos, 'eq=', pb.equity) + except Exception as e: + print('Loop error:', repr(e)) + time.sleep(cfg['live']['poll_seconds']) + +if __name__ == '__main__': + main() diff --git a/sol_quant_pro/meta.py b/sol_quant_pro/meta.py new file mode 100644 index 0000000..601b2fe --- /dev/null +++ b/sol_quant_pro/meta.py @@ -0,0 +1,41 @@ +import pandas as pd +import numpy as np +import yaml, joblib, os +from sklearn.model_selection import TimeSeriesSplit +from sklearn.preprocessing import StandardScaler +from sklearn.pipeline import Pipeline +from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from xgboost import XGBClassifier, XGBRegressor + +FEATS = ['ret1','ret5','ret20','roll_std_20','roll_std_100','ema_diff','macd','macd_sig','macd_diff','rsi','stoch_k','stoch_d','bb_pos','atr_norm','hl_range','close_pos'] + +def load_cfg(): + with open('config.yaml','r') as f: return yaml.safe_load(f) + +def train(): + cfg = load_cfg() + df = pd.read_csv('data/features.csv', parse_dates=['timestamp']) + X = df[FEATS].values + y_cls = df['label_up'].values + y_reg = df['fwd_ret'].values + + if cfg['strategy']['ai_model'] == 'xgboost': + cls = XGBClassifier(n_estimators=300, max_depth=4, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_lambda=1.0, n_jobs=-1, tree_method='hist') + reg = XGBRegressor(n_estimators=300, max_depth=4, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_lambda=1.0, n_jobs=-1, tree_method='hist') + else: + cls = RandomForestClassifier(n_estimators=400, max_depth=6, n_jobs=-1) + reg = RandomForestRegressor(n_estimators=400, max_depth=6, n_jobs=-1) + + cls_pipe = Pipeline([('scaler', StandardScaler()), ('model', cls)]) + reg_pipe = Pipeline([('scaler', StandardScaler()), ('model', reg)]) + + cls_pipe.fit(X, y_cls) + reg_pipe.fit(X, y_reg) + + os.makedirs('artifacts', exist_ok=True) + joblib.dump({'pipe': cls_pipe, 'feats': FEATS}, 'artifacts/cls.joblib') + joblib.dump({'pipe': reg_pipe, 'feats': FEATS}, 'artifacts/reg.joblib') + print('Wrote artifacts/cls.joblib and artifacts/reg.joblib') + +if __name__ == '__main__': + train() diff --git a/sol_quant_pro/policy/acceptance.py b/sol_quant_pro/policy/acceptance.py new file mode 100644 index 0000000..bbcbd10 --- /dev/null +++ b/sol_quant_pro/policy/acceptance.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +@dataclass +class AcceptanceInputs: + closes_15m_beyond: int + close_1h_beyond: bool + confirming_delta: bool + +def accepted(inp: AcceptanceInputs) -> bool: + return (inp.close_1h_beyond or inp.closes_15m_beyond >= 2) and inp.confirming_delta diff --git a/sol_quant_pro/policy/bias_gate.py b/sol_quant_pro/policy/bias_gate.py new file mode 100644 index 0000000..2a943c4 --- /dev/null +++ b/sol_quant_pro/policy/bias_gate.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass + +@dataclass +class BiasInputs: + last_4h_close: float + daily_pivot: float + ma_4h_50: float + +def bias_gate(inp: BiasInputs) -> str: + if inp.last_4h_close > inp.daily_pivot and inp.last_4h_close > inp.ma_4h_50: + return 'Bullish' + if inp.last_4h_close < inp.daily_pivot and inp.last_4h_close < inp.ma_4h_50: + return 'Bearish' + return 'Neutral' diff --git a/sol_quant_pro/policy/btc_coupling.py b/sol_quant_pro/policy/btc_coupling.py new file mode 100644 index 0000000..5769fbd --- /dev/null +++ b/sol_quant_pro/policy/btc_coupling.py @@ -0,0 +1,4 @@ +def adjust_by_btc_coupling(prob_bull: float, corr_30d: float, btc_below_pivot_and_ma: bool, atr_widen: float): + if corr_30d >= 0.6 and btc_below_pivot_and_ma: + return max(0.0, prob_bull * 0.8), atr_widen + 0.5 + return prob_bull, atr_widen diff --git a/sol_quant_pro/policy/derivs_filter.py b/sol_quant_pro/policy/derivs_filter.py new file mode 100644 index 0000000..f25f417 --- /dev/null +++ b/sol_quant_pro/policy/derivs_filter.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + +@dataclass +class DerivsInputs: + price: float + pivot: float + oi_24h_change: float + funding_rate: float + +def derivatives_confirmation(inp: DerivsInputs) -> str: + if inp.price < inp.pivot and inp.oi_24h_change <= 0 and inp.funding_rate >= 0: + return 'Neutral_TacticalBearish' + if inp.price > inp.pivot and inp.oi_24h_change >= 0 and inp.funding_rate <= 0: + return 'Neutral_TacticalBullish' + return 'None' diff --git a/sol_quant_pro/policy/guards.py b/sol_quant_pro/policy/guards.py new file mode 100644 index 0000000..da97337 --- /dev/null +++ b/sol_quant_pro/policy/guards.py @@ -0,0 +1,12 @@ +def sanity_failsafe(bias_gate_result: str, computed_target_half: str): + if bias_gate_result == 'Bullish' and computed_target_half == 'lower': + return 'Neutral', 'upper' + if bias_gate_result == 'Bearish' and computed_target_half == 'upper': + return 'Neutral', 'lower' + return bias_gate_result, computed_target_half + +def post_print_guard(exited_by_half_atr: bool, band_edges, atr): + if exited_by_half_atr: + lower, upper = band_edges + return (lower - 0.5*atr, upper + 0.5*atr) + return band_edges diff --git a/sol_quant_pro/policy/range_engine.py b/sol_quant_pro/policy/range_engine.py new file mode 100644 index 0000000..8ee2f92 --- /dev/null +++ b/sol_quant_pro/policy/range_engine.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + +@dataclass +class RangeInputs: + midpoint: float + atr_4h: float + regime_broken: bool = False + skew_sign: int = 0 + +def compute_range(inp: RangeInputs): + base = 1.0 * inp.atr_4h + if inp.regime_broken: + base = max(1.5 * inp.atr_4h, 2.0 * inp.atr_4h) + lower = inp.midpoint - base + upper = inp.midpoint + base + skew = 0.3 * inp.atr_4h * inp.skew_sign + return (lower - skew, upper - skew), base diff --git a/sol_quant_pro/policy/targets.py b/sol_quant_pro/policy/targets.py new file mode 100644 index 0000000..e369d66 --- /dev/null +++ b/sol_quant_pro/policy/targets.py @@ -0,0 +1,8 @@ +def point_target(midpoint: float, favored_half: str, pivot: float, r1: float, s1: float) -> float: + if favored_half == 'upper': + tgt = (pivot + r1) / 2.0 + elif favored_half == 'lower': + tgt = (s1 + pivot) / 2.0 + else: + tgt = pivot + return round(tgt, 2) diff --git a/sol_quant_pro/policy/volatility.py b/sol_quant_pro/policy/volatility.py new file mode 100644 index 0000000..ecb708e --- /dev/null +++ b/sol_quant_pro/policy/volatility.py @@ -0,0 +1,5 @@ +def volatility_discipline(bb_expanding: bool, atr_4h_ddelta_pct: float, base_half_range_atr: float) -> float: + widen = 0.0 + if bb_expanding or atr_4h_ddelta_pct >= 0.20: + widen += 0.25 + return base_half_range_atr * (1.0 + widen) diff --git a/sol_quant_pro/reporter_absolute.py b/sol_quant_pro/reporter_absolute.py new file mode 100644 index 0000000..d5679d5 --- /dev/null +++ b/sol_quant_pro/reporter_absolute.py @@ -0,0 +1,18 @@ +def render_absolute(snapshot: dict) -> str: + lines = [] + lines.append('Data Snapshot & Context (UTC)') + for k, v in snapshot.get('context', {}).items(): + lines.append(f'- {k}: {v}') + lines.append('Key Price Levels (Calculated)') + for k, v in snapshot.get('levels', {}).items(): + lines.append(f'- {k}: {v}') + lines.append('Synthesis & Bias Determination') + lines.append(f'- Bias: {snapshot.get('bias')}') + lines.append('24-Hour Prediction') + lines.append(f'- Range: {snapshot.get('range')}') + lines.append(f'- Target: {snapshot.get('target')}') + lines.append('Actionable Setups & Risk Management') + lines.append(f'- Setup: {snapshot.get('setup', 'No high-probability setup.')}') + lines.append(f'- Invalidation: {snapshot.get('invalidation', 'N/A')}') + lines.append(f'- Auto-Flip: {snapshot.get('auto_flip', 'N/A')}') + return '\n'.join(lines) diff --git a/sol_quant_pro/risk.py b/sol_quant_pro/risk.py new file mode 100644 index 0000000..861b2e2 --- /dev/null +++ b/sol_quant_pro/risk.py @@ -0,0 +1,5 @@ +def position_size(account_equity: float, risk_per_trade: float, entry: float, stop: float): + risk_amt = account_equity * risk_per_trade + per_unit = abs(entry - stop) + if per_unit <= 0: return 0.0 + return max(0.0, risk_amt / per_unit) diff --git a/sol_quant_pro/tests/test_policy.py b/sol_quant_pro/tests/test_policy.py new file mode 100644 index 0000000..a1d9c6a --- /dev/null +++ b/sol_quant_pro/tests/test_policy.py @@ -0,0 +1,23 @@ +import unittest +from policy.bias_gate import BiasInputs, bias_gate +from policy.derivs_filter import DerivsInputs, derivatives_confirmation +from policy.acceptance import AcceptanceInputs, accepted + +class TestPolicy(unittest.TestCase): + def test_bias_gate(self): + self.assertEqual(bias_gate(BiasInputs(201, 200, 199)), 'Bullish') + self.assertEqual(bias_gate(BiasInputs(199, 200, 201)), 'Bearish') + self.assertEqual(bias_gate(BiasInputs(200, 200, 200)), 'Neutral') + + def test_derivs(self): + self.assertEqual(derivatives_confirmation(DerivsInputs(190, 200, -0.01, 0.001)), 'Neutral_TacticalBearish') + self.assertEqual(derivatives_confirmation(DerivsInputs(210, 200, 0.02, -0.001)), 'Neutral_TacticalBullish') + self.assertEqual(derivatives_confirmation(DerivsInputs(210, 200, 0.0, 0.0)), 'None') + + def test_acceptance(self): + self.assertTrue(accepted(AcceptanceInputs(2, False, True))) + self.assertTrue(accepted(AcceptanceInputs(0, True, True))) + self.assertFalse(accepted(AcceptanceInputs(2, False, False))) + +if __name__ == '__main__': + unittest.main() diff --git a/sol_quant_pro/utils.py b/sol_quant_pro/utils.py new file mode 100644 index 0000000..9e77864 --- /dev/null +++ b/sol_quant_pro/utils.py @@ -0,0 +1,29 @@ +import pandas as pd +import numpy as np + +def resample_to_4h(df5: pd.DataFrame) -> pd.DataFrame: + # 5m -> 4H OHLCV + df = df5.set_index('timestamp').sort_index() + o = df['open'].resample('4H').first() + h = df['high'].resample('4H').max() + l = df['low'].resample('4H').min() + c = df['close'].resample('4H').last() + v = df['volume'].resample('4H').sum() + out = pd.DataFrame({'open':o,'high':h,'low':l,'close':c,'volume':v}) + out = out.dropna().reset_index() + return out + +def ema(series: pd.Series, n: int) -> pd.Series: + return series.ewm(span=n, adjust=False).mean() + +def atr(high: pd.Series, low: pd.Series, close: pd.Series, n: int=14) -> pd.Series: + prev_close = close.shift(1) + tr = pd.concat([(high-low), (high-prev_close).abs(), (low-prev_close).abs()], axis=1).max(axis=1) + return tr.rolling(n).mean() + +def pivot_levels_from_daily(df_daily: pd.DataFrame): + last = df_daily.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} + From 1d76888da2b7c9d0ad72f9ccb9da565341381a9a Mon Sep 17 00:00:00 2001 From: lotbones1-code Date: Sun, 19 Oct 2025 21:38:53 -0600 Subject: [PATCH 5/5] =?UTF-8?q?Add=20Binance=20derivatives=20adapters=20(f?= =?UTF-8?q?unding,=20OI=2024h=20change,=20LSR)=20+=20BTC=E2=80=93SOL=2030d?= =?UTF-8?q?=20correlation=20util;=20CI=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 19 +++++++++++++++++++ sol_quant_pro/data_sources.py | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 sol_quant_pro/data_sources.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..48ba040 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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_ \ No newline at end of file diff --git a/sol_quant_pro/data_sources.py b/sol_quant_pro/data_sources.py new file mode 100644 index 0000000..15978e5 --- /dev/null +++ b/sol_quant_pro/data_sources.py @@ -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 \ No newline at end of file