diff --git a/app.py b/app.py index 9afafdfb2a8979f60f374d8ddef35aa58999ed35..f6da3083085a38faddd6f0a472547b886fffffc0 100644 --- a/app.py +++ b/app.py @@ -6,10 +6,13 @@ from fastapi.middleware.cors import CORSMiddleware from zoneinfo import ZoneInfo from data_updater import update_daily_data, is_trading_day from forecaster_engine import generate_predictions +from signal_generator import generate_signals IST = ZoneInfo("Asia/Kolkata") MARKET_CLOSE_BUFFER = time(15, 45) # Update runs after 3:45 PM +SIGNAL_TIME = time(9, 30) # Signal generation at 9:30 AM PREDICTIONS_FILE = os.path.join(os.path.dirname(__file__), "predictions.json") +SIGNALS_FILE = os.path.join(os.path.dirname(__file__), "signals.json") app = FastAPI(title="HF NIFTY Forecaster Backend") @@ -33,6 +36,16 @@ def run_update_pipeline(): except Exception as e: print(f"Pipeline error: {e}") +def run_signal_pipeline(): + """Run the 5-ticker signal generator.""" + try: + result = generate_signals() + print(f"Signal generation result: {result.get('primary_signal', {}).get('action', 'UNKNOWN')}") + except Exception as e: + print(f"Signal pipeline error: {e}") + +# ── Existing Endpoints ─────────────────────────────────────────────────────── + @app.get("/predictions") def get_predictions(): if not os.path.exists(PREDICTIONS_FILE): @@ -62,6 +75,87 @@ def cron_trigger(background_tasks: BackgroundTasks): return {"status": "triggered", "message": "Update and forecast pipeline started in the background."} +# ── NEW: Signal Generator Endpoints ────────────────────────────────────────── + +@app.get("/signals") +def get_signals(): + """Get the latest generated trading signals for the 5-ticker system.""" + if not os.path.exists(SIGNALS_FILE): + raise HTTPException(status_code=404, detail="Signals not yet generated. Trigger /cron/signal first.") + + with open(SIGNALS_FILE, "r") as f: + data = json.load(f) + + return data + +@app.post("/cron/signal") +def signal_trigger(background_tasks: BackgroundTasks): + """ + Trigger signal generation at 9:30 AM IST. + Trains models, fetches live candles, generates BUY/SELL signals. + """ + now = datetime.now(IST) + today = now.date() + + # Check if it's a trading day + if not is_trading_day(today): + return {"status": "skipped", "reason": f"{today} is a holiday or weekend"} + + # Run signal generation in background + background_tasks.add_task(run_signal_pipeline) + + return { + "status": "triggered", + "message": "Signal generation pipeline started. Check /signals for results.", + "trigger_time": now.isoformat(), + } + +@app.post("/signals/generate-now") +def force_signal_generation(background_tasks: BackgroundTasks): + """Force signal generation immediately, bypassing time checks.""" + background_tasks.add_task(run_signal_pipeline) + return { + "status": "triggered", + "message": "Signal generation forced. Check /signals for results.", + "trigger_time": datetime.now(IST).isoformat(), + } + +@app.get("/portfolio") +def get_portfolio(): + """Get current portfolio status from trade journal.""" + trade_log = os.path.join(os.path.dirname(__file__), "data", "live_trades.json") + if not os.path.exists(trade_log): + return { + "starting_capital": 3692.0, + "current_capital": 3692.0, + "total_pnl": 0, + "trades_count": 0, + "win_rate": 0, + } + + with open(trade_log, "r") as f: + data = json.load(f) + + trades = data.get("trades", []) + starting_cap = data.get("starting_capital", 3692.0) + + cap = starting_cap + for t in trades: + if "net_pnl" in t and t["net_pnl"] is not None: + cap += t["net_pnl"] + + n_closed = len([t for t in trades if t.get("net_pnl") is not None]) + n_wins = len([t for t in trades if (t.get("net_pnl") or 0) > 0]) + + return { + "starting_capital": starting_cap, + "current_capital": round(cap, 2), + "total_pnl": round(cap - starting_cap, 2), + "trades_count": n_closed, + "win_rate": round(n_wins / n_closed * 100, 1) if n_closed > 0 else 0, + "last_updated": data.get("last_updated"), + } + @app.get("/health") def health_check(): return {"status": "alive", "server_time_ist": datetime.now(IST).isoformat()} diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d95643edbbf1cc0129801508957d45f762ac6c00 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1,6 @@ +# core/__init__.py +from core.config import * +from core.taxes import calculate_taxes_and_slippage +from core.features import extract_semantic_features, extract_sequential_features +from core.models import build_pipeline_map, train_models +from core.groww import fetch_groww_candles diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0adcadc6a1e560328ebd999581214621d8812a6a Binary files /dev/null and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/config.cpython-311.pyc b/core/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9920a3b87b26faf63cecea9b6a0d37511ff3e32 Binary files /dev/null and b/core/__pycache__/config.cpython-311.pyc differ diff --git a/core/__pycache__/features.cpython-311.pyc b/core/__pycache__/features.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d45de1f0b473d15916a750a2fe2129334e63f48 Binary files /dev/null and b/core/__pycache__/features.cpython-311.pyc differ diff --git a/core/__pycache__/groww.cpython-311.pyc b/core/__pycache__/groww.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e9482179ca34c8c8d59e5b16ce2252b941143af Binary files /dev/null and b/core/__pycache__/groww.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17dcf3a284b67fb2817d3a51b93c74b644e24d76 Binary files /dev/null and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/taxes.cpython-311.pyc b/core/__pycache__/taxes.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c49c07d020bb4883335a1dae06dff2834bae707f Binary files /dev/null and b/core/__pycache__/taxes.cpython-311.pyc differ diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000000000000000000000000000000000000..881dbc5e344ee03f7d133ea61d28f671c1f73c8e --- /dev/null +++ b/core/config.py @@ -0,0 +1,37 @@ +""" +Central configuration for the intraday trading system. +All constants, paths, and model architecture definitions live here. +""" + +from pathlib import Path + +# ── Paths ── +PROJECT_DIR = Path(__file__).resolve().parent.parent +DATA_DIR = PROJECT_DIR / "data" / "minute_ohlcv" +TRADE_LOG = PROJECT_DIR / "data" / "live_trades.json" +LOG_FILE = PROJECT_DIR / "logs" / "live_trader.log" + +# ── Universe ── +TICKERS = ["INFY", "ASIANPAINT", "TECHM", "POWERGRID", "ONGC"] + +# ── Capital & Risk ── +STARTING_CAP = 3692.0 +LEVERAGE = 5.0 +MIN_CONFIDENCE = 0.50 + +# ── Schedule (IST) ── +SIGNAL_HOUR = 9 +SIGNAL_MINUTE = 31 + +# ── Pipeline mapping ── +# Defines which feature extractor and model architecture each ticker uses. +# Format: { ticker: (feature_type, model_key) } +# feature_type : "semantic" | "sequential" +# model_key : "ensemble" | "lr_pipeline" +PIPELINE_MAP = { + "INFY": ("semantic", "ensemble"), + "TECHM": ("sequential", "kbest15_lr"), + "ASIANPAINT": ("sequential", "ensemble"), + "POWERGRID": ("sequential", "ensemble"), + "ONGC": ("semantic", "lr_pipeline"), +} diff --git a/core/features.py b/core/features.py new file mode 100644 index 0000000000000000000000000000000000000000..c672a92cc47e46019f7e730f9a7d630921b40eb0 --- /dev/null +++ b/core/features.py @@ -0,0 +1,202 @@ +""" +Feature extraction from 1-min OHLCV DataFrames. + +Two pipelines: + - semantic: Hand-crafted morning indicators (gap, VWAP deviation, momentum, etc.) + - sequential: Raw normalised return and volume vectors for the 09:15-09:30 window. + +Both return (X, y, metadata) where: + X : pd.DataFrame of features, indexed by date + y : pd.Series of binary labels (1 = price up 09:30->15:10) + metadata : dict[date] -> { c_0930, h_0930, l_0930, v_0930, c_1510, h_1510, l_1510 } +""" + +import numpy as np +import pandas as pd + + +# ── Helpers ────────────────────────────────────────────────────────────────── + +def _safe_fill(arr): + """Forward-fill then back-fill NaNs in a 1-D array.""" + return pd.Series(arr).ffill().bfill().values + + +# ── Semantic features ──────────────────────────────────────────────────────── + +def extract_semantic_features(df): + """ + Compute high-level morning indicators from the 09:15-09:30 candle window. + + Features: gap, return_16m, return_first_5m, return_next_11m, std_dev, + range_pct, upper_shadow, lower_shadow, total_vol, vwap_dev, + price_momentum, vol_momentum, morning_trend. + """ + df = df.copy() + df["time"] = df.index.time + df["date_only"] = df.index.date + required_times = ( + pd.date_range("09:15", "09:30", freq="min").time.tolist() + + [pd.to_datetime("15:10").time()] + ) + df = df[~df.index.duplicated(keep="first")] + + daily_close = df.groupby("date_only")["close"].last() + prev_daily_close = daily_close.shift(1) + + # Compute morning session dip/peak (09:31 to 12:00) for limit-order entries + time_0931 = pd.to_datetime("09:31").time() + time_1200 = pd.to_datetime("12:00").time() + df_morning = df[(df["time"] >= time_0931) & (df["time"] <= time_1200)] + morning_low_per_date = df_morning.groupby("date_only")["low"].min() + morning_high_per_date = df_morning.groupby("date_only")["high"].max() + + df_filtered = df[df["time"].isin(required_times)].copy() + pivot_close = df_filtered.pivot(index="date_only", columns="time", values="close") + pivot_open = df_filtered.pivot(index="date_only", columns="time", values="open") + pivot_high = df_filtered.pivot(index="date_only", columns="time", values="high") + pivot_low = df_filtered.pivot(index="date_only", columns="time", values="low") + pivot_vol = df_filtered.pivot(index="date_only", columns="time", values="volume") + + time_0930 = pd.to_datetime("09:30").time() + time_1510 = pd.to_datetime("15:10").time() + + if time_0930 not in pivot_close.columns: + return None, None, None + + pivot_close = pivot_close.dropna(subset=[time_0930]) + valid_dates = pivot_close.index + times_15m = pd.date_range("09:15", "09:30", freq="min").time + + feature_dicts = [] + metadata = {} + + for date in valid_dates: + f = {} + c_series = _safe_fill(pivot_close.loc[date, times_15m].values.astype(float)) + o_series = _safe_fill(pivot_open.loc[date, times_15m].values.astype(float)) + h_series = _safe_fill(pivot_high.loc[date, times_15m].values.astype(float)) + l_series = _safe_fill(pivot_low.loc[date, times_15m].values.astype(float)) + v_series = pd.Series(pivot_vol.loc[date, times_15m].values.astype(float)).fillna(0).values + + o_915 = o_series[0] + c_930 = c_series[-1] + c_919 = c_series[4] if len(c_series) > 4 else c_series[-1] + + pdc = prev_daily_close.get(date, np.nan) + f["gap"] = 0 if (pd.isna(pdc) or pdc == 0) else (o_915 / pdc) - 1.0 + f["return_16m"] = (c_930 / o_915) - 1.0 if o_915 != 0 else 0 + f["return_first_5m"] = (c_919 / o_915) - 1.0 if o_915 != 0 else 0 + f["return_next_11m"] = (c_930 / c_919) - 1.0 if c_919 != 0 else 0 + f["std_dev"] = np.std(c_series / (o_915 + 1e-8)) + + max_h = np.max(h_series) + min_l = np.min(l_series) + f["range_pct"] = (max_h - min_l) / (o_915 + 1e-8) + f["upper_shadow"] = (max_h - max(o_915, c_930)) / (o_915 + 1e-8) + f["lower_shadow"] = (min(o_915, c_930) - min_l) / (o_915 + 1e-8) + f["total_vol"] = np.sum(v_series) + + vwap = np.sum(((h_series + l_series + c_series) / 3.0) * v_series) / (np.sum(v_series) + 1e-8) + f["vwap_dev"] = (c_930 / vwap) - 1.0 if vwap != 0 else 0 + f["price_momentum"] = (c_series[-1] - c_series[-3]) / (c_series[-3] + 1e-8) + f["vol_momentum"] = (v_series[-1] - v_series[-3]) / (v_series[-3] + 1e-8) + f["morning_trend"] = np.polyfit(np.arange(len(c_series)), c_series, 1)[0] + + feature_dicts.append(f) + + metadata[date] = { + "c_0930": c_930, + "h_0930": float(pivot_high.loc[date, time_0930]) if time_0930 in pivot_high.columns else c_930, + "l_0930": float(pivot_low.loc[date, time_0930]) if time_0930 in pivot_low.columns else c_930, + "v_0930": float(pivot_vol.loc[date, time_0930]) if time_0930 in pivot_vol.columns else 0, + "c_1510": float(pivot_close.loc[date, time_1510]) if time_1510 in pivot_close.columns else c_930, + "h_1510": float(pivot_high.loc[date, time_1510]) if time_1510 in pivot_high.columns else c_930, + "l_1510": float(pivot_low.loc[date, time_1510]) if time_1510 in pivot_low.columns else c_930, + "dip_low": float(morning_low_per_date.get(date, c_930)), + "peak_high": float(morning_high_per_date.get(date, c_930)), + } + + X = pd.DataFrame(feature_dicts, index=valid_dates).fillna(0) + + if time_1510 in pivot_close.columns: + target = (pivot_close[time_1510] > pivot_close[time_0930]).astype(int) + else: + target = pd.Series(0, index=valid_dates) + + return X, target, metadata + + +# ── Sequential features ────────────────────────────────────────────────────── + +def extract_sequential_features(df): + """ + Compute normalised return and raw volume vectors for each minute + in the 09:15-09:30 window, relative to the 09:30 close. + """ + df = df.copy() + df["time"] = df.index.time + df["date_only"] = df.index.date + required_times = ( + pd.date_range("09:15", "09:30", freq="min").time.tolist() + + [pd.to_datetime("15:10").time()] + ) + df = df[~df.index.duplicated(keep="first")] + + # Compute morning session dip/peak (09:31 to 12:00) for limit-order entries + time_0931 = pd.to_datetime("09:31").time() + time_1200 = pd.to_datetime("12:00").time() + df_morning = df[(df["time"] >= time_0931) & (df["time"] <= time_1200)] + morning_low_per_date = df_morning.groupby("date_only")["low"].min() + morning_high_per_date = df_morning.groupby("date_only")["high"].max() + + df_filtered = df[df["time"].isin(required_times)].copy() + pivot_close = df_filtered.pivot(index="date_only", columns="time", values="close") + pivot_high = df_filtered.pivot(index="date_only", columns="time", values="high") + pivot_low = df_filtered.pivot(index="date_only", columns="time", values="low") + pivot_vol = df_filtered.pivot(index="date_only", columns="time", values="volume") + + time_0930 = pd.to_datetime("09:30").time() + time_1510 = pd.to_datetime("15:10").time() + + if time_0930 not in pivot_close.columns: + return None, None, None + + pivot_close = pivot_close.dropna(subset=[time_0930]) + valid_dates = pivot_close.index + times_15m = pd.date_range("09:15", "09:30", freq="min").time + + feature_dicts = [] + metadata = {} + + for date in valid_dates: + f = {} + c_series = _safe_fill(pivot_close.loc[date, times_15m].values.astype(float)) + v_series = pd.Series(pivot_vol.loc[date, times_15m].values.astype(float)).fillna(0).values + c_ref = c_series[-1] + + for i, t in enumerate(times_15m): + f[f"ret_c_{i}"] = (c_series[i] / (c_ref + 1e-8)) - 1.0 + f[f"raw_vol_{i}"] = v_series[i] + feature_dicts.append(f) + + metadata[date] = { + "c_0930": c_ref, + "h_0930": float(pivot_high.loc[date, time_0930]) if time_0930 in pivot_high.columns else c_ref, + "l_0930": float(pivot_low.loc[date, time_0930]) if time_0930 in pivot_low.columns else c_ref, + "v_0930": float(pivot_vol.loc[date, time_0930]) if time_0930 in pivot_vol.columns else 0, + "c_1510": float(pivot_close.loc[date, time_1510]) if time_1510 in pivot_close.columns else c_ref, + "h_1510": float(pivot_high.loc[date, time_1510]) if time_1510 in pivot_high.columns else c_ref, + "l_1510": float(pivot_low.loc[date, time_1510]) if time_1510 in pivot_low.columns else c_ref, + "dip_low": float(morning_low_per_date.get(date, c_ref)), + "peak_high": float(morning_high_per_date.get(date, c_ref)), + } + + X = pd.DataFrame(feature_dicts, index=valid_dates).fillna(0) + + if time_1510 in pivot_close.columns: + target = (pivot_close[time_1510] > pivot_close[time_0930]).astype(int) + else: + target = pd.Series(0, index=valid_dates) + + return X, target, metadata diff --git a/core/groww.py b/core/groww.py new file mode 100644 index 0000000000000000000000000000000000000000..826f497035010cbabd60257b991bca327a1be349 --- /dev/null +++ b/core/groww.py @@ -0,0 +1,112 @@ +""" +Groww charting API client. + +Fetches 1-minute OHLCV candle data for NSE CASH segment tickers. +Handles: + - Retry with exponential backoff (3 attempts) + - None / missing values in candle arrays + - Cumulative volume -> per-candle volume conversion +""" + +import time +import logging +import traceback +from datetime import datetime + +import numpy as np +import pandas as pd +import requests + +logger = logging.getLogger("live_trader") + +GROWW_HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ), + "Accept": "application/json", +} + + +def fetch_groww_candles(ticker, days=5, max_retries=3): + """ + Fetch 1-min OHLCV from Groww for the last *days* calendar days. + + Returns a DataFrame [open, high, low, close, volume] with DateTimeIndex, + or None on complete failure. + """ + end_ts = int(time.time() * 1000) + start_ts = end_ts - (days * 24 * 3600 * 1000) + + url = ( + f"https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE" + f"/segment/CASH/{ticker}" + f"?endTimeInMillis={end_ts}" + f"&intervalInMinutes=1" + f"&startTimeInMillis={start_ts}" + ) + + for attempt in range(1, max_retries + 1): + try: + resp = requests.get(url, headers=GROWW_HEADERS, timeout=15) + resp.raise_for_status() + data = resp.json() + + if "candles" not in data or not data["candles"]: + logger.warning(f"[{ticker}] No candles in API response (attempt {attempt})") + if attempt < max_retries: + time.sleep(2 * attempt) + continue + return None + + rows = [] + for c in data["candles"]: + # Skip candles with None / missing OHLCV values + 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: + continue + try: + dt = datetime.fromtimestamp(c[0]) + rows.append({ + "date": dt, + "open": float(c[1]), + "high": float(c[2]), + "low": float(c[3]), + "close": float(c[4]), + "cum_vol": float(c[5]), + }) + except (TypeError, ValueError): + continue + + if not rows: + logger.warning(f"[{ticker}] All candles had None values (attempt {attempt})") + if attempt < max_retries: + time.sleep(2 * attempt) + continue + return None + + df = pd.DataFrame(rows) + df.set_index("date", inplace=True) + df.sort_index(inplace=True) + + # Groww volume is cumulative per day -> difference it + df["date_only"] = df.index.date + df["volume"] = df.groupby("date_only")["cum_vol"].diff().fillna(df["cum_vol"]) + df["volume"] = np.where(df["volume"] < 0, df["cum_vol"], df["volume"]) + df.drop(columns=["cum_vol", "date_only"], inplace=True) + + logger.info(f"[{ticker}] Fetched {len(df)} candles " + f"({df.index.min()} -> {df.index.max()})") + return df + + except requests.exceptions.RequestException as e: + logger.error(f"[{ticker}] API error attempt {attempt}/{max_retries}: {e}") + if attempt < max_retries: + time.sleep(3 * attempt) + + except Exception as e: + logger.error(f"[{ticker}] Unexpected error attempt {attempt}/{max_retries}: {e}") + logger.debug(traceback.format_exc()) + if attempt < max_retries: + time.sleep(3 * attempt) + + return None diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000000000000000000000000000000000000..eb059fb81243b7b6c9564e0a2f9404d6003a2292 --- /dev/null +++ b/core/models.py @@ -0,0 +1,117 @@ +""" +Model construction and training. + +build_pipeline_map() -> dict of {ticker: (feature_type, fresh_model_clone)} +train_models() -> dict of {ticker: (feature_type, fitted_model)} +""" + +import pandas as pd +from sklearn.ensemble import ( + RandomForestClassifier, + HistGradientBoostingClassifier, + VotingClassifier, +) +from sklearn.linear_model import LogisticRegression +from sklearn.preprocessing import StandardScaler +from sklearn.pipeline import Pipeline +from sklearn.feature_selection import SelectKBest, f_classif +from sklearn.base import clone + +from core.config import TICKERS, DATA_DIR, PIPELINE_MAP +from core.features import extract_semantic_features, extract_sequential_features + + +# ── Base model templates ───────────────────────────────────────────────────── + +_RF = RandomForestClassifier( + random_state=42, n_estimators=300, max_depth=8, + min_samples_leaf=5, n_jobs=-1, +) +_GBM = HistGradientBoostingClassifier( + random_state=42, max_iter=300, l2_regularization=1.0, max_depth=8, +) +_ENSEMBLE = VotingClassifier( + estimators=[("rf", _RF), ("gbm", _GBM)], voting="soft", +) +_LR_PIPELINE = Pipeline([ + ("scaler", StandardScaler()), + ("lr", LogisticRegression(C=0.1, max_iter=1000)), +]) +_KBEST15_LR = Pipeline([ + ("scaler", StandardScaler()), + ("kbest", SelectKBest(f_classif, k=15)), + ("lr", LogisticRegression(C=0.1, max_iter=1000)), +]) + +_MODEL_TEMPLATES = { + "ensemble": _ENSEMBLE, + "lr_pipeline": _LR_PIPELINE, + "kbest15_lr": _KBEST15_LR, +} + + +# ── Public API ─────────────────────────────────────────────────────────────── + +def build_pipeline_map(): + """ + Return a dict of {ticker: (feature_type, fresh_model_clone)}. + Uses PIPELINE_MAP from config to look up the architecture per ticker. + """ + result = {} + for ticker in TICKERS: + feat_type, model_key = PIPELINE_MAP[ticker] + result[ticker] = (feat_type, clone(_MODEL_TEMPLATES[model_key])) + return result + + +def train_models(log_fn=None): + """ + Load parquet data, extract features, and train all ticker models. + + Parameters + ---------- + log_fn : callable(str), optional + Logging function (e.g. logger.info). Falls back to print. + + Returns + ------- + dict { ticker: (feature_type, fitted_model) } + """ + if log_fn is None: + log_fn = print + + pipeline_map = build_pipeline_map() + models = {} + + for ticker in TICKERS: + fpath = DATA_DIR / f"{ticker}_minute.parquet" + if not fpath.exists(): + log_fn(f"[{ticker}] Parquet file not found: {fpath}") + continue + + df = pd.read_parquet(fpath) + df["date"] = pd.to_datetime(df["date"]) + df.set_index("date", inplace=True) + df.sort_index(inplace=True) + + # Keep at most 300 trading days + unique_days = df.index.normalize().unique() + if len(unique_days) > 300: + df = df[df.index.normalize().isin(unique_days[-300:])] + + feat_type, clf = pipeline_map[ticker] + + if feat_type == "semantic": + X, y, _ = extract_semantic_features(df) + else: + X, y, _ = extract_sequential_features(df) + + if X is None or X.empty: + log_fn(f"[{ticker}] Feature extraction returned empty!") + continue + + clf.fit(X, y) + models[ticker] = (feat_type, clf) + log_fn(f"[{ticker}] Model trained ({feat_type}) | {len(X)} samples") + + return models diff --git a/core/taxes.py b/core/taxes.py new file mode 100644 index 0000000000000000000000000000000000000000..7feaf7e2ed30f86b12aa2a217af890393ba0fd65 --- /dev/null +++ b/core/taxes.py @@ -0,0 +1,50 @@ +""" +Tax, brokerage, and slippage calculator for Indian equity intraday trades. +Covers: brokerage (flat Rs 20), STT, exchange txn charge, GST, SEBI fee, stamp duty. +""" + + +def calculate_taxes_and_slippage( + price_0930, price_1510, qty, + high_0930, low_0930, high_1510, low_1510, + is_short, +): + """ + Compute net PnL after realistic slippage and all Indian regulatory charges. + + Slippage model: 10% of the 1-min candle's (high - low) range, + applied as an adverse fill on both entry and exit. + + Returns + ------- + (net_pnl, total_taxes, exec_buy_price, exec_sell_price) + """ + slip_0930 = (high_0930 - low_0930) * 0.10 + slip_1510 = (high_1510 - low_1510) * 0.10 + + if not is_short: + # LONG: Buy at 09:30 (ask penalty), Sell at 15:10 (bid penalty) + actual_buy_price = price_0930 + slip_0930 + actual_sell_price = price_1510 - slip_1510 + else: + # SHORT: Sell at 09:30 (bid penalty), Buy-to-cover at 15:10 (ask penalty) + actual_sell_price = price_0930 - slip_0930 + actual_buy_price = price_1510 + slip_1510 + + buy_turnover = actual_buy_price * qty + sell_turnover = actual_sell_price * qty + total_turnover = buy_turnover + sell_turnover + + brokerage = 20.0 + stt = sell_turnover * 0.00025 + exc_txn_charge = total_turnover * 0.0000325 + gst = (brokerage + exc_txn_charge) * 0.18 + sebi_fee = total_turnover * 0.000001 + stamp_duty = buy_turnover * 0.00003 + + total_taxes = brokerage + stt + exc_txn_charge + gst + sebi_fee + stamp_duty + + gross_pnl = (actual_sell_price - actual_buy_price) * qty + net_pnl = gross_pnl - total_taxes + + return net_pnl, total_taxes, actual_buy_price, actual_sell_price diff --git a/data/live_trades.json b/data/live_trades.json new file mode 100644 index 0000000000000000000000000000000000000000..93cfd6b75fcbf629287ab7ed38b8b803b1e47b86 --- /dev/null +++ b/data/live_trades.json @@ -0,0 +1,45 @@ +{ + "starting_capital": 3692.0, + "leverage": 5.0, + "last_updated": "2026-06-19T17:26:58.267583+05:30", + "trades": [ + { + "date": "2026-06-19", + "ticker": "POWERGRID", + "direction": "LONG", + "confidence": 0.7291, + "entry_price": 289.6, + "shares": 63, + "capital_before": 3692.0, + "buying_power": 18460.0, + "liquidity_capped": false, + "candle_volume": 7916, + "signal_time": "2026-06-19T17:26:58.267583+05:30", + "net_pnl": null, + "exit_price": null, + "status": "OPEN", + "all_predictions": { + "INFY": { + "prob_up": 0.5328, + "prob_down": 0.4672 + }, + "ASIANPAINT": { + "prob_up": 0.4417, + "prob_down": 0.5583 + }, + "TECHM": { + "prob_up": 0.529, + "prob_down": 0.471 + }, + "POWERGRID": { + "prob_up": 0.7291, + "prob_down": 0.2709 + }, + "ONGC": { + "prob_up": 0.4045, + "prob_down": 0.5955 + } + } + } + ] +} \ No newline at end of file diff --git a/data/minute_ohlcv/ADANIENT_minute.parquet b/data/minute_ohlcv/ADANIENT_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..6e77c110c0fb702f7703250a9022413c9da0de64 --- /dev/null +++ b/data/minute_ohlcv/ADANIENT_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eebc57112ba3b638ac0eb0bfa0afb8ba20fc761f6f45aa91c488e383912a0810 +size 14773599 diff --git a/data/minute_ohlcv/ADANIPORTS_minute.parquet b/data/minute_ohlcv/ADANIPORTS_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..078ab146818f9da8f4aa95b7362cf1bb00e2bf9a --- /dev/null +++ b/data/minute_ohlcv/ADANIPORTS_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b67f6879f390678a08e49699c291d9d5f571919efbac93d698e36a33fb9cc18a +size 15238934 diff --git a/data/minute_ohlcv/APOLLOHOSP_minute.parquet b/data/minute_ohlcv/APOLLOHOSP_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..57a2bdd59aa08ad0abeae8b7f5c3d71d48f377bc --- /dev/null +++ b/data/minute_ohlcv/APOLLOHOSP_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:383b2cae56de9e5148107517334adbbd2a75135e6338e1253722054945aee6db +size 16979850 diff --git a/data/minute_ohlcv/ASIANPAINT_minute.parquet b/data/minute_ohlcv/ASIANPAINT_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..dad2b6586c4cc4c7dc5c8440daf8c30ed23c8fcf --- /dev/null +++ b/data/minute_ohlcv/ASIANPAINT_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:091db8991ee9823b39280e0f498de5bee5da5fb6f870766c4f669538b43dc2a3 +size 15802023 diff --git a/data/minute_ohlcv/AXISBANK_minute.parquet b/data/minute_ohlcv/AXISBANK_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..f34af7c7da58a444d1ec9964fe6aae99eb4c5595 --- /dev/null +++ b/data/minute_ohlcv/AXISBANK_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15f276861577a02701e486b4f3e67203158a45d2187a39f374656ba17b911388 +size 15441163 diff --git a/data/minute_ohlcv/BAJAJ-AUTO_minute.parquet b/data/minute_ohlcv/BAJAJ-AUTO_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..211306aa5b216e06547c8ab59cb49f02842cc844 --- /dev/null +++ b/data/minute_ohlcv/BAJAJ-AUTO_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:601e67d2b7a51e965abd48b74d4eb358492f5e5a78d4667039658af70e5a8220 +size 14106851 diff --git a/data/minute_ohlcv/BAJAJFINSV_minute.parquet b/data/minute_ohlcv/BAJAJFINSV_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..2d84caa8738639d422d0f291c253ce4f8a937a3f --- /dev/null +++ b/data/minute_ohlcv/BAJAJFINSV_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03a29d080333706109ebfc5cbe4c443b768000a52e70357aaa74439e54fb7b52 +size 15227892 diff --git a/data/minute_ohlcv/BAJFINANCE_minute.parquet b/data/minute_ohlcv/BAJFINANCE_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..2dbbd55669011559f2e105ae3945686d5b129994 --- /dev/null +++ b/data/minute_ohlcv/BAJFINANCE_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a396f007b0d7cea66ad3c70e1c82056d51b4ce9551ebe3055003833eaa1c837c +size 10715033 diff --git a/data/minute_ohlcv/BHARTIARTL_minute.parquet b/data/minute_ohlcv/BHARTIARTL_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..a9902bab49ad19cf7d45ea226912ba0fed668425 --- /dev/null +++ b/data/minute_ohlcv/BHARTIARTL_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7d35c8f229c9dc1d7962c45161a57ef748e4aa3546652c1d33f2c92be9d90fc +size 14982789 diff --git a/data/minute_ohlcv/BPCL_minute.parquet b/data/minute_ohlcv/BPCL_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..8f8f4476984a87bf0303742e67b40c659be6b1a0 --- /dev/null +++ b/data/minute_ohlcv/BPCL_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cacbf2ddd085066cf589e1e06afa5396830d7372e08a4a0531a884246317df9c +size 13187593 diff --git a/data/minute_ohlcv/BRITANNIA_minute.parquet b/data/minute_ohlcv/BRITANNIA_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..11c1526cd005c136006371c2ee6804562cd56cf3 --- /dev/null +++ b/data/minute_ohlcv/BRITANNIA_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e5d0892ed6492f785ad3fd45b46c5588f7e621894cd99dfd7440c4fe26c7359 +size 17360107 diff --git a/data/minute_ohlcv/CIPLA_minute.parquet b/data/minute_ohlcv/CIPLA_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..4f48297eccd1f85ecaa3eb62347d71a1f44e8457 --- /dev/null +++ b/data/minute_ohlcv/CIPLA_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f27a7153429f341b26463c1b87d01360f7d5d39e8f829dc485bbe450d64c2910 +size 14792094 diff --git a/data/minute_ohlcv/COALINDIA_minute.parquet b/data/minute_ohlcv/COALINDIA_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..30eb1b92f06e0396e847d3d33544165ec09537af --- /dev/null +++ b/data/minute_ohlcv/COALINDIA_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bde38dda44b53363d369c08c6e0e786dc6f1c86ef9f9eca5305b6c20689aebba +size 14020728 diff --git a/data/minute_ohlcv/DIVISLAB_minute.parquet b/data/minute_ohlcv/DIVISLAB_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..8ba7ebf0f9b239c10307f77cd2e55ce3a665972f --- /dev/null +++ b/data/minute_ohlcv/DIVISLAB_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2dc67e4779c2518d6b785d512c3c3e712fc68983c4e94735e651d9e5ba467788 +size 16927541 diff --git a/data/minute_ohlcv/DRREDDY_minute.parquet b/data/minute_ohlcv/DRREDDY_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..4f261df8ad6ea50e1eaba929b4e42b73f6e1dbf0 --- /dev/null +++ b/data/minute_ohlcv/DRREDDY_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:829acfff4c1ff2e33b080182da331bd94ff5b926bc7cf332ca6cc18f03bdf36e +size 14544884 diff --git a/data/minute_ohlcv/EICHERMOT_minute.parquet b/data/minute_ohlcv/EICHERMOT_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..d1f0bc3e4fab53d9889527cec5b199c90d6420a6 --- /dev/null +++ b/data/minute_ohlcv/EICHERMOT_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14ec70cd80a92c4951630a4333ca552da7098a6b8ba7e695cbdd6b6a058eccc4 +size 16372533 diff --git a/data/minute_ohlcv/GRASIM_minute.parquet b/data/minute_ohlcv/GRASIM_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..54b7d5b30a6dfca6253f78763f3bc2b7cdce9cb8 --- /dev/null +++ b/data/minute_ohlcv/GRASIM_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4968f3b0d7aa3540178fe1cfa3b730dd0f29f517dc8ccd043fb618e637633600 +size 15487201 diff --git a/data/minute_ohlcv/HCLTECH_minute.parquet b/data/minute_ohlcv/HCLTECH_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..4f257a11f5f1764b2075e1cd0312260fe59ad893 --- /dev/null +++ b/data/minute_ohlcv/HCLTECH_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ffb95b06a486264d6aa3c194f74848bd337e4ed1db5a3ca13fa4c3697896cb6 +size 14860521 diff --git a/data/minute_ohlcv/HDFCBANK_minute.parquet b/data/minute_ohlcv/HDFCBANK_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..5218e9a2f73d5bbebb56d14f01215958ad885f4e --- /dev/null +++ b/data/minute_ohlcv/HDFCBANK_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23a13cd204544eacde2f66350dc0c757e5bba16c2614863e3f5a600c5621d1ca +size 14126028 diff --git a/data/minute_ohlcv/HDFCLIFE_minute.parquet b/data/minute_ohlcv/HDFCLIFE_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..a03158de7fcb9225f3ed639d978823dde2d9c93a --- /dev/null +++ b/data/minute_ohlcv/HDFCLIFE_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1cce52af098c3a52ab43dfdf0ffc6a35d51d2a8582e2b76438ce2128dfe55b +size 10940288 diff --git a/data/minute_ohlcv/HEROMOTOCO_minute.parquet b/data/minute_ohlcv/HEROMOTOCO_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..ca481aacc52fe27198b00c24bd1cda6c33f04698 --- /dev/null +++ b/data/minute_ohlcv/HEROMOTOCO_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e85aba15ae24c84ecab70d7c95d951137321ae46653ea59e2aebd7fb552f7ae +size 16496977 diff --git a/data/minute_ohlcv/HINDALCO_minute.parquet b/data/minute_ohlcv/HINDALCO_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..eea8cf40749a4332637b2fa4d80854767f56abc2 --- /dev/null +++ b/data/minute_ohlcv/HINDALCO_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2ef7911d15de19463ab781a8d99ef8bb8cffca5ea84966a74bfa4f6cd0e5217 +size 14811915 diff --git a/data/minute_ohlcv/HINDUNILVR_minute.parquet b/data/minute_ohlcv/HINDUNILVR_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..d3a4b3b0b5c82e69bf7e2314d4dae5256809e7a1 --- /dev/null +++ b/data/minute_ohlcv/HINDUNILVR_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d23ce684444fa3fb77f0728322f095333046cfeb41ce04d24fe3e11004bfb04a +size 15418103 diff --git a/data/minute_ohlcv/ICICIBANK_minute.parquet b/data/minute_ohlcv/ICICIBANK_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..abcec7b2719ad4afa4a660086b3ddc16dc125c1c --- /dev/null +++ b/data/minute_ohlcv/ICICIBANK_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76586d7192f781323d5f2247be3138b8f68b6836f59b1e250d8c54ad4fb772cc +size 16701861 diff --git a/data/minute_ohlcv/INDUSINDBK_minute.parquet b/data/minute_ohlcv/INDUSINDBK_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..572499a676cb98235c7d462f58fe8776d3a66c62 --- /dev/null +++ b/data/minute_ohlcv/INDUSINDBK_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1b276308d798525dc310eb1c0761b72492feecf199ff1cc0ee2c655efe6a70f +size 15676926 diff --git a/data/minute_ohlcv/INFY_minute.parquet b/data/minute_ohlcv/INFY_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..acf13476110c80073897a39fa9625781fc5f95e2 --- /dev/null +++ b/data/minute_ohlcv/INFY_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3ff8af67ff82f0fb8d0feebee6e9ad69198112623e0017bb4f526e5761fbc3f +size 15787029 diff --git a/data/minute_ohlcv/ITC_minute.parquet b/data/minute_ohlcv/ITC_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..d63cc88c42ca3b9d7e4d39f0ff5b1ae842fc2255 --- /dev/null +++ b/data/minute_ohlcv/ITC_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4549e21d5b011b39303c1deaebc41d2095f6a54b789de59d3019cb7dafe18c81 +size 14482090 diff --git a/data/minute_ohlcv/JSWSTEEL_minute.parquet b/data/minute_ohlcv/JSWSTEEL_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..969c0a4beac73fd790dbd10307734724b3478a48 --- /dev/null +++ b/data/minute_ohlcv/JSWSTEEL_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abb7ac6787da7e99e28acb1d0e3ac73987cc4ddd2b62f34381354f23f0411cff +size 15132654 diff --git a/data/minute_ohlcv/KOTAKBANK_minute.parquet b/data/minute_ohlcv/KOTAKBANK_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..fdd0c287b3ce46a618a7a2a559969dd383e64173 --- /dev/null +++ b/data/minute_ohlcv/KOTAKBANK_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e8714261774b11205a22bfa52c5eea47a15dbf43989e8ddaa172356e96e4187 +size 12401933 diff --git a/data/minute_ohlcv/LTIM_minute.parquet b/data/minute_ohlcv/LTIM_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e6e028d3cdaac68f10f1967ce04a86e35a69f9b5 --- /dev/null +++ b/data/minute_ohlcv/LTIM_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c58a67f3d9e2aff5aff191fed63f05f405e01a85290b9ba6371d13eda18c4bf3 +size 14677815 diff --git a/data/minute_ohlcv/LT_minute.parquet b/data/minute_ohlcv/LT_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..b21c6b486bd1eaf56497a084eeddb9db8184ff5a --- /dev/null +++ b/data/minute_ohlcv/LT_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1479bc457abdcd5ae385f15ea0139b88a11e89026f6d89ff3bb467049df6dca +size 16306243 diff --git a/data/minute_ohlcv/MARUTI_minute.parquet b/data/minute_ohlcv/MARUTI_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e8b15d211fc29522ea920e92706d879d18da7d00 --- /dev/null +++ b/data/minute_ohlcv/MARUTI_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72d247e8eb660cd8807ca5bf123d5cb29405811d973eda9595981032c15bf9d5 +size 18866904 diff --git a/data/minute_ohlcv/MM_minute.parquet b/data/minute_ohlcv/MM_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..ed281ba58218022cab933fd02ed9063b706ff1f6 --- /dev/null +++ b/data/minute_ohlcv/MM_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cbfa7c3ba868bbff6ef6b7143f9f3c2573c8e25b742cf899121095be9829c8a +size 15397098 diff --git a/data/minute_ohlcv/NESTLEIND_minute.parquet b/data/minute_ohlcv/NESTLEIND_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..bb66bc5d40550d133cf0c8e1cad261c286a77f23 --- /dev/null +++ b/data/minute_ohlcv/NESTLEIND_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7fa10b93a3d177f38784b59744ef0c610dc4de681f0818424fecb7a73722a66 +size 13168657 diff --git a/data/minute_ohlcv/NTPC_minute.parquet b/data/minute_ohlcv/NTPC_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..55939b9b36fddf27fdcadc975c81c370f28d73cc --- /dev/null +++ b/data/minute_ohlcv/NTPC_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:269f3bcf9e9cecfc7455b72d082639d57cad7d2b9c5c02c8a36f5c03aa53b85f +size 13715104 diff --git a/data/minute_ohlcv/ONGC_minute.parquet b/data/minute_ohlcv/ONGC_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..d4606f1bca4952a64b6900efed5be7bcd5493538 --- /dev/null +++ b/data/minute_ohlcv/ONGC_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e55660ed66362851e0350f4bd3b217eabd5fc31d092a2bd22515c077b3cd7f99 +size 13962049 diff --git a/data/minute_ohlcv/POWERGRID_minute.parquet b/data/minute_ohlcv/POWERGRID_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..be17b8682b7da3ee6031e827aabec920fa8bbe5f --- /dev/null +++ b/data/minute_ohlcv/POWERGRID_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96c10ff19f6f0e181711858e025c79ad22cf003929404e4cb44c401b05631aeb +size 12764538 diff --git a/data/minute_ohlcv/RELIANCE_minute.parquet b/data/minute_ohlcv/RELIANCE_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..3c4333ad68e6aceaf595d993beca4495307c7bf2 --- /dev/null +++ b/data/minute_ohlcv/RELIANCE_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ff189d724f6bb712c6ab3c950bfd3701af669c1452bcf87c25acbae8fa38949 +size 15513464 diff --git a/data/minute_ohlcv/SBILIFE_minute.parquet b/data/minute_ohlcv/SBILIFE_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..7c4f8d766baec9e9b917861e3c1d1a9293c5ab61 --- /dev/null +++ b/data/minute_ohlcv/SBILIFE_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e61e16dce97f5c763cbe6af49e4979b89df26ec56319c6ddeefb161e283a92eb +size 11563073 diff --git a/data/minute_ohlcv/SBIN_minute.parquet b/data/minute_ohlcv/SBIN_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..8cea5358174a7618a577b84b3fbadb51bc833da5 --- /dev/null +++ b/data/minute_ohlcv/SBIN_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:197a2fa9ed765a7d29689bf1c832ea3f7e9c82ab9486844276bebf7e9a927925 +size 16547280 diff --git a/data/minute_ohlcv/SUNPHARMA_minute.parquet b/data/minute_ohlcv/SUNPHARMA_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..7e396adf7c3afa18e0ce0b7a62aaf13a3c96aec9 --- /dev/null +++ b/data/minute_ohlcv/SUNPHARMA_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05233d2de31bb9a7253dc3a521f2f02d2cbd94c851d103dcfc4fed401e507e80 +size 15125025 diff --git a/data/minute_ohlcv/TATACONSUM_minute.parquet b/data/minute_ohlcv/TATACONSUM_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..b698b2ff5ea7126a5c0f2e3fd83a202805d0b040 --- /dev/null +++ b/data/minute_ohlcv/TATACONSUM_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fd462b005ba2c098c098c2421ba07fdd7c67a937db341118cae56ae0236e13e +size 14533682 diff --git a/data/minute_ohlcv/TATASTEEL_minute.parquet b/data/minute_ohlcv/TATASTEEL_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..f9ff7fb6d5aee7077344846df8eb148eb4b6dc0c --- /dev/null +++ b/data/minute_ohlcv/TATASTEEL_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10431ba56030879353a8f4717456e2520de76cbe31a78f4465c1cff62eade7ce +size 13276561 diff --git a/data/minute_ohlcv/TCS_minute.parquet b/data/minute_ohlcv/TCS_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..eb584bbd11ca7236cd35153d2ce4d31c7c8c868b --- /dev/null +++ b/data/minute_ohlcv/TCS_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aeb1041b8a58c56fb8cd4c74d1997dd801ad031a0ab7f176615602bf901a7494 +size 16632551 diff --git a/data/minute_ohlcv/TECHM_minute.parquet b/data/minute_ohlcv/TECHM_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..bc4c662a8f8abb55b8b11062fbf9f93a238014e9 --- /dev/null +++ b/data/minute_ohlcv/TECHM_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f41b1baaecc19264e3c4fbf414af665fe184f34503f7fee324613ed4a1efaefa +size 14062891 diff --git a/data/minute_ohlcv/TITAN_minute.parquet b/data/minute_ohlcv/TITAN_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..7601a74a65aeed1ebc6e4127a6abd6654328d2f1 --- /dev/null +++ b/data/minute_ohlcv/TITAN_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b9dff915639efda044a9e621bddfbd4f1a1035eec9adbf703bef4a7e9279ad8 +size 16228537 diff --git a/data/minute_ohlcv/ULTRACEMCO_minute.parquet b/data/minute_ohlcv/ULTRACEMCO_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..90d6fee9edac7a353dee114769c208b7c9748ba0 --- /dev/null +++ b/data/minute_ohlcv/ULTRACEMCO_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7eebed335593cc9a33504d7c7f30628483862523e27b334c23dbe2a665e2087 +size 18118880 diff --git a/data/minute_ohlcv/UPL_minute.parquet b/data/minute_ohlcv/UPL_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..4093a5fe0f64d12e4d48709516b256ce78f6a60d --- /dev/null +++ b/data/minute_ohlcv/UPL_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e23ae4f692dae2fb5b01095539067a3e8bcc9c3282feacf50f0af089876bf4f3 +size 14391509 diff --git a/data/minute_ohlcv/WIPRO_minute.parquet b/data/minute_ohlcv/WIPRO_minute.parquet new file mode 100644 index 0000000000000000000000000000000000000000..447382bc9fd71e0bcb9395b2076fe5679382be4d --- /dev/null +++ b/data/minute_ohlcv/WIPRO_minute.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:398f968a03d9668bbbeafd71915913da98873fdb2c44dc0cb736a22d6773d933 +size 13271815 diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/requirements.txt b/requirements.txt index bba6a3f1e2da8518d1d31e89be0e0168c2c685b6..8984def9d6c9dc4aee10b1568f31928a5816d2f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ fastapi uvicorn pandas +numpy requests pandas_market_calendars pyarrow fastparquet +scikit-learn diff --git a/signal_generator.py b/signal_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..882a5004a69ee1b8059aece95f296915b81eb32c --- /dev/null +++ b/signal_generator.py @@ -0,0 +1,318 @@ +""" +Signal Generator Backend for the 5-Ticker Intraday System. + +This module integrates the existing signal generator (core/) into the +HF Space FastAPI backend. It: + 1. Trains models on stored minute OHLCV parquet data. + 2. Fetches live 09:15-09:30 candles from Groww API. + 3. Generates BUY/SELL signals with confidence and share allocation. + 4. Saves signals to signals.json for the frontend to consume. + 5. Exposes /signals endpoint and /cron/signal trigger. +""" + +import os +import sys +import json +import math +import logging +import traceback +from datetime import datetime, date as dt_date, time as dt_time +from pathlib import Path +from zoneinfo import ZoneInfo + +# Ensure core is importable +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from core.config import ( + TICKERS, STARTING_CAP, LEVERAGE, MIN_CONFIDENCE, + SIGNAL_HOUR, SIGNAL_MINUTE, TRADE_LOG, +) +from core.features import extract_semantic_features, extract_sequential_features +from core.models import train_models +from core.groww import fetch_groww_candles + +IST = ZoneInfo("Asia/Kolkata") +SIGNALS_FILE = os.path.join(os.path.dirname(__file__), "signals.json") + +logger = logging.getLogger("signal_generator") +logger.setLevel(logging.DEBUG) +if not logger.handlers: + _ch = logging.StreamHandler(sys.stdout) + _ch.setLevel(logging.INFO) + _ch.setFormatter(logging.Formatter("%(asctime)s | %(levelname)-7s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S")) + logger.addHandler(_ch) + + +# ── Trade Journal ──────────────────────────────────────────────────────────── + +def load_trade_journal(): + if os.path.exists(TRADE_LOG): + try: + with open(TRADE_LOG, "r") as f: + data = json.load(f) + if isinstance(data, dict): + return data.get("trades", []) + return data + except Exception: + pass + return [] + + +def save_trade_journal(trades): + journal = { + "starting_capital": STARTING_CAP, + "leverage": LEVERAGE, + "last_updated": datetime.now(IST).isoformat(), + "trades": trades, + } + with open(TRADE_LOG, "w") as f: + json.dump(journal, f, indent=2, default=str) + + +def get_current_capital(trades): + cap = STARTING_CAP + for t in trades: + if "net_pnl" in t and t["net_pnl"] is not None: + cap += t["net_pnl"] + return cap + + +def already_traded_today(trades, today_str): + return any(t.get("date") == today_str for t in trades) + + +# ── Signal Generation ──────────────────────────────────────────────────────── + +def generate_signals(): + """ + Full signal generation pipeline: + 1. Train models on parquet data + 2. Fetch live candles from Groww + 3. Extract features from 09:15-09:30 window + 4. Generate predictions for all 5 tickers + 5. Pick the best signal and allocate shares based on ₹3692 capital + 6. Save to signals.json + """ + today = dt_date.today() + today_str = today.isoformat() + + logger.info(f"Starting signal generation for {today_str}...") + + # Step 1: Train models + logger.info("Training models on historical minute data...") + models = train_models(log_fn=logger.info) + if not models: + logger.error("No models trained. Cannot generate signals.") + return {"status": "error", "reason": "model training failed"} + + # Step 2: Generate predictions for each ticker + predictions = [] + + for ticker in TICKERS: + if ticker not in models: + logger.warning(f"[{ticker}] No trained model, skipping.") + continue + + pipe_type, clf = models[ticker] + + df_live = fetch_groww_candles(ticker, days=5) + if df_live is None or df_live.empty: + logger.error(f"[{ticker}] Could not fetch live data, skipping.") + continue + + try: + if pipe_type == "semantic": + X_live, _, meta = extract_semantic_features(df_live) + else: + X_live, _, meta = extract_sequential_features(df_live) + except Exception as e: + logger.error(f"[{ticker}] Feature extraction failed: {e}") + logger.debug(traceback.format_exc()) + continue + + if X_live is None or X_live.empty: + logger.warning(f"[{ticker}] No features extracted from live data.") + continue + + # Find today's row (or fall back to latest available) + today_row = None + today_meta = None + for d in X_live.index: + if d == today: + today_row = X_live.loc[[d]] + today_meta = meta.get(d) + break + + if today_row is None: + last_date = X_live.index[-1] + logger.warning(f"[{ticker}] Today ({today}) not found. Using latest: {last_date}") + today_row = X_live.iloc[[-1]] + today_meta = meta.get(last_date) + + if today_meta is None: + logger.warning(f"[{ticker}] No metadata for today.") + continue + + try: + prob_up = clf.predict_proba(today_row)[0][1] + prob_down = 1.0 - prob_up + except Exception as e: + logger.error(f"[{ticker}] Prediction failed: {e}") + logger.debug(traceback.format_exc()) + continue + + predictions.append({ + "ticker": ticker, + "prob_up": prob_up, + "prob_down": prob_down, + "c_0930": today_meta["c_0930"], + "h_0930": today_meta.get("h_0930", today_meta["c_0930"]), + "l_0930": today_meta.get("l_0930", today_meta["c_0930"]), + "v_0930": today_meta.get("v_0930", 0), + }) + + if not predictions: + logger.error("No predictions generated for any ticker.") + _save_no_signal(today_str) + return {"status": "error", "reason": "no predictions generated"} + + # Step 3: Pick best signal + best = None + best_conf = 0.0 + best_short = False + + for p in predictions: + if p["prob_up"] > best_conf: + best_conf = p["prob_up"] + best = p + best_short = False + if p["prob_down"] > best_conf: + best_conf = p["prob_down"] + best = p + best_short = True + + if best is None or best_conf <= MIN_CONFIDENCE: + logger.info("No signal above minimum confidence. Sitting in cash.") + _save_no_signal(today_str) + return {"status": "no_trade", "reason": "below confidence threshold"} + + # Step 4: Position sizing + trades = load_trade_journal() + capital = get_current_capital(trades) + buying_power = capital * LEVERAGE + entry_price = best["c_0930"] + + max_shares_cap = math.floor(buying_power / entry_price) if entry_price > 0 else 0 + candle_vol = best["v_0930"] + max_shares_liq = math.floor(candle_vol * 0.10) if candle_vol > 0 else max_shares_cap + shares = min(max_shares_cap, max_shares_liq) + + if shares <= 0: + logger.warning(f"Position size is 0 (capital={capital:.2f}, price={entry_price:.2f}).") + _save_no_signal(today_str) + return {"status": "no_trade", "reason": "position size is 0"} + + direction = "SELL" if best_short else "BUY" + + # Step 5: Build signal output + signal = { + "date": today_str, + "ticker": best["ticker"], + "action": direction, + "confidence": round(best_conf * 100, 2), + "entry_price": round(entry_price, 2), + "shares": shares, + "position_value": round(entry_price * shares, 2), + "capital": round(capital, 2), + "buying_power": round(buying_power, 2), + "liquidity_capped": shares < max_shares_cap, + "signal_time": datetime.now(IST).isoformat(), + "status": "OPEN", + } + + # All ticker probabilities + all_tickers = [] + for p in predictions: + conf = max(p["prob_up"], p["prob_down"]) + act = "SELL" if p["prob_down"] > p["prob_up"] else "BUY" + + ticker_capital_share = capital / len(predictions) + ticker_bp = ticker_capital_share * LEVERAGE + ticker_shares = math.floor(ticker_bp / p["c_0930"]) if p["c_0930"] > 0 else 0 + ticker_vol = p["v_0930"] + ticker_liq = math.floor(ticker_vol * 0.10) if ticker_vol > 0 else ticker_shares + ticker_shares = min(ticker_shares, ticker_liq) + + all_tickers.append({ + "ticker": p["ticker"], + "action": act, + "confidence": round(conf * 100, 2), + "prob_up": round(p["prob_up"] * 100, 2), + "prob_down": round(p["prob_down"] * 100, 2), + "price": round(p["c_0930"], 2), + "shares": ticker_shares, + "position_value": round(p["c_0930"] * ticker_shares, 2), + }) + + output = { + "generated_at": datetime.now(IST).isoformat(), + "forecast_date": today_str, + "capital": round(capital, 2), + "primary_signal": signal, + "all_signals": sorted(all_tickers, key=lambda x: x["confidence"], reverse=True), + } + + with open(SIGNALS_FILE, "w") as f: + json.dump(output, f, indent=4, default=str) + + # Also log to trade journal + trade_entry = { + "date": today_str, + "ticker": best["ticker"], + "direction": "SHORT" if best_short else "LONG", + "confidence": round(best_conf, 4), + "entry_price": round(entry_price, 2), + "shares": shares, + "capital_before": round(capital, 2), + "buying_power": round(buying_power, 2), + "liquidity_capped": shares < max_shares_cap, + "candle_volume": int(candle_vol), + "signal_time": datetime.now(IST).isoformat(), + "net_pnl": None, + "exit_price": None, + "status": "OPEN", + "all_predictions": {p["ticker"]: {"prob_up": round(p["prob_up"], 4), "prob_down": round(p["prob_down"], 4)} for p in predictions}, + } + trades.append(trade_entry) + save_trade_journal(trades) + + logger.info(f"SIGNAL: {direction} {shares}x {best['ticker']} @ Rs.{entry_price:.2f} (conf={best_conf:.2%})") + + return output + + +def _save_no_signal(today_str): + """Save a no-trade signal.""" + output = { + "generated_at": datetime.now(IST).isoformat(), + "forecast_date": today_str, + "capital": get_current_capital(load_trade_journal()), + "primary_signal": { + "date": today_str, + "ticker": None, + "action": "HOLD", + "confidence": 0, + "shares": 0, + "status": "NO_TRADE", + "signal_time": datetime.now(IST).isoformat(), + }, + "all_signals": [], + } + with open(SIGNALS_FILE, "w") as f: + json.dump(output, f, indent=4, default=str) + + +if __name__ == "__main__": + result = generate_signals() + print(json.dumps(result, indent=2, default=str)) diff --git a/signals.json b/signals.json new file mode 100644 index 0000000000000000000000000000000000000000..faedc8d0bd3007fd697360e23b619bc7789d0bda --- /dev/null +++ b/signals.json @@ -0,0 +1,71 @@ +{ + "generated_at": "2026-06-19T17:26:58.267583+05:30", + "forecast_date": "2026-06-19", + "capital": 3692.0, + "primary_signal": { + "date": "2026-06-19", + "ticker": "POWERGRID", + "action": "BUY", + "confidence": 72.91, + "entry_price": 289.6, + "shares": 63, + "position_value": 18244.8, + "capital": 3692.0, + "buying_power": 18460.0, + "liquidity_capped": false, + "signal_time": "2026-06-19T17:26:58.267583+05:30", + "status": "OPEN" + }, + "all_signals": [ + { + "ticker": "POWERGRID", + "action": "BUY", + "confidence": 72.91, + "prob_up": 72.91, + "prob_down": 27.09, + "price": 289.6, + "shares": 12, + "position_value": 3475.2 + }, + { + "ticker": "ONGC", + "action": "SELL", + "confidence": 59.55, + "prob_up": 40.45, + "prob_down": 59.55, + "price": 244.9, + "shares": 15, + "position_value": 3673.5 + }, + { + "ticker": "ASIANPAINT", + "action": "SELL", + "confidence": 55.83, + "prob_up": 44.17, + "prob_down": 55.83, + "price": 2735.5, + "shares": 1, + "position_value": 2735.5 + }, + { + "ticker": "INFY", + "action": "BUY", + "confidence": 53.28, + "prob_up": 53.28, + "prob_down": 46.72, + "price": 1032.4, + "shares": 3, + "position_value": 3097.2 + }, + { + "ticker": "TECHM", + "action": "BUY", + "confidence": 52.9, + "prob_up": 52.9, + "prob_down": 47.1, + "price": 1366.6, + "shares": 2, + "position_value": 2733.2 + } + ] +} \ No newline at end of file