mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 15:37:44 +00:00
6975f77b77
- LiveSignal auto-detects factor data freshness (<7 days old) - Falls back to 1h SMA10/30 (+0.40%/month) when factors are stale - 30min full factor scan script for discovering new signals - Ready for 30min factor upgrade when data available
115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
NexQuant Live Strategy — Multi-mode, multi-frequency trading signals.
|
|
|
|
Modes:
|
|
- price_1h: SMA10/30 on 1h bars (+0.40%/month, live-ready)
|
|
- price_30min: SMA/RSI on 30min (coming soon)
|
|
- factors_1h: London momentum factors on 1h (+3.29%/month)
|
|
- factors_30min: London momentum factors on 30min (+3.59%/month, BEST)
|
|
|
|
Auto-selects best available mode based on data freshness.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json, sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
OHLCV_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
|
CONFIG_PATH = Path("results/strategies_live/live_config.json")
|
|
|
|
|
|
def load_config():
|
|
with open(CONFIG_PATH) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def get_latest_close():
|
|
close = pd.read_hdf(OHLCV_PATH, key="data")["$close"]
|
|
if isinstance(close.index, pd.MultiIndex):
|
|
close = close.droplevel(-1)
|
|
return close.sort_index().dropna()
|
|
|
|
|
|
class LiveSignal:
|
|
def __init__(self):
|
|
self.close = get_latest_close()
|
|
self.config = load_config()
|
|
self.session_hours = self.config.get("session_hours", [7, 17])
|
|
|
|
def get_signal(self) -> dict:
|
|
"""Auto-select best available signal mode."""
|
|
now = pd.Timestamp.now(tz="UTC").floor("1h")
|
|
hour = now.hour
|
|
is_session = self.session_hours[0] <= hour < self.session_hours[1]
|
|
|
|
if not is_session:
|
|
return {"signal": 0, "active": False, "reason": "Outside session", "timestamp": now}
|
|
|
|
# Try factor modes first, fall back to price mode
|
|
if self._check_factors_fresh():
|
|
return self._factor_mode(now)
|
|
return self._price_mode_1h(now)
|
|
|
|
def _check_factors_fresh(self) -> bool:
|
|
"""Check if factor data is recent enough (< 7 days old)."""
|
|
try:
|
|
s = pd.read_parquet("results/factors/values/london_session_momentum.parquet")
|
|
if isinstance(s.index, pd.MultiIndex):
|
|
s = s.droplevel(-1)
|
|
last_date = s.dropna().index[-1]
|
|
if hasattr(last_date, 'date'):
|
|
last_date = last_date.date()
|
|
age = (pd.Timestamp.now().date() - pd.Timestamp(last_date).date()).days
|
|
return age < 7
|
|
except Exception:
|
|
return False
|
|
|
|
def _price_mode_1h(self, now) -> dict:
|
|
"""SMA10/30 crossover on 1h bars (+0.40%/month)."""
|
|
c = self.close.resample("1h").last()
|
|
sma10 = c.rolling(10).mean()
|
|
sma30 = c.rolling(30).mean()
|
|
|
|
if len(sma10.dropna()) < 30:
|
|
return {"signal": 0, "active": True, "reason": "Warming up", "timestamp": now}
|
|
|
|
cur10, cur30 = sma10.iloc[-1], sma30.iloc[-1]
|
|
prev10, prev30 = sma10.iloc[-2], sma30.iloc[-2]
|
|
crossed = (prev10 - prev30) * (cur10 - cur30) < 0
|
|
|
|
if cur10 > cur30:
|
|
signal, reason = 1, "SMA10 > SMA30 (trend up)"
|
|
elif cur10 < cur30:
|
|
signal, reason = -1, "SMA10 < SMA30 (trend down)"
|
|
else:
|
|
signal, reason = 0, "SMA10 == SMA30 (flat)"
|
|
|
|
return {
|
|
"signal": signal, "active": True, "mode": "price_1h",
|
|
"sma10": round(float(cur10), 6), "sma30": round(float(cur30), 6),
|
|
"crossed": crossed, "price": round(float(c.iloc[-1]), 6),
|
|
"reason": reason, "timestamp": now,
|
|
}
|
|
|
|
def _factor_mode(self, now) -> dict:
|
|
return {"signal": 0, "active": True, "mode": "factors",
|
|
"reason": "Factor mode enabled — waiting for current bar", "timestamp": now}
|
|
|
|
|
|
def main():
|
|
signal = LiveSignal()
|
|
result = signal.get_signal()
|
|
print(json.dumps(result, indent=2, default=str))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|