Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- app.py +94 -0
- core/__init__.py +6 -0
- core/__pycache__/__init__.cpython-311.pyc +0 -0
- core/__pycache__/config.cpython-311.pyc +0 -0
- core/__pycache__/features.cpython-311.pyc +0 -0
- core/__pycache__/groww.cpython-311.pyc +0 -0
- core/__pycache__/models.cpython-311.pyc +0 -0
- core/__pycache__/taxes.cpython-311.pyc +0 -0
- core/config.py +37 -0
- core/features.py +202 -0
- core/groww.py +112 -0
- core/models.py +117 -0
- core/taxes.py +50 -0
- data/live_trades.json +45 -0
- data/minute_ohlcv/ADANIENT_minute.parquet +3 -0
- data/minute_ohlcv/ADANIPORTS_minute.parquet +3 -0
- data/minute_ohlcv/APOLLOHOSP_minute.parquet +3 -0
- data/minute_ohlcv/ASIANPAINT_minute.parquet +3 -0
- data/minute_ohlcv/AXISBANK_minute.parquet +3 -0
- data/minute_ohlcv/BAJAJ-AUTO_minute.parquet +3 -0
- data/minute_ohlcv/BAJAJFINSV_minute.parquet +3 -0
- data/minute_ohlcv/BAJFINANCE_minute.parquet +3 -0
- data/minute_ohlcv/BHARTIARTL_minute.parquet +3 -0
- data/minute_ohlcv/BPCL_minute.parquet +3 -0
- data/minute_ohlcv/BRITANNIA_minute.parquet +3 -0
- data/minute_ohlcv/CIPLA_minute.parquet +3 -0
- data/minute_ohlcv/COALINDIA_minute.parquet +3 -0
- data/minute_ohlcv/DIVISLAB_minute.parquet +3 -0
- data/minute_ohlcv/DRREDDY_minute.parquet +3 -0
- data/minute_ohlcv/EICHERMOT_minute.parquet +3 -0
- data/minute_ohlcv/GRASIM_minute.parquet +3 -0
- data/minute_ohlcv/HCLTECH_minute.parquet +3 -0
- data/minute_ohlcv/HDFCBANK_minute.parquet +3 -0
- data/minute_ohlcv/HDFCLIFE_minute.parquet +3 -0
- data/minute_ohlcv/HEROMOTOCO_minute.parquet +3 -0
- data/minute_ohlcv/HINDALCO_minute.parquet +3 -0
- data/minute_ohlcv/HINDUNILVR_minute.parquet +3 -0
- data/minute_ohlcv/ICICIBANK_minute.parquet +3 -0
- data/minute_ohlcv/INDUSINDBK_minute.parquet +3 -0
- data/minute_ohlcv/INFY_minute.parquet +3 -0
- data/minute_ohlcv/ITC_minute.parquet +3 -0
- data/minute_ohlcv/JSWSTEEL_minute.parquet +3 -0
- data/minute_ohlcv/KOTAKBANK_minute.parquet +3 -0
- data/minute_ohlcv/LTIM_minute.parquet +3 -0
- data/minute_ohlcv/LT_minute.parquet +3 -0
- data/minute_ohlcv/MARUTI_minute.parquet +3 -0
- data/minute_ohlcv/MM_minute.parquet +3 -0
- data/minute_ohlcv/NESTLEIND_minute.parquet +3 -0
- data/minute_ohlcv/NTPC_minute.parquet +3 -0
- data/minute_ohlcv/ONGC_minute.parquet +3 -0
app.py
CHANGED
|
@@ -6,10 +6,13 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 6 |
from zoneinfo import ZoneInfo
|
| 7 |
from data_updater import update_daily_data, is_trading_day
|
| 8 |
from forecaster_engine import generate_predictions
|
|
|
|
| 9 |
|
| 10 |
IST = ZoneInfo("Asia/Kolkata")
|
| 11 |
MARKET_CLOSE_BUFFER = time(15, 45) # Update runs after 3:45 PM
|
|
|
|
| 12 |
PREDICTIONS_FILE = os.path.join(os.path.dirname(__file__), "predictions.json")
|
|
|
|
| 13 |
|
| 14 |
app = FastAPI(title="HF NIFTY Forecaster Backend")
|
| 15 |
|
|
@@ -33,6 +36,16 @@ def run_update_pipeline():
|
|
| 33 |
except Exception as e:
|
| 34 |
print(f"Pipeline error: {e}")
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
@app.get("/predictions")
|
| 37 |
def get_predictions():
|
| 38 |
if not os.path.exists(PREDICTIONS_FILE):
|
|
@@ -62,6 +75,87 @@ def cron_trigger(background_tasks: BackgroundTasks):
|
|
| 62 |
|
| 63 |
return {"status": "triggered", "message": "Update and forecast pipeline started in the background."}
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
@app.get("/health")
|
| 66 |
def health_check():
|
| 67 |
return {"status": "alive", "server_time_ist": datetime.now(IST).isoformat()}
|
|
|
|
| 6 |
from zoneinfo import ZoneInfo
|
| 7 |
from data_updater import update_daily_data, is_trading_day
|
| 8 |
from forecaster_engine import generate_predictions
|
| 9 |
+
from signal_generator import generate_signals
|
| 10 |
|
| 11 |
IST = ZoneInfo("Asia/Kolkata")
|
| 12 |
MARKET_CLOSE_BUFFER = time(15, 45) # Update runs after 3:45 PM
|
| 13 |
+
SIGNAL_TIME = time(9, 30) # Signal generation at 9:30 AM
|
| 14 |
PREDICTIONS_FILE = os.path.join(os.path.dirname(__file__), "predictions.json")
|
| 15 |
+
SIGNALS_FILE = os.path.join(os.path.dirname(__file__), "signals.json")
|
| 16 |
|
| 17 |
app = FastAPI(title="HF NIFTY Forecaster Backend")
|
| 18 |
|
|
|
|
| 36 |
except Exception as e:
|
| 37 |
print(f"Pipeline error: {e}")
|
| 38 |
|
| 39 |
+
def run_signal_pipeline():
|
| 40 |
+
"""Run the 5-ticker signal generator."""
|
| 41 |
+
try:
|
| 42 |
+
result = generate_signals()
|
| 43 |
+
print(f"Signal generation result: {result.get('primary_signal', {}).get('action', 'UNKNOWN')}")
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"Signal pipeline error: {e}")
|
| 46 |
+
|
| 47 |
+
# ── Existing Endpoints ───────────────────────────────────────────────────────
|
| 48 |
+
|
| 49 |
@app.get("/predictions")
|
| 50 |
def get_predictions():
|
| 51 |
if not os.path.exists(PREDICTIONS_FILE):
|
|
|
|
| 75 |
|
| 76 |
return {"status": "triggered", "message": "Update and forecast pipeline started in the background."}
|
| 77 |
|
| 78 |
+
# ── NEW: Signal Generator Endpoints ──────────────────────────────────────────
|
| 79 |
+
|
| 80 |
+
@app.get("/signals")
|
| 81 |
+
def get_signals():
|
| 82 |
+
"""Get the latest generated trading signals for the 5-ticker system."""
|
| 83 |
+
if not os.path.exists(SIGNALS_FILE):
|
| 84 |
+
raise HTTPException(status_code=404, detail="Signals not yet generated. Trigger /cron/signal first.")
|
| 85 |
+
|
| 86 |
+
with open(SIGNALS_FILE, "r") as f:
|
| 87 |
+
data = json.load(f)
|
| 88 |
+
|
| 89 |
+
return data
|
| 90 |
+
|
| 91 |
+
@app.post("/cron/signal")
|
| 92 |
+
def signal_trigger(background_tasks: BackgroundTasks):
|
| 93 |
+
"""
|
| 94 |
+
Trigger signal generation at 9:30 AM IST.
|
| 95 |
+
Trains models, fetches live candles, generates BUY/SELL signals.
|
| 96 |
+
"""
|
| 97 |
+
now = datetime.now(IST)
|
| 98 |
+
today = now.date()
|
| 99 |
+
|
| 100 |
+
# Check if it's a trading day
|
| 101 |
+
if not is_trading_day(today):
|
| 102 |
+
return {"status": "skipped", "reason": f"{today} is a holiday or weekend"}
|
| 103 |
+
|
| 104 |
+
# Run signal generation in background
|
| 105 |
+
background_tasks.add_task(run_signal_pipeline)
|
| 106 |
+
|
| 107 |
+
return {
|
| 108 |
+
"status": "triggered",
|
| 109 |
+
"message": "Signal generation pipeline started. Check /signals for results.",
|
| 110 |
+
"trigger_time": now.isoformat(),
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
@app.post("/signals/generate-now")
|
| 114 |
+
def force_signal_generation(background_tasks: BackgroundTasks):
|
| 115 |
+
"""Force signal generation immediately, bypassing time checks."""
|
| 116 |
+
background_tasks.add_task(run_signal_pipeline)
|
| 117 |
+
return {
|
| 118 |
+
"status": "triggered",
|
| 119 |
+
"message": "Signal generation forced. Check /signals for results.",
|
| 120 |
+
"trigger_time": datetime.now(IST).isoformat(),
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
@app.get("/portfolio")
|
| 124 |
+
def get_portfolio():
|
| 125 |
+
"""Get current portfolio status from trade journal."""
|
| 126 |
+
trade_log = os.path.join(os.path.dirname(__file__), "data", "live_trades.json")
|
| 127 |
+
if not os.path.exists(trade_log):
|
| 128 |
+
return {
|
| 129 |
+
"starting_capital": 3692.0,
|
| 130 |
+
"current_capital": 3692.0,
|
| 131 |
+
"total_pnl": 0,
|
| 132 |
+
"trades_count": 0,
|
| 133 |
+
"win_rate": 0,
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
with open(trade_log, "r") as f:
|
| 137 |
+
data = json.load(f)
|
| 138 |
+
|
| 139 |
+
trades = data.get("trades", [])
|
| 140 |
+
starting_cap = data.get("starting_capital", 3692.0)
|
| 141 |
+
|
| 142 |
+
cap = starting_cap
|
| 143 |
+
for t in trades:
|
| 144 |
+
if "net_pnl" in t and t["net_pnl"] is not None:
|
| 145 |
+
cap += t["net_pnl"]
|
| 146 |
+
|
| 147 |
+
n_closed = len([t for t in trades if t.get("net_pnl") is not None])
|
| 148 |
+
n_wins = len([t for t in trades if (t.get("net_pnl") or 0) > 0])
|
| 149 |
+
|
| 150 |
+
return {
|
| 151 |
+
"starting_capital": starting_cap,
|
| 152 |
+
"current_capital": round(cap, 2),
|
| 153 |
+
"total_pnl": round(cap - starting_cap, 2),
|
| 154 |
+
"trades_count": n_closed,
|
| 155 |
+
"win_rate": round(n_wins / n_closed * 100, 1) if n_closed > 0 else 0,
|
| 156 |
+
"last_updated": data.get("last_updated"),
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
@app.get("/health")
|
| 160 |
def health_check():
|
| 161 |
return {"status": "alive", "server_time_ist": datetime.now(IST).isoformat()}
|
core/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/__init__.py
|
| 2 |
+
from core.config import *
|
| 3 |
+
from core.taxes import calculate_taxes_and_slippage
|
| 4 |
+
from core.features import extract_semantic_features, extract_sequential_features
|
| 5 |
+
from core.models import build_pipeline_map, train_models
|
| 6 |
+
from core.groww import fetch_groww_candles
|
core/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (571 Bytes). View file
|
|
|
core/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (1.09 kB). View file
|
|
|
core/__pycache__/features.cpython-311.pyc
ADDED
|
Binary file (13.4 kB). View file
|
|
|
core/__pycache__/groww.cpython-311.pyc
ADDED
|
Binary file (5.66 kB). View file
|
|
|
core/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (4.82 kB). View file
|
|
|
core/__pycache__/taxes.cpython-311.pyc
ADDED
|
Binary file (1.63 kB). View file
|
|
|
core/config.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Central configuration for the intraday trading system.
|
| 3 |
+
All constants, paths, and model architecture definitions live here.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
# ── Paths ──
|
| 9 |
+
PROJECT_DIR = Path(__file__).resolve().parent.parent
|
| 10 |
+
DATA_DIR = PROJECT_DIR / "data" / "minute_ohlcv"
|
| 11 |
+
TRADE_LOG = PROJECT_DIR / "data" / "live_trades.json"
|
| 12 |
+
LOG_FILE = PROJECT_DIR / "logs" / "live_trader.log"
|
| 13 |
+
|
| 14 |
+
# ── Universe ──
|
| 15 |
+
TICKERS = ["INFY", "ASIANPAINT", "TECHM", "POWERGRID", "ONGC"]
|
| 16 |
+
|
| 17 |
+
# ── Capital & Risk ──
|
| 18 |
+
STARTING_CAP = 3692.0
|
| 19 |
+
LEVERAGE = 5.0
|
| 20 |
+
MIN_CONFIDENCE = 0.50
|
| 21 |
+
|
| 22 |
+
# ── Schedule (IST) ──
|
| 23 |
+
SIGNAL_HOUR = 9
|
| 24 |
+
SIGNAL_MINUTE = 31
|
| 25 |
+
|
| 26 |
+
# ── Pipeline mapping ──
|
| 27 |
+
# Defines which feature extractor and model architecture each ticker uses.
|
| 28 |
+
# Format: { ticker: (feature_type, model_key) }
|
| 29 |
+
# feature_type : "semantic" | "sequential"
|
| 30 |
+
# model_key : "ensemble" | "lr_pipeline"
|
| 31 |
+
PIPELINE_MAP = {
|
| 32 |
+
"INFY": ("semantic", "ensemble"),
|
| 33 |
+
"TECHM": ("sequential", "kbest15_lr"),
|
| 34 |
+
"ASIANPAINT": ("sequential", "ensemble"),
|
| 35 |
+
"POWERGRID": ("sequential", "ensemble"),
|
| 36 |
+
"ONGC": ("semantic", "lr_pipeline"),
|
| 37 |
+
}
|
core/features.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Feature extraction from 1-min OHLCV DataFrames.
|
| 3 |
+
|
| 4 |
+
Two pipelines:
|
| 5 |
+
- semantic: Hand-crafted morning indicators (gap, VWAP deviation, momentum, etc.)
|
| 6 |
+
- sequential: Raw normalised return and volume vectors for the 09:15-09:30 window.
|
| 7 |
+
|
| 8 |
+
Both return (X, y, metadata) where:
|
| 9 |
+
X : pd.DataFrame of features, indexed by date
|
| 10 |
+
y : pd.Series of binary labels (1 = price up 09:30->15:10)
|
| 11 |
+
metadata : dict[date] -> { c_0930, h_0930, l_0930, v_0930, c_1510, h_1510, l_1510 }
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
import pandas as pd
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ── Helpers ──────────────────────────────────────────────────────────────────
|
| 19 |
+
|
| 20 |
+
def _safe_fill(arr):
|
| 21 |
+
"""Forward-fill then back-fill NaNs in a 1-D array."""
|
| 22 |
+
return pd.Series(arr).ffill().bfill().values
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ── Semantic features ────────────────────────────────────────────────────────
|
| 26 |
+
|
| 27 |
+
def extract_semantic_features(df):
|
| 28 |
+
"""
|
| 29 |
+
Compute high-level morning indicators from the 09:15-09:30 candle window.
|
| 30 |
+
|
| 31 |
+
Features: gap, return_16m, return_first_5m, return_next_11m, std_dev,
|
| 32 |
+
range_pct, upper_shadow, lower_shadow, total_vol, vwap_dev,
|
| 33 |
+
price_momentum, vol_momentum, morning_trend.
|
| 34 |
+
"""
|
| 35 |
+
df = df.copy()
|
| 36 |
+
df["time"] = df.index.time
|
| 37 |
+
df["date_only"] = df.index.date
|
| 38 |
+
required_times = (
|
| 39 |
+
pd.date_range("09:15", "09:30", freq="min").time.tolist()
|
| 40 |
+
+ [pd.to_datetime("15:10").time()]
|
| 41 |
+
)
|
| 42 |
+
df = df[~df.index.duplicated(keep="first")]
|
| 43 |
+
|
| 44 |
+
daily_close = df.groupby("date_only")["close"].last()
|
| 45 |
+
prev_daily_close = daily_close.shift(1)
|
| 46 |
+
|
| 47 |
+
# Compute morning session dip/peak (09:31 to 12:00) for limit-order entries
|
| 48 |
+
time_0931 = pd.to_datetime("09:31").time()
|
| 49 |
+
time_1200 = pd.to_datetime("12:00").time()
|
| 50 |
+
df_morning = df[(df["time"] >= time_0931) & (df["time"] <= time_1200)]
|
| 51 |
+
morning_low_per_date = df_morning.groupby("date_only")["low"].min()
|
| 52 |
+
morning_high_per_date = df_morning.groupby("date_only")["high"].max()
|
| 53 |
+
|
| 54 |
+
df_filtered = df[df["time"].isin(required_times)].copy()
|
| 55 |
+
pivot_close = df_filtered.pivot(index="date_only", columns="time", values="close")
|
| 56 |
+
pivot_open = df_filtered.pivot(index="date_only", columns="time", values="open")
|
| 57 |
+
pivot_high = df_filtered.pivot(index="date_only", columns="time", values="high")
|
| 58 |
+
pivot_low = df_filtered.pivot(index="date_only", columns="time", values="low")
|
| 59 |
+
pivot_vol = df_filtered.pivot(index="date_only", columns="time", values="volume")
|
| 60 |
+
|
| 61 |
+
time_0930 = pd.to_datetime("09:30").time()
|
| 62 |
+
time_1510 = pd.to_datetime("15:10").time()
|
| 63 |
+
|
| 64 |
+
if time_0930 not in pivot_close.columns:
|
| 65 |
+
return None, None, None
|
| 66 |
+
|
| 67 |
+
pivot_close = pivot_close.dropna(subset=[time_0930])
|
| 68 |
+
valid_dates = pivot_close.index
|
| 69 |
+
times_15m = pd.date_range("09:15", "09:30", freq="min").time
|
| 70 |
+
|
| 71 |
+
feature_dicts = []
|
| 72 |
+
metadata = {}
|
| 73 |
+
|
| 74 |
+
for date in valid_dates:
|
| 75 |
+
f = {}
|
| 76 |
+
c_series = _safe_fill(pivot_close.loc[date, times_15m].values.astype(float))
|
| 77 |
+
o_series = _safe_fill(pivot_open.loc[date, times_15m].values.astype(float))
|
| 78 |
+
h_series = _safe_fill(pivot_high.loc[date, times_15m].values.astype(float))
|
| 79 |
+
l_series = _safe_fill(pivot_low.loc[date, times_15m].values.astype(float))
|
| 80 |
+
v_series = pd.Series(pivot_vol.loc[date, times_15m].values.astype(float)).fillna(0).values
|
| 81 |
+
|
| 82 |
+
o_915 = o_series[0]
|
| 83 |
+
c_930 = c_series[-1]
|
| 84 |
+
c_919 = c_series[4] if len(c_series) > 4 else c_series[-1]
|
| 85 |
+
|
| 86 |
+
pdc = prev_daily_close.get(date, np.nan)
|
| 87 |
+
f["gap"] = 0 if (pd.isna(pdc) or pdc == 0) else (o_915 / pdc) - 1.0
|
| 88 |
+
f["return_16m"] = (c_930 / o_915) - 1.0 if o_915 != 0 else 0
|
| 89 |
+
f["return_first_5m"] = (c_919 / o_915) - 1.0 if o_915 != 0 else 0
|
| 90 |
+
f["return_next_11m"] = (c_930 / c_919) - 1.0 if c_919 != 0 else 0
|
| 91 |
+
f["std_dev"] = np.std(c_series / (o_915 + 1e-8))
|
| 92 |
+
|
| 93 |
+
max_h = np.max(h_series)
|
| 94 |
+
min_l = np.min(l_series)
|
| 95 |
+
f["range_pct"] = (max_h - min_l) / (o_915 + 1e-8)
|
| 96 |
+
f["upper_shadow"] = (max_h - max(o_915, c_930)) / (o_915 + 1e-8)
|
| 97 |
+
f["lower_shadow"] = (min(o_915, c_930) - min_l) / (o_915 + 1e-8)
|
| 98 |
+
f["total_vol"] = np.sum(v_series)
|
| 99 |
+
|
| 100 |
+
vwap = np.sum(((h_series + l_series + c_series) / 3.0) * v_series) / (np.sum(v_series) + 1e-8)
|
| 101 |
+
f["vwap_dev"] = (c_930 / vwap) - 1.0 if vwap != 0 else 0
|
| 102 |
+
f["price_momentum"] = (c_series[-1] - c_series[-3]) / (c_series[-3] + 1e-8)
|
| 103 |
+
f["vol_momentum"] = (v_series[-1] - v_series[-3]) / (v_series[-3] + 1e-8)
|
| 104 |
+
f["morning_trend"] = np.polyfit(np.arange(len(c_series)), c_series, 1)[0]
|
| 105 |
+
|
| 106 |
+
feature_dicts.append(f)
|
| 107 |
+
|
| 108 |
+
metadata[date] = {
|
| 109 |
+
"c_0930": c_930,
|
| 110 |
+
"h_0930": float(pivot_high.loc[date, time_0930]) if time_0930 in pivot_high.columns else c_930,
|
| 111 |
+
"l_0930": float(pivot_low.loc[date, time_0930]) if time_0930 in pivot_low.columns else c_930,
|
| 112 |
+
"v_0930": float(pivot_vol.loc[date, time_0930]) if time_0930 in pivot_vol.columns else 0,
|
| 113 |
+
"c_1510": float(pivot_close.loc[date, time_1510]) if time_1510 in pivot_close.columns else c_930,
|
| 114 |
+
"h_1510": float(pivot_high.loc[date, time_1510]) if time_1510 in pivot_high.columns else c_930,
|
| 115 |
+
"l_1510": float(pivot_low.loc[date, time_1510]) if time_1510 in pivot_low.columns else c_930,
|
| 116 |
+
"dip_low": float(morning_low_per_date.get(date, c_930)),
|
| 117 |
+
"peak_high": float(morning_high_per_date.get(date, c_930)),
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
X = pd.DataFrame(feature_dicts, index=valid_dates).fillna(0)
|
| 121 |
+
|
| 122 |
+
if time_1510 in pivot_close.columns:
|
| 123 |
+
target = (pivot_close[time_1510] > pivot_close[time_0930]).astype(int)
|
| 124 |
+
else:
|
| 125 |
+
target = pd.Series(0, index=valid_dates)
|
| 126 |
+
|
| 127 |
+
return X, target, metadata
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# ── Sequential features ──────────────────────────────────────────────────────
|
| 131 |
+
|
| 132 |
+
def extract_sequential_features(df):
|
| 133 |
+
"""
|
| 134 |
+
Compute normalised return and raw volume vectors for each minute
|
| 135 |
+
in the 09:15-09:30 window, relative to the 09:30 close.
|
| 136 |
+
"""
|
| 137 |
+
df = df.copy()
|
| 138 |
+
df["time"] = df.index.time
|
| 139 |
+
df["date_only"] = df.index.date
|
| 140 |
+
required_times = (
|
| 141 |
+
pd.date_range("09:15", "09:30", freq="min").time.tolist()
|
| 142 |
+
+ [pd.to_datetime("15:10").time()]
|
| 143 |
+
)
|
| 144 |
+
df = df[~df.index.duplicated(keep="first")]
|
| 145 |
+
|
| 146 |
+
# Compute morning session dip/peak (09:31 to 12:00) for limit-order entries
|
| 147 |
+
time_0931 = pd.to_datetime("09:31").time()
|
| 148 |
+
time_1200 = pd.to_datetime("12:00").time()
|
| 149 |
+
df_morning = df[(df["time"] >= time_0931) & (df["time"] <= time_1200)]
|
| 150 |
+
morning_low_per_date = df_morning.groupby("date_only")["low"].min()
|
| 151 |
+
morning_high_per_date = df_morning.groupby("date_only")["high"].max()
|
| 152 |
+
|
| 153 |
+
df_filtered = df[df["time"].isin(required_times)].copy()
|
| 154 |
+
pivot_close = df_filtered.pivot(index="date_only", columns="time", values="close")
|
| 155 |
+
pivot_high = df_filtered.pivot(index="date_only", columns="time", values="high")
|
| 156 |
+
pivot_low = df_filtered.pivot(index="date_only", columns="time", values="low")
|
| 157 |
+
pivot_vol = df_filtered.pivot(index="date_only", columns="time", values="volume")
|
| 158 |
+
|
| 159 |
+
time_0930 = pd.to_datetime("09:30").time()
|
| 160 |
+
time_1510 = pd.to_datetime("15:10").time()
|
| 161 |
+
|
| 162 |
+
if time_0930 not in pivot_close.columns:
|
| 163 |
+
return None, None, None
|
| 164 |
+
|
| 165 |
+
pivot_close = pivot_close.dropna(subset=[time_0930])
|
| 166 |
+
valid_dates = pivot_close.index
|
| 167 |
+
times_15m = pd.date_range("09:15", "09:30", freq="min").time
|
| 168 |
+
|
| 169 |
+
feature_dicts = []
|
| 170 |
+
metadata = {}
|
| 171 |
+
|
| 172 |
+
for date in valid_dates:
|
| 173 |
+
f = {}
|
| 174 |
+
c_series = _safe_fill(pivot_close.loc[date, times_15m].values.astype(float))
|
| 175 |
+
v_series = pd.Series(pivot_vol.loc[date, times_15m].values.astype(float)).fillna(0).values
|
| 176 |
+
c_ref = c_series[-1]
|
| 177 |
+
|
| 178 |
+
for i, t in enumerate(times_15m):
|
| 179 |
+
f[f"ret_c_{i}"] = (c_series[i] / (c_ref + 1e-8)) - 1.0
|
| 180 |
+
f[f"raw_vol_{i}"] = v_series[i]
|
| 181 |
+
feature_dicts.append(f)
|
| 182 |
+
|
| 183 |
+
metadata[date] = {
|
| 184 |
+
"c_0930": c_ref,
|
| 185 |
+
"h_0930": float(pivot_high.loc[date, time_0930]) if time_0930 in pivot_high.columns else c_ref,
|
| 186 |
+
"l_0930": float(pivot_low.loc[date, time_0930]) if time_0930 in pivot_low.columns else c_ref,
|
| 187 |
+
"v_0930": float(pivot_vol.loc[date, time_0930]) if time_0930 in pivot_vol.columns else 0,
|
| 188 |
+
"c_1510": float(pivot_close.loc[date, time_1510]) if time_1510 in pivot_close.columns else c_ref,
|
| 189 |
+
"h_1510": float(pivot_high.loc[date, time_1510]) if time_1510 in pivot_high.columns else c_ref,
|
| 190 |
+
"l_1510": float(pivot_low.loc[date, time_1510]) if time_1510 in pivot_low.columns else c_ref,
|
| 191 |
+
"dip_low": float(morning_low_per_date.get(date, c_ref)),
|
| 192 |
+
"peak_high": float(morning_high_per_date.get(date, c_ref)),
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
X = pd.DataFrame(feature_dicts, index=valid_dates).fillna(0)
|
| 196 |
+
|
| 197 |
+
if time_1510 in pivot_close.columns:
|
| 198 |
+
target = (pivot_close[time_1510] > pivot_close[time_0930]).astype(int)
|
| 199 |
+
else:
|
| 200 |
+
target = pd.Series(0, index=valid_dates)
|
| 201 |
+
|
| 202 |
+
return X, target, metadata
|
core/groww.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Groww charting API client.
|
| 3 |
+
|
| 4 |
+
Fetches 1-minute OHLCV candle data for NSE CASH segment tickers.
|
| 5 |
+
Handles:
|
| 6 |
+
- Retry with exponential backoff (3 attempts)
|
| 7 |
+
- None / missing values in candle arrays
|
| 8 |
+
- Cumulative volume -> per-candle volume conversion
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import time
|
| 12 |
+
import logging
|
| 13 |
+
import traceback
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import pandas as pd
|
| 18 |
+
import requests
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger("live_trader")
|
| 21 |
+
|
| 22 |
+
GROWW_HEADERS = {
|
| 23 |
+
"User-Agent": (
|
| 24 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
| 25 |
+
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
| 26 |
+
),
|
| 27 |
+
"Accept": "application/json",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def fetch_groww_candles(ticker, days=5, max_retries=3):
|
| 32 |
+
"""
|
| 33 |
+
Fetch 1-min OHLCV from Groww for the last *days* calendar days.
|
| 34 |
+
|
| 35 |
+
Returns a DataFrame [open, high, low, close, volume] with DateTimeIndex,
|
| 36 |
+
or None on complete failure.
|
| 37 |
+
"""
|
| 38 |
+
end_ts = int(time.time() * 1000)
|
| 39 |
+
start_ts = end_ts - (days * 24 * 3600 * 1000)
|
| 40 |
+
|
| 41 |
+
url = (
|
| 42 |
+
f"https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE"
|
| 43 |
+
f"/segment/CASH/{ticker}"
|
| 44 |
+
f"?endTimeInMillis={end_ts}"
|
| 45 |
+
f"&intervalInMinutes=1"
|
| 46 |
+
f"&startTimeInMillis={start_ts}"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
for attempt in range(1, max_retries + 1):
|
| 50 |
+
try:
|
| 51 |
+
resp = requests.get(url, headers=GROWW_HEADERS, timeout=15)
|
| 52 |
+
resp.raise_for_status()
|
| 53 |
+
data = resp.json()
|
| 54 |
+
|
| 55 |
+
if "candles" not in data or not data["candles"]:
|
| 56 |
+
logger.warning(f"[{ticker}] No candles in API response (attempt {attempt})")
|
| 57 |
+
if attempt < max_retries:
|
| 58 |
+
time.sleep(2 * attempt)
|
| 59 |
+
continue
|
| 60 |
+
return None
|
| 61 |
+
|
| 62 |
+
rows = []
|
| 63 |
+
for c in data["candles"]:
|
| 64 |
+
# Skip candles with None / missing OHLCV values
|
| 65 |
+
if c[1] is None or c[2] is None or c[3] is None or c[4] is None or c[5] is None:
|
| 66 |
+
continue
|
| 67 |
+
try:
|
| 68 |
+
dt = datetime.fromtimestamp(c[0])
|
| 69 |
+
rows.append({
|
| 70 |
+
"date": dt,
|
| 71 |
+
"open": float(c[1]),
|
| 72 |
+
"high": float(c[2]),
|
| 73 |
+
"low": float(c[3]),
|
| 74 |
+
"close": float(c[4]),
|
| 75 |
+
"cum_vol": float(c[5]),
|
| 76 |
+
})
|
| 77 |
+
except (TypeError, ValueError):
|
| 78 |
+
continue
|
| 79 |
+
|
| 80 |
+
if not rows:
|
| 81 |
+
logger.warning(f"[{ticker}] All candles had None values (attempt {attempt})")
|
| 82 |
+
if attempt < max_retries:
|
| 83 |
+
time.sleep(2 * attempt)
|
| 84 |
+
continue
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
df = pd.DataFrame(rows)
|
| 88 |
+
df.set_index("date", inplace=True)
|
| 89 |
+
df.sort_index(inplace=True)
|
| 90 |
+
|
| 91 |
+
# Groww volume is cumulative per day -> difference it
|
| 92 |
+
df["date_only"] = df.index.date
|
| 93 |
+
df["volume"] = df.groupby("date_only")["cum_vol"].diff().fillna(df["cum_vol"])
|
| 94 |
+
df["volume"] = np.where(df["volume"] < 0, df["cum_vol"], df["volume"])
|
| 95 |
+
df.drop(columns=["cum_vol", "date_only"], inplace=True)
|
| 96 |
+
|
| 97 |
+
logger.info(f"[{ticker}] Fetched {len(df)} candles "
|
| 98 |
+
f"({df.index.min()} -> {df.index.max()})")
|
| 99 |
+
return df
|
| 100 |
+
|
| 101 |
+
except requests.exceptions.RequestException as e:
|
| 102 |
+
logger.error(f"[{ticker}] API error attempt {attempt}/{max_retries}: {e}")
|
| 103 |
+
if attempt < max_retries:
|
| 104 |
+
time.sleep(3 * attempt)
|
| 105 |
+
|
| 106 |
+
except Exception as e:
|
| 107 |
+
logger.error(f"[{ticker}] Unexpected error attempt {attempt}/{max_retries}: {e}")
|
| 108 |
+
logger.debug(traceback.format_exc())
|
| 109 |
+
if attempt < max_retries:
|
| 110 |
+
time.sleep(3 * attempt)
|
| 111 |
+
|
| 112 |
+
return None
|
core/models.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Model construction and training.
|
| 3 |
+
|
| 4 |
+
build_pipeline_map() -> dict of {ticker: (feature_type, fresh_model_clone)}
|
| 5 |
+
train_models() -> dict of {ticker: (feature_type, fitted_model)}
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import pandas as pd
|
| 9 |
+
from sklearn.ensemble import (
|
| 10 |
+
RandomForestClassifier,
|
| 11 |
+
HistGradientBoostingClassifier,
|
| 12 |
+
VotingClassifier,
|
| 13 |
+
)
|
| 14 |
+
from sklearn.linear_model import LogisticRegression
|
| 15 |
+
from sklearn.preprocessing import StandardScaler
|
| 16 |
+
from sklearn.pipeline import Pipeline
|
| 17 |
+
from sklearn.feature_selection import SelectKBest, f_classif
|
| 18 |
+
from sklearn.base import clone
|
| 19 |
+
|
| 20 |
+
from core.config import TICKERS, DATA_DIR, PIPELINE_MAP
|
| 21 |
+
from core.features import extract_semantic_features, extract_sequential_features
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# ── Base model templates ─────────────────────────────────────────────────────
|
| 25 |
+
|
| 26 |
+
_RF = RandomForestClassifier(
|
| 27 |
+
random_state=42, n_estimators=300, max_depth=8,
|
| 28 |
+
min_samples_leaf=5, n_jobs=-1,
|
| 29 |
+
)
|
| 30 |
+
_GBM = HistGradientBoostingClassifier(
|
| 31 |
+
random_state=42, max_iter=300, l2_regularization=1.0, max_depth=8,
|
| 32 |
+
)
|
| 33 |
+
_ENSEMBLE = VotingClassifier(
|
| 34 |
+
estimators=[("rf", _RF), ("gbm", _GBM)], voting="soft",
|
| 35 |
+
)
|
| 36 |
+
_LR_PIPELINE = Pipeline([
|
| 37 |
+
("scaler", StandardScaler()),
|
| 38 |
+
("lr", LogisticRegression(C=0.1, max_iter=1000)),
|
| 39 |
+
])
|
| 40 |
+
_KBEST15_LR = Pipeline([
|
| 41 |
+
("scaler", StandardScaler()),
|
| 42 |
+
("kbest", SelectKBest(f_classif, k=15)),
|
| 43 |
+
("lr", LogisticRegression(C=0.1, max_iter=1000)),
|
| 44 |
+
])
|
| 45 |
+
|
| 46 |
+
_MODEL_TEMPLATES = {
|
| 47 |
+
"ensemble": _ENSEMBLE,
|
| 48 |
+
"lr_pipeline": _LR_PIPELINE,
|
| 49 |
+
"kbest15_lr": _KBEST15_LR,
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ── Public API ───────────────────────────────────────────────────────────────
|
| 54 |
+
|
| 55 |
+
def build_pipeline_map():
|
| 56 |
+
"""
|
| 57 |
+
Return a dict of {ticker: (feature_type, fresh_model_clone)}.
|
| 58 |
+
Uses PIPELINE_MAP from config to look up the architecture per ticker.
|
| 59 |
+
"""
|
| 60 |
+
result = {}
|
| 61 |
+
for ticker in TICKERS:
|
| 62 |
+
feat_type, model_key = PIPELINE_MAP[ticker]
|
| 63 |
+
result[ticker] = (feat_type, clone(_MODEL_TEMPLATES[model_key]))
|
| 64 |
+
return result
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def train_models(log_fn=None):
|
| 68 |
+
"""
|
| 69 |
+
Load parquet data, extract features, and train all ticker models.
|
| 70 |
+
|
| 71 |
+
Parameters
|
| 72 |
+
----------
|
| 73 |
+
log_fn : callable(str), optional
|
| 74 |
+
Logging function (e.g. logger.info). Falls back to print.
|
| 75 |
+
|
| 76 |
+
Returns
|
| 77 |
+
-------
|
| 78 |
+
dict { ticker: (feature_type, fitted_model) }
|
| 79 |
+
"""
|
| 80 |
+
if log_fn is None:
|
| 81 |
+
log_fn = print
|
| 82 |
+
|
| 83 |
+
pipeline_map = build_pipeline_map()
|
| 84 |
+
models = {}
|
| 85 |
+
|
| 86 |
+
for ticker in TICKERS:
|
| 87 |
+
fpath = DATA_DIR / f"{ticker}_minute.parquet"
|
| 88 |
+
if not fpath.exists():
|
| 89 |
+
log_fn(f"[{ticker}] Parquet file not found: {fpath}")
|
| 90 |
+
continue
|
| 91 |
+
|
| 92 |
+
df = pd.read_parquet(fpath)
|
| 93 |
+
df["date"] = pd.to_datetime(df["date"])
|
| 94 |
+
df.set_index("date", inplace=True)
|
| 95 |
+
df.sort_index(inplace=True)
|
| 96 |
+
|
| 97 |
+
# Keep at most 300 trading days
|
| 98 |
+
unique_days = df.index.normalize().unique()
|
| 99 |
+
if len(unique_days) > 300:
|
| 100 |
+
df = df[df.index.normalize().isin(unique_days[-300:])]
|
| 101 |
+
|
| 102 |
+
feat_type, clf = pipeline_map[ticker]
|
| 103 |
+
|
| 104 |
+
if feat_type == "semantic":
|
| 105 |
+
X, y, _ = extract_semantic_features(df)
|
| 106 |
+
else:
|
| 107 |
+
X, y, _ = extract_sequential_features(df)
|
| 108 |
+
|
| 109 |
+
if X is None or X.empty:
|
| 110 |
+
log_fn(f"[{ticker}] Feature extraction returned empty!")
|
| 111 |
+
continue
|
| 112 |
+
|
| 113 |
+
clf.fit(X, y)
|
| 114 |
+
models[ticker] = (feat_type, clf)
|
| 115 |
+
log_fn(f"[{ticker}] Model trained ({feat_type}) | {len(X)} samples")
|
| 116 |
+
|
| 117 |
+
return models
|
core/taxes.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tax, brokerage, and slippage calculator for Indian equity intraday trades.
|
| 3 |
+
Covers: brokerage (flat Rs 20), STT, exchange txn charge, GST, SEBI fee, stamp duty.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def calculate_taxes_and_slippage(
|
| 8 |
+
price_0930, price_1510, qty,
|
| 9 |
+
high_0930, low_0930, high_1510, low_1510,
|
| 10 |
+
is_short,
|
| 11 |
+
):
|
| 12 |
+
"""
|
| 13 |
+
Compute net PnL after realistic slippage and all Indian regulatory charges.
|
| 14 |
+
|
| 15 |
+
Slippage model: 10% of the 1-min candle's (high - low) range,
|
| 16 |
+
applied as an adverse fill on both entry and exit.
|
| 17 |
+
|
| 18 |
+
Returns
|
| 19 |
+
-------
|
| 20 |
+
(net_pnl, total_taxes, exec_buy_price, exec_sell_price)
|
| 21 |
+
"""
|
| 22 |
+
slip_0930 = (high_0930 - low_0930) * 0.10
|
| 23 |
+
slip_1510 = (high_1510 - low_1510) * 0.10
|
| 24 |
+
|
| 25 |
+
if not is_short:
|
| 26 |
+
# LONG: Buy at 09:30 (ask penalty), Sell at 15:10 (bid penalty)
|
| 27 |
+
actual_buy_price = price_0930 + slip_0930
|
| 28 |
+
actual_sell_price = price_1510 - slip_1510
|
| 29 |
+
else:
|
| 30 |
+
# SHORT: Sell at 09:30 (bid penalty), Buy-to-cover at 15:10 (ask penalty)
|
| 31 |
+
actual_sell_price = price_0930 - slip_0930
|
| 32 |
+
actual_buy_price = price_1510 + slip_1510
|
| 33 |
+
|
| 34 |
+
buy_turnover = actual_buy_price * qty
|
| 35 |
+
sell_turnover = actual_sell_price * qty
|
| 36 |
+
total_turnover = buy_turnover + sell_turnover
|
| 37 |
+
|
| 38 |
+
brokerage = 20.0
|
| 39 |
+
stt = sell_turnover * 0.00025
|
| 40 |
+
exc_txn_charge = total_turnover * 0.0000325
|
| 41 |
+
gst = (brokerage + exc_txn_charge) * 0.18
|
| 42 |
+
sebi_fee = total_turnover * 0.000001
|
| 43 |
+
stamp_duty = buy_turnover * 0.00003
|
| 44 |
+
|
| 45 |
+
total_taxes = brokerage + stt + exc_txn_charge + gst + sebi_fee + stamp_duty
|
| 46 |
+
|
| 47 |
+
gross_pnl = (actual_sell_price - actual_buy_price) * qty
|
| 48 |
+
net_pnl = gross_pnl - total_taxes
|
| 49 |
+
|
| 50 |
+
return net_pnl, total_taxes, actual_buy_price, actual_sell_price
|
data/live_trades.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"starting_capital": 3692.0,
|
| 3 |
+
"leverage": 5.0,
|
| 4 |
+
"last_updated": "2026-06-19T17:26:58.267583+05:30",
|
| 5 |
+
"trades": [
|
| 6 |
+
{
|
| 7 |
+
"date": "2026-06-19",
|
| 8 |
+
"ticker": "POWERGRID",
|
| 9 |
+
"direction": "LONG",
|
| 10 |
+
"confidence": 0.7291,
|
| 11 |
+
"entry_price": 289.6,
|
| 12 |
+
"shares": 63,
|
| 13 |
+
"capital_before": 3692.0,
|
| 14 |
+
"buying_power": 18460.0,
|
| 15 |
+
"liquidity_capped": false,
|
| 16 |
+
"candle_volume": 7916,
|
| 17 |
+
"signal_time": "2026-06-19T17:26:58.267583+05:30",
|
| 18 |
+
"net_pnl": null,
|
| 19 |
+
"exit_price": null,
|
| 20 |
+
"status": "OPEN",
|
| 21 |
+
"all_predictions": {
|
| 22 |
+
"INFY": {
|
| 23 |
+
"prob_up": 0.5328,
|
| 24 |
+
"prob_down": 0.4672
|
| 25 |
+
},
|
| 26 |
+
"ASIANPAINT": {
|
| 27 |
+
"prob_up": 0.4417,
|
| 28 |
+
"prob_down": 0.5583
|
| 29 |
+
},
|
| 30 |
+
"TECHM": {
|
| 31 |
+
"prob_up": 0.529,
|
| 32 |
+
"prob_down": 0.471
|
| 33 |
+
},
|
| 34 |
+
"POWERGRID": {
|
| 35 |
+
"prob_up": 0.7291,
|
| 36 |
+
"prob_down": 0.2709
|
| 37 |
+
},
|
| 38 |
+
"ONGC": {
|
| 39 |
+
"prob_up": 0.4045,
|
| 40 |
+
"prob_down": 0.5955
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
]
|
| 45 |
+
}
|
data/minute_ohlcv/ADANIENT_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:eebc57112ba3b638ac0eb0bfa0afb8ba20fc761f6f45aa91c488e383912a0810
|
| 3 |
+
size 14773599
|
data/minute_ohlcv/ADANIPORTS_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b67f6879f390678a08e49699c291d9d5f571919efbac93d698e36a33fb9cc18a
|
| 3 |
+
size 15238934
|
data/minute_ohlcv/APOLLOHOSP_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:383b2cae56de9e5148107517334adbbd2a75135e6338e1253722054945aee6db
|
| 3 |
+
size 16979850
|
data/minute_ohlcv/ASIANPAINT_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:091db8991ee9823b39280e0f498de5bee5da5fb6f870766c4f669538b43dc2a3
|
| 3 |
+
size 15802023
|
data/minute_ohlcv/AXISBANK_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:15f276861577a02701e486b4f3e67203158a45d2187a39f374656ba17b911388
|
| 3 |
+
size 15441163
|
data/minute_ohlcv/BAJAJ-AUTO_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:601e67d2b7a51e965abd48b74d4eb358492f5e5a78d4667039658af70e5a8220
|
| 3 |
+
size 14106851
|
data/minute_ohlcv/BAJAJFINSV_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:03a29d080333706109ebfc5cbe4c443b768000a52e70357aaa74439e54fb7b52
|
| 3 |
+
size 15227892
|
data/minute_ohlcv/BAJFINANCE_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a396f007b0d7cea66ad3c70e1c82056d51b4ce9551ebe3055003833eaa1c837c
|
| 3 |
+
size 10715033
|
data/minute_ohlcv/BHARTIARTL_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e7d35c8f229c9dc1d7962c45161a57ef748e4aa3546652c1d33f2c92be9d90fc
|
| 3 |
+
size 14982789
|
data/minute_ohlcv/BPCL_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cacbf2ddd085066cf589e1e06afa5396830d7372e08a4a0531a884246317df9c
|
| 3 |
+
size 13187593
|
data/minute_ohlcv/BRITANNIA_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1e5d0892ed6492f785ad3fd45b46c5588f7e621894cd99dfd7440c4fe26c7359
|
| 3 |
+
size 17360107
|
data/minute_ohlcv/CIPLA_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f27a7153429f341b26463c1b87d01360f7d5d39e8f829dc485bbe450d64c2910
|
| 3 |
+
size 14792094
|
data/minute_ohlcv/COALINDIA_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bde38dda44b53363d369c08c6e0e786dc6f1c86ef9f9eca5305b6c20689aebba
|
| 3 |
+
size 14020728
|
data/minute_ohlcv/DIVISLAB_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2dc67e4779c2518d6b785d512c3c3e712fc68983c4e94735e651d9e5ba467788
|
| 3 |
+
size 16927541
|
data/minute_ohlcv/DRREDDY_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:829acfff4c1ff2e33b080182da331bd94ff5b926bc7cf332ca6cc18f03bdf36e
|
| 3 |
+
size 14544884
|
data/minute_ohlcv/EICHERMOT_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:14ec70cd80a92c4951630a4333ca552da7098a6b8ba7e695cbdd6b6a058eccc4
|
| 3 |
+
size 16372533
|
data/minute_ohlcv/GRASIM_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4968f3b0d7aa3540178fe1cfa3b730dd0f29f517dc8ccd043fb618e637633600
|
| 3 |
+
size 15487201
|
data/minute_ohlcv/HCLTECH_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ffb95b06a486264d6aa3c194f74848bd337e4ed1db5a3ca13fa4c3697896cb6
|
| 3 |
+
size 14860521
|
data/minute_ohlcv/HDFCBANK_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:23a13cd204544eacde2f66350dc0c757e5bba16c2614863e3f5a600c5621d1ca
|
| 3 |
+
size 14126028
|
data/minute_ohlcv/HDFCLIFE_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2e1cce52af098c3a52ab43dfdf0ffc6a35d51d2a8582e2b76438ce2128dfe55b
|
| 3 |
+
size 10940288
|
data/minute_ohlcv/HEROMOTOCO_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2e85aba15ae24c84ecab70d7c95d951137321ae46653ea59e2aebd7fb552f7ae
|
| 3 |
+
size 16496977
|
data/minute_ohlcv/HINDALCO_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e2ef7911d15de19463ab781a8d99ef8bb8cffca5ea84966a74bfa4f6cd0e5217
|
| 3 |
+
size 14811915
|
data/minute_ohlcv/HINDUNILVR_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d23ce684444fa3fb77f0728322f095333046cfeb41ce04d24fe3e11004bfb04a
|
| 3 |
+
size 15418103
|
data/minute_ohlcv/ICICIBANK_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:76586d7192f781323d5f2247be3138b8f68b6836f59b1e250d8c54ad4fb772cc
|
| 3 |
+
size 16701861
|
data/minute_ohlcv/INDUSINDBK_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a1b276308d798525dc310eb1c0761b72492feecf199ff1cc0ee2c655efe6a70f
|
| 3 |
+
size 15676926
|
data/minute_ohlcv/INFY_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a3ff8af67ff82f0fb8d0feebee6e9ad69198112623e0017bb4f526e5761fbc3f
|
| 3 |
+
size 15787029
|
data/minute_ohlcv/ITC_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4549e21d5b011b39303c1deaebc41d2095f6a54b789de59d3019cb7dafe18c81
|
| 3 |
+
size 14482090
|
data/minute_ohlcv/JSWSTEEL_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:abb7ac6787da7e99e28acb1d0e3ac73987cc4ddd2b62f34381354f23f0411cff
|
| 3 |
+
size 15132654
|
data/minute_ohlcv/KOTAKBANK_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8e8714261774b11205a22bfa52c5eea47a15dbf43989e8ddaa172356e96e4187
|
| 3 |
+
size 12401933
|
data/minute_ohlcv/LTIM_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c58a67f3d9e2aff5aff191fed63f05f405e01a85290b9ba6371d13eda18c4bf3
|
| 3 |
+
size 14677815
|
data/minute_ohlcv/LT_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a1479bc457abdcd5ae385f15ea0139b88a11e89026f6d89ff3bb467049df6dca
|
| 3 |
+
size 16306243
|
data/minute_ohlcv/MARUTI_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:72d247e8eb660cd8807ca5bf123d5cb29405811d973eda9595981032c15bf9d5
|
| 3 |
+
size 18866904
|
data/minute_ohlcv/MM_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6cbfa7c3ba868bbff6ef6b7143f9f3c2573c8e25b742cf899121095be9829c8a
|
| 3 |
+
size 15397098
|
data/minute_ohlcv/NESTLEIND_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e7fa10b93a3d177f38784b59744ef0c610dc4de681f0818424fecb7a73722a66
|
| 3 |
+
size 13168657
|
data/minute_ohlcv/NTPC_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:269f3bcf9e9cecfc7455b72d082639d57cad7d2b9c5c02c8a36f5c03aa53b85f
|
| 3 |
+
size 13715104
|
data/minute_ohlcv/ONGC_minute.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e55660ed66362851e0350f4bd3b217eabd5fc31d092a2bd22515c077b3cd7f99
|
| 3 |
+
size 13962049
|