sbasu2512 commited on
Commit
d60ae27
·
1 Parent(s): 2c3d9d3

remove unwanted files and create proper sql tables

Browse files
Dockerfile CHANGED
@@ -2,7 +2,7 @@ FROM python:3.12-slim
2
 
3
  WORKDIR /app
4
 
5
- # build-essential: needed for xgboost/duckdb wheels.
6
  # The rest: Playwright Chromium runtime libs (from the scrapper service).
7
  RUN apt-get update \
8
  && apt-get install -y --no-install-recommends \
@@ -42,16 +42,14 @@ RUN python -m playwright install chromium
42
  COPY api ./api
43
  COPY app ./app
44
  COPY database ./database
45
- COPY docker ./docker
46
  COPY evaluation ./evaluation
47
  COPY feature_engine ./feature_engine
48
  COPY inference ./inference
49
  COPY jobs ./jobs
50
  COPY static_data ./static_data
51
- COPY tests ./tests
52
  COPY scrapper_service ./scrapper_service
53
 
54
- RUN mkdir -p /tmp/duckdb_feature_engine /data/db
55
 
56
  EXPOSE 8000
57
 
 
2
 
3
  WORKDIR /app
4
 
5
+ # build-essential: needed for xgboost wheels.
6
  # The rest: Playwright Chromium runtime libs (from the scrapper service).
7
  RUN apt-get update \
8
  && apt-get install -y --no-install-recommends \
 
42
  COPY api ./api
43
  COPY app ./app
44
  COPY database ./database
 
45
  COPY evaluation ./evaluation
46
  COPY feature_engine ./feature_engine
47
  COPY inference ./inference
48
  COPY jobs ./jobs
49
  COPY static_data ./static_data
 
50
  COPY scrapper_service ./scrapper_service
51
 
52
+ RUN mkdir -p /data/db
53
 
54
  EXPOSE 8000
55
 
README.md CHANGED
@@ -499,6 +499,19 @@ assert result.actual_return == pytest.approx(0.03)
499
 
500
  # 9. Architecture: SQLite vs Market Data
501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  A critical architectural detail:
503
 
504
  ## The database does NOT contain the engineered feature matrix
@@ -1831,7 +1844,7 @@ ORDER BY created_at DESC;
1831
  Run:
1832
 
1833
  ```bash
1834
- pytest -q tests/test_resolver.py
1835
  ```
1836
 
1837
  Expected:
 
499
 
500
  # 9. Architecture: SQLite vs Market Data
501
 
502
+ ## Current SQLite feature pipeline
503
+
504
+ Raw data is separated into `ohlcv` (India stocks), `nifty_ohlcv`,
505
+ `india_vix_ohlcv`, `us_stocks_ohlcv`, and `macro_ohlcv`. Macro rows use the
506
+ `macro_type` values `gold`, `brent_crude_oil`, and `usd_inr`; the
507
+ `all_market_ohlcv` view is the unified read path.
508
+
509
+ `jobs.rebuild_feature_stores` persists the five source feature tables, the
510
+ model-ready `merged_features` table, and the D+1-open / next-five-closes
511
+ `training_labels` table in SQLite. No DuckDB dependency is used. The guarded
512
+ `POST /predict?prediction_date=YYYY-MM-DD` endpoint returns
513
+ `not_a_trading_day` when no NSE candle exists for the requested date.
514
+
515
  A critical architectural detail:
516
 
517
  ## The database does NOT contain the engineered feature matrix
 
1844
  Run:
1845
 
1846
  ```bash
1847
+ python -m jobs.resolve_predictions
1848
  ```
1849
 
1850
  Expected:
app/config.py CHANGED
@@ -39,7 +39,7 @@ class Settings:
39
  STATIC_DATA_DIR = Path(
40
  os.getenv(
41
  "STATIC_DATA_DIR",
42
- "/app/static_data",
43
  )
44
  )
45
 
@@ -115,4 +115,4 @@ class Settings:
115
  CORR_LOOKBACK_DAYS = 70
116
 
117
 
118
- settings = Settings()
 
39
  STATIC_DATA_DIR = Path(
40
  os.getenv(
41
  "STATIC_DATA_DIR",
42
+ str(Path(__file__).resolve().parents[1] / "static_data"),
43
  )
44
  )
45
 
 
115
  CORR_LOOKBACK_DAYS = 70
116
 
117
 
118
+ settings = Settings()
app/main.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  from fastapi import Depends, FastAPI, HTTPException
2
  from fastapi.middleware.gzip import GZipMiddleware
3
  from dotenv import load_dotenv
@@ -24,10 +26,28 @@ from jobs.daily_prediction import (
24
  )
25
 
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  app = FastAPI(
28
  title="Stock Prediction Engine",
29
  version="1.0.0",
30
- lifespan=scrapper_lifespan,
31
  )
32
 
33
  app.add_middleware(GZipMiddleware, minimum_size=1000)
@@ -42,18 +62,38 @@ def health():
42
 
43
 
44
  @app.post("/predict", dependencies=[Depends(require_pipeline_guid)])
45
- def predict():
46
 
47
  try:
48
 
49
- result = run_daily_prediction()
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  return {
52
  "status": "success",
53
  "rows": len(result),
54
  "date": str(
55
- result["timestamp"].iloc[0]
56
  ),
 
 
 
 
 
 
 
 
57
  }
58
 
59
  except Exception as exc:
 
1
+ from contextlib import asynccontextmanager
2
+ from datetime import date
3
  from fastapi import Depends, FastAPI, HTTPException
4
  from fastapi.middleware.gzip import GZipMiddleware
5
  from dotenv import load_dotenv
 
26
  )
27
 
28
 
29
+ @asynccontextmanager
30
+ async def lifespan(app: FastAPI):
31
+ # Keep the market universe and its SQLite cache ready before requests.
32
+ # Ingestion starts at each ticker's first missing date, so restarts do not
33
+ # repeatedly download the initial 800-day history.
34
+ from app.config import settings
35
+ from database.connection import connect
36
+ from jobs.market_ingestion import ingest_market_data, sync_static_tickers
37
+ from jobs.rebuild_feature_stores import rebuild_feature_stores
38
+
39
+ with connect(settings.DATABASE_URL) as conn:
40
+ sync_static_tickers(conn, settings.STATIC_DATA_DIR)
41
+ ingest_market_data(conn)
42
+ rebuild_feature_stores()
43
+ async with scrapper_lifespan(app):
44
+ yield
45
+
46
+
47
  app = FastAPI(
48
  title="Stock Prediction Engine",
49
  version="1.0.0",
50
+ lifespan=lifespan,
51
  )
52
 
53
  app.add_middleware(GZipMiddleware, minimum_size=1000)
 
62
 
63
 
64
  @app.post("/predict", dependencies=[Depends(require_pipeline_guid)])
65
+ def predict(prediction_date: date | None = None):
66
 
67
  try:
68
 
69
+ target_date = prediction_date or pd.Timestamp.now(tz="Asia/Kolkata").date()
70
+ from app.config import settings
71
+ from database.connection import connect
72
+
73
+ # NSE candles are the trading-calendar authority. This also handles
74
+ # exchange holidays without maintaining a second hard-coded calendar.
75
+ with connect(settings.DATABASE_URL) as conn:
76
+ with conn.cursor() as cur:
77
+ cur.execute("SELECT 1 FROM ohlcv WHERE trading_date = ? LIMIT 1", (target_date,))
78
+ if cur.fetchone() is None:
79
+ return {"status": "not_a_trading_day", "date": target_date.isoformat()}
80
+
81
+ result = run_daily_prediction(pd.Timestamp(target_date))
82
 
83
  return {
84
  "status": "success",
85
  "rows": len(result),
86
  "date": str(
87
+ target_date
88
  ),
89
+ "predictions": [
90
+ {
91
+ "symbol": row.symbol,
92
+ "predicted_probability": float(row.predicted_probability),
93
+ "rank": int(row.rank),
94
+ }
95
+ for row in result.itertuples(index=False)
96
+ ],
97
  }
98
 
99
  except Exception as exc:
database/market_repository.py CHANGED
@@ -40,15 +40,35 @@ def delete_ticker(conn, ticker_id: int) -> bool:
40
  return cur.rowcount == 1
41
 
42
 
43
- def upsert_ohlcv(conn, ticker_id: int, candle: dict[str, Any]) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  with conn.cursor() as cur:
 
 
 
 
 
 
 
45
  cur.execute("""
46
- INSERT INTO market_ohlcv (ticker_id, trading_date, open, high, low, close, volume)
47
- VALUES (:ticker_id, :trading_date, :open, :high, :low, :close, :volume)
48
  ON CONFLICT (ticker_id, trading_date) DO UPDATE SET
49
  open = excluded.open, high = excluded.high, low = excluded.low,
50
  close = excluded.close, volume = excluded.volume, fetched_at = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')
51
- """, {"ticker_id": ticker_id, **candle})
52
 
53
 
54
  def list_ohlcv(
@@ -62,7 +82,7 @@ def list_ohlcv(
62
  with conn.cursor() as cur:
63
  cur.execute("""
64
  SELECT t.symbol, t.market, o.trading_date, o.open, o.high, o.low, o.close, o.volume
65
- FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id
66
  WHERE (:symbol IS NULL OR t.symbol = :symbol)
67
  AND (:market IS NULL OR t.market = :market)
68
  AND (:start IS NULL OR o.trading_date >= :start)
@@ -78,5 +98,5 @@ def latest_ohlcv_dates(conn) -> dict[int, object]:
78
  # column type for it (unlike a bare column reference) and the PARSE_DECLTYPES
79
  # date converter never fires here — parse the returned ISO text explicitly.
80
  with conn.cursor() as cur:
81
- cur.execute("SELECT ticker_id, MAX(trading_date) AS trading_date FROM market_ohlcv GROUP BY ticker_id")
82
  return {row["ticker_id"]: date.fromisoformat(row["trading_date"]) for row in cur.fetchall()}
 
40
  return cur.rowcount == 1
41
 
42
 
43
+ def _ohlcv_table(ticker: dict[str, Any]) -> tuple[str, str | None]:
44
+ if ticker["market"] == "NSE":
45
+ return "ohlcv", None
46
+ if ticker["market"] == "US":
47
+ return "us_stocks_ohlcv", None
48
+ if ticker["symbol"] == "NIFTY":
49
+ return "nifty_ohlcv", None
50
+ if ticker["symbol"] == "INDIA_VIX":
51
+ return "india_vix_ohlcv", None
52
+ macro_types = {"GOLD": "gold", "BRENT": "brent_crude_oil", "USDINR": "usd_inr"}
53
+ return "macro_ohlcv", macro_types[ticker["symbol"]]
54
+
55
+
56
+ def upsert_ohlcv(conn, ticker: dict[str, Any], candle: dict[str, Any]) -> None:
57
+ table, macro_type = _ohlcv_table(ticker)
58
  with conn.cursor() as cur:
59
+ columns = "ticker_id, trading_date, open, high, low, close, volume"
60
+ values = ":ticker_id, :trading_date, :open, :high, :low, :close, :volume"
61
+ params = {"ticker_id": ticker["id"], **candle}
62
+ if macro_type:
63
+ columns = "ticker_id, macro_type, trading_date, open, high, low, close, volume"
64
+ values = ":ticker_id, :macro_type, :trading_date, :open, :high, :low, :close, :volume"
65
+ params["macro_type"] = macro_type
66
  cur.execute("""
67
+ INSERT INTO {table} ({columns}) VALUES ({values})
 
68
  ON CONFLICT (ticker_id, trading_date) DO UPDATE SET
69
  open = excluded.open, high = excluded.high, low = excluded.low,
70
  close = excluded.close, volume = excluded.volume, fetched_at = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')
71
+ """.format(table=table, columns=columns, values=values), params)
72
 
73
 
74
  def list_ohlcv(
 
82
  with conn.cursor() as cur:
83
  cur.execute("""
84
  SELECT t.symbol, t.market, o.trading_date, o.open, o.high, o.low, o.close, o.volume
85
+ FROM all_market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id
86
  WHERE (:symbol IS NULL OR t.symbol = :symbol)
87
  AND (:market IS NULL OR t.market = :market)
88
  AND (:start IS NULL OR o.trading_date >= :start)
 
98
  # column type for it (unlike a bare column reference) and the PARSE_DECLTYPES
99
  # date converter never fires here — parse the returned ISO text explicitly.
100
  with conn.cursor() as cur:
101
+ cur.execute("SELECT ticker_id, MAX(trading_date) AS trading_date FROM all_market_ohlcv GROUP BY ticker_id")
102
  return {row["ticker_id"]: date.fromisoformat(row["trading_date"]) for row in cur.fetchall()}
database/resolution_repository.py CHANGED
@@ -21,7 +21,7 @@ def fetch_future_ohlcv(conn, symbol: str, prediction_date: date, forward_days: i
21
  with conn.cursor() as cur:
22
  cur.execute("""
23
  SELECT o.trading_date AS timestamp, o.open, o.close
24
- FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id
25
  WHERE t.symbol = ? AND t.market = 'NSE' AND o.trading_date > ?
26
  ORDER BY o.trading_date ASC LIMIT ?
27
  """, (symbol, prediction_date, forward_days))
 
21
  with conn.cursor() as cur:
22
  cur.execute("""
23
  SELECT o.trading_date AS timestamp, o.open, o.close
24
+ FROM ohlcv o JOIN market_tickers t ON t.id = o.ticker_id
25
  WHERE t.symbol = ? AND t.market = 'NSE' AND o.trading_date > ?
26
  ORDER BY o.trading_date ASC LIMIT ?
27
  """, (symbol, prediction_date, forward_days))
database/schema.sql CHANGED
@@ -250,3 +250,93 @@ CREATE TABLE IF NOT EXISTS market_ohlcv (
250
 
251
  CREATE INDEX IF NOT EXISTS idx_market_ohlcv_ticker_date
252
  ON market_ohlcv (ticker_id, trading_date DESC);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
  CREATE INDEX IF NOT EXISTS idx_market_ohlcv_ticker_date
252
  ON market_ohlcv (ticker_id, trading_date DESC);
253
+
254
+ -- ============================================================
255
+ -- 6. MARKET DATA BY DATASET
256
+ --
257
+ -- The legacy market_ohlcv table remains only as a migration source for an
258
+ -- existing installation. New reads and writes use these dataset tables.
259
+ -- ============================================================
260
+
261
+ CREATE TABLE IF NOT EXISTS ohlcv (
262
+ ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
263
+ trading_date DATE NOT NULL,
264
+ open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
265
+ volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
266
+ fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
267
+ PRIMARY KEY (ticker_id, trading_date)
268
+ );
269
+
270
+ CREATE TABLE IF NOT EXISTS nifty_ohlcv (
271
+ ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
272
+ trading_date DATE NOT NULL,
273
+ open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
274
+ volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
275
+ fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
276
+ PRIMARY KEY (ticker_id, trading_date)
277
+ );
278
+
279
+ CREATE TABLE IF NOT EXISTS india_vix_ohlcv (
280
+ ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
281
+ trading_date DATE NOT NULL,
282
+ open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
283
+ volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
284
+ fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
285
+ PRIMARY KEY (ticker_id, trading_date)
286
+ );
287
+
288
+ CREATE TABLE IF NOT EXISTS us_stocks_ohlcv (
289
+ ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
290
+ trading_date DATE NOT NULL,
291
+ open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
292
+ volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
293
+ fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
294
+ PRIMARY KEY (ticker_id, trading_date)
295
+ );
296
+
297
+ CREATE TABLE IF NOT EXISTS macro_ohlcv (
298
+ ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
299
+ macro_type TEXT NOT NULL CHECK (macro_type IN ('gold', 'brent_crude_oil', 'usd_inr')),
300
+ trading_date DATE NOT NULL,
301
+ open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
302
+ volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
303
+ fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
304
+ PRIMARY KEY (ticker_id, trading_date)
305
+ );
306
+
307
+ CREATE VIEW IF NOT EXISTS all_market_ohlcv AS
308
+ SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM ohlcv
309
+ UNION ALL SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM nifty_ohlcv
310
+ UNION ALL SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM india_vix_ohlcv
311
+ UNION ALL SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM us_stocks_ohlcv
312
+ UNION ALL SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM macro_ohlcv;
313
+
314
+ -- Copy legacy data exactly once when upgrading an existing database.
315
+ INSERT OR IGNORE INTO ohlcv SELECT o.* FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.market = 'NSE';
316
+ INSERT OR IGNORE INTO nifty_ohlcv SELECT o.* FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.symbol = 'NIFTY';
317
+ INSERT OR IGNORE INTO india_vix_ohlcv SELECT o.* FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.symbol = 'INDIA_VIX';
318
+ INSERT OR IGNORE INTO us_stocks_ohlcv SELECT o.* FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.market = 'US';
319
+ INSERT OR IGNORE INTO macro_ohlcv (ticker_id, macro_type, trading_date, open, high, low, close, volume, source, fetched_at)
320
+ SELECT o.ticker_id, CASE t.symbol WHEN 'GOLD' THEN 'gold' WHEN 'BRENT' THEN 'brent_crude_oil' WHEN 'USDINR' THEN 'usd_inr' END,
321
+ o.trading_date, o.open, o.high, o.low, o.close, o.volume, o.source, o.fetched_at
322
+ FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.symbol IN ('GOLD', 'BRENT', 'USDINR');
323
+
324
+ -- Feature tables are populated by jobs/rebuild_feature_stores.py in SQLite.
325
+ -- Their dynamic model columns replace these minimal bootstrap definitions.
326
+ CREATE TABLE IF NOT EXISTS ohlcv_features (timestamp DATE NOT NULL, symbol TEXT NOT NULL);
327
+ CREATE TABLE IF NOT EXISTS nifty_features (timestamp DATE NOT NULL);
328
+ CREATE TABLE IF NOT EXISTS vix_features (timestamp DATE NOT NULL);
329
+ CREATE TABLE IF NOT EXISTS macro_features (timestamp DATE NOT NULL);
330
+ CREATE TABLE IF NOT EXISTS us_features (timestamp DATE NOT NULL, symbol TEXT NOT NULL);
331
+ CREATE TABLE IF NOT EXISTS merged_features (timestamp DATE NOT NULL, symbol TEXT NOT NULL);
332
+
333
+ CREATE TABLE IF NOT EXISTS training_labels (
334
+ timestamp DATE NOT NULL,
335
+ symbol TEXT NOT NULL,
336
+ entry_date DATE,
337
+ entry_open REAL,
338
+ max_close_5d REAL,
339
+ actual_return REAL,
340
+ label INTEGER CHECK (label IN (0, 1)),
341
+ PRIMARY KEY (timestamp, symbol)
342
+ );
docker/entrypoint.sh DELETED
@@ -1,8 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
-
4
- echo "Production scheduler starting..."
5
- echo "Timezone: ${TZ:-Asia/Kolkata}"
6
- echo "Schedule: Monday-Friday at 16:00 IST"
7
-
8
- exec python -m jobs.scheduler
 
 
 
 
 
 
 
 
 
evaluation/README.md DELETED
@@ -1,40 +0,0 @@
1
- # Prediction Resolver
2
-
3
- The production contract is fixed:
4
-
5
- ```text
6
- Prediction D
7
- |
8
- +--> D+1 OPEN = entry price
9
- |
10
- +--> D+1 CLOSE
11
- +--> D+2 CLOSE
12
- +--> D+3 CLOSE
13
- +--> D+4 CLOSE
14
- +--> D+5 CLOSE
15
- |
16
- v
17
- MAX(CLOSE)
18
- |
19
- v
20
- actual_return = (max_close / entry_open) - 1
21
- |
22
- v
23
- actual_label = 1 if actual_return >= 0.03 else 0
24
- ```
25
-
26
- Important:
27
-
28
- - `D` itself is never part of the return window.
29
- - Entry is always the **next trading day's OPEN**.
30
- - The maximum is taken over the **five closes D+1 through D+5**.
31
- - A prediction with fewer than five future trading sessions remains unresolved.
32
- - Exactly `3.0%` is a positive label.
33
- - This resolver does not recalculate model features. It only resolves the outcome.
34
- - The OHLCV price source must use the same adjustment convention as training.
35
-
36
- Run tests from the project root:
37
-
38
- ```bash
39
- pytest -q tests/test_resolver.py
40
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evaluation/resolver.py DELETED
@@ -1,9 +0,0 @@
1
- """Compatibility import for the canonical resolution calculation layer."""
2
-
3
- from jobs.resolver import (
4
- FORWARD_TRADING_DAYS,
5
- TARGET_RETURN,
6
- Resolution,
7
- resolve_from_ohlcv_rows,
8
- resolve_prediction_rows,
9
- )
 
 
 
 
 
 
 
 
 
 
feature_engine/engine.py CHANGED
@@ -1,8 +1,8 @@
 
 
1
  from __future__ import annotations
2
 
3
  import json
4
-
5
- import duckdb
6
  import pandas as pd
7
 
8
  from app.config import settings
@@ -16,78 +16,61 @@ class FeatureEngine:
16
  with settings.METADATA_PATH.open() as handle:
17
  self.required_features = set(json.load(handle)["feature_cols"])
18
 
19
- def _connection(self):
20
- con = duckdb.connect()
21
- con.execute("PRAGMA threads=2")
22
- con.execute("PRAGMA memory_limit='4GB'")
23
- con.execute("PRAGMA temp_directory='/tmp/duckdb_feature_engine'")
24
- return con
25
-
26
  @staticmethod
27
- def _read_table(sqlite_conn, table_name: str) -> pd.DataFrame:
28
- return pd.read_sql(f"SELECT * FROM {table_name}", sqlite_conn, parse_dates=["timestamp"])
29
 
30
- @staticmethod
31
- def _columns(con, table_name: str):
32
- return [row[0] for row in con.execute(f"DESCRIBE SELECT * FROM {table_name}").fetchall()]
 
33
 
34
- def build_features_for_date(self, prediction_date: pd.Timestamp) -> pd.DataFrame:
35
- prediction_date = pd.Timestamp(prediction_date)
36
- start_date = prediction_date - pd.Timedelta(days=settings.FEATURE_LOOKBACK_DAYS)
 
 
 
 
 
 
37
 
38
- sqlite_conn = connect(settings.DATABASE_URL)
39
- # pandas' sqlite reader assumes plain tuple rows to infer column dtypes;
40
- # our shared connect() sets a dict row_factory for the rest of the app,
41
- # which silently corrupts pd.read_sql results if left in place here.
42
- sqlite_conn.row_factory = None
 
 
 
 
 
 
 
 
 
43
  try:
44
- ohlcv_df = self._read_table(sqlite_conn, settings.OHLCV_FEATURES_TABLE)
45
- nifty_df = self._read_table(sqlite_conn, settings.NIFTY_FEATURES_TABLE)
46
- vix_df = self._read_table(sqlite_conn, settings.VIX_FEATURES_TABLE)
47
- macro_df = self._read_table(sqlite_conn, settings.MACRO_FEATURES_TABLE)
48
- us_features_df = self._read_table(sqlite_conn, settings.US_FEATURES_TABLE)
49
  finally:
50
- sqlite_conn.close()
 
 
 
51
 
52
- us_wide = build_us_wide(us_features_df, start_date, prediction_date)
53
- df = self._load_and_merge(ohlcv_df, nifty_df, vix_df, macro_df, us_wide, start_date, prediction_date)
54
- df = add_correlation_features(df)
55
- df = df[df["timestamp"].dt.normalize() == prediction_date.normalize()].copy()
 
 
 
 
 
56
  missing = sorted(self.required_features - set(df.columns))
57
  if missing:
58
  raise RuntimeError(f"Feature-store merge is missing model columns: {missing}")
59
  return df
60
-
61
- def _load_and_merge(self, ohlcv_df, nifty_df, vix_df, macro_df, us_wide, start_date, end_date):
62
- con = self._connection()
63
- try:
64
- con.register("ohlcv_df", ohlcv_df)
65
- con.register("nifty_df", nifty_df)
66
- con.register("vix_df", vix_df)
67
- con.register("macro_df", macro_df)
68
- con.register("us_wide", us_wide)
69
-
70
- nifty_columns = self._columns(con, "nifty_df")
71
- vix_columns = self._columns(con, "vix_df")
72
- macro_columns = self._columns(con, "macro_df")
73
-
74
- nifty_select = [f'n."{col}" AS "nifty_{col}"' for col in nifty_columns if f"nifty_{col}" in self.required_features]
75
- vix_select = [f'v."{col}"' for col in vix_columns if col != "timestamp" and col in self.required_features]
76
- if "close_1" in self.required_features and "close" in vix_columns:
77
- vix_select.append('v."close" AS "close_1"')
78
- macro_select = [f'm."{col}"' for col in macro_columns if col != "timestamp" and col in self.required_features]
79
- us_select = [f'u."{col}"' for col in us_wide.columns if col != "timestamp" and col in self.required_features]
80
- selects = ["o.*", *nifty_select, *vix_select, *macro_select, *us_select]
81
- query = f"""
82
- SELECT {", ".join(selects)}
83
- FROM ohlcv_df o
84
- ASOF LEFT JOIN nifty_df n ON o.timestamp >= n.timestamp
85
- ASOF LEFT JOIN vix_df v ON o.timestamp >= v.timestamp
86
- ASOF LEFT JOIN macro_df m ON o.timestamp >= m.timestamp
87
- ASOF LEFT JOIN us_wide u ON o.timestamp >= u.timestamp
88
- WHERE o.timestamp >= ? AND o.timestamp <= ?
89
- ORDER BY o.symbol, o.timestamp
90
- """
91
- return con.execute(query, [start_date.date(), end_date.date()]).df()
92
- finally:
93
- con.close()
 
1
+ """Build and persist model features using SQLite-backed feature stores only."""
2
+
3
  from __future__ import annotations
4
 
5
  import json
 
 
6
  import pandas as pd
7
 
8
  from app.config import settings
 
16
  with settings.METADATA_PATH.open() as handle:
17
  self.required_features = set(json.load(handle)["feature_cols"])
18
 
 
 
 
 
 
 
 
19
  @staticmethod
20
+ def _read_table(conn, table_name: str) -> pd.DataFrame:
21
+ return pd.read_sql_query(f'SELECT * FROM "{table_name}"', conn, parse_dates=["timestamp"])
22
 
23
+ def _merge(self, ohlcv_df, nifty_df, vix_df, macro_df, us_features_df) -> pd.DataFrame:
24
+ """Attach only information available on or before each NSE session."""
25
+ us_wide = build_us_wide(us_features_df)
26
+ base = ohlcv_df.sort_values(["timestamp", "symbol"]).copy()
27
 
28
+ def attach(left, right, columns):
29
+ if not columns:
30
+ return left
31
+ right = right[["timestamp", *columns]].sort_values("timestamp")
32
+ return pd.merge_asof(left.sort_values("timestamp"), right, on="timestamp", direction="backward")
33
+
34
+ nifty_cols = [c for c in nifty_df if c != "timestamp" and f"nifty_{c}" in self.required_features]
35
+ nifty = nifty_df.rename(columns={c: f"nifty_{c}" for c in nifty_cols})
36
+ base = attach(base, nifty, [f"nifty_{c}" for c in nifty_cols])
37
 
38
+ vix_cols = [c for c in vix_df if c != "timestamp" and c in self.required_features]
39
+ if "close_1" in self.required_features and "close" in vix_df:
40
+ vix_df = vix_df.rename(columns={"close": "close_1"})
41
+ vix_cols.append("close_1")
42
+ base = attach(base, vix_df, list(dict.fromkeys(vix_cols)))
43
+ base = attach(base, macro_df, [c for c in macro_df if c != "timestamp" and c in self.required_features])
44
+ base = attach(base, us_wide, [c for c in us_wide if c != "timestamp" and c in self.required_features])
45
+ return add_correlation_features(base.sort_values(["symbol", "timestamp"]).reset_index(drop=True))
46
+
47
+ def build_all_features(self, conn=None) -> pd.DataFrame:
48
+ owns_connection = conn is None
49
+ conn = conn or connect(settings.DATABASE_URL)
50
+ original_row_factory = conn.row_factory
51
+ conn.row_factory = None
52
  try:
53
+ frames = [self._read_table(conn, name) for name in (
54
+ settings.OHLCV_FEATURES_TABLE, settings.NIFTY_FEATURES_TABLE,
55
+ settings.VIX_FEATURES_TABLE, settings.MACRO_FEATURES_TABLE,
56
+ settings.US_FEATURES_TABLE,
57
+ )]
58
  finally:
59
+ conn.row_factory = original_row_factory
60
+ if owns_connection:
61
+ conn.close()
62
+ return self._merge(*frames)
63
 
64
+ def build_features_for_date(self, prediction_date: pd.Timestamp) -> pd.DataFrame:
65
+ prediction_date = pd.Timestamp(prediction_date).normalize()
66
+ conn = connect(settings.DATABASE_URL)
67
+ conn.row_factory = None
68
+ try:
69
+ df = pd.read_sql_query('SELECT * FROM merged_features WHERE timestamp = ?', conn,
70
+ params=(prediction_date.date().isoformat(),), parse_dates=["timestamp"])
71
+ finally:
72
+ conn.close()
73
  missing = sorted(self.required_features - set(df.columns))
74
  if missing:
75
  raise RuntimeError(f"Feature-store merge is missing model columns: {missing}")
76
  return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feature_engine/macro.py DELETED
File without changes
feature_engine/nifty.py DELETED
@@ -1,27 +0,0 @@
1
- from pathlib import Path
2
- import duckdb
3
- import pandas as pd
4
-
5
-
6
- def load_nifty(
7
- path: Path,
8
- start_date: pd.Timestamp,
9
- end_date: pd.Timestamp,
10
- ) -> pd.DataFrame:
11
-
12
- con = duckdb.connect()
13
-
14
- try:
15
- return con.execute(
16
- f"""
17
- SELECT *
18
- FROM read_parquet('{path.as_posix()}')
19
- WHERE
20
- timestamp <= '{end_date.date()}'
21
- AND timestamp >= '{start_date.date()}'
22
- ORDER BY timestamp
23
- """
24
- ).df()
25
-
26
- finally:
27
- con.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feature_engine/stock.py DELETED
@@ -1,29 +0,0 @@
1
- from pathlib import Path
2
-
3
- import duckdb
4
- import pandas as pd
5
-
6
-
7
- def load_stock_features(
8
- path: Path,
9
- start_date: pd.Timestamp,
10
- end_date: pd.Timestamp,
11
- ) -> pd.DataFrame:
12
-
13
- con = duckdb.connect()
14
-
15
- try:
16
-
17
- query = f"""
18
- SELECT *
19
- FROM read_parquet('{path.as_posix()}')
20
- WHERE
21
- timestamp >= '{start_date.date()}'
22
- AND timestamp <= '{end_date.date()}'
23
- ORDER BY symbol, timestamp
24
- """
25
-
26
- return con.execute(query).df()
27
-
28
- finally:
29
- con.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feature_engine/us_market.py CHANGED
@@ -1,76 +1,17 @@
1
- import duckdb
2
  import pandas as pd
3
 
4
 
5
- def build_us_wide(
6
- us_features_df: pd.DataFrame,
7
- start_date: pd.Timestamp,
8
- end_date: pd.Timestamp,
9
- ) -> pd.DataFrame:
10
-
11
- con = duckdb.connect()
12
- con.register("us_features", us_features_df)
13
-
14
- query = f"""
15
- SELECT
16
- timestamp,
17
-
18
- AVG(ret_1d) AS us_ret_1d,
19
- AVG(ret_5d) AS us_ret_5d,
20
- AVG(ret_20d) AS us_ret_20d,
21
- AVG(ret_60d) AS us_ret_60d,
22
-
23
- AVG(
24
- CASE
25
- WHEN ret_1d > 0 THEN 1.0
26
- ELSE 0.0
27
- END
28
- ) AS us_breadth_1d,
29
-
30
- AVG(
31
- CASE
32
- WHEN ret_5d > 0 THEN 1.0
33
- ELSE 0.0
34
- END
35
- ) AS us_breadth_5d,
36
-
37
- AVG(
38
- CASE
39
- WHEN ret_20d > 0 THEN 1.0
40
- ELSE 0.0
41
- END
42
- ) AS us_breadth_20d,
43
-
44
- STDDEV(ret_1d) AS us_dispersion_1d,
45
- STDDEV(ret_5d) AS us_dispersion_5d,
46
-
47
- AVG(vol_ratio) AS us_avg_vol_ratio,
48
-
49
- AVG(
50
- CASE
51
- WHEN close_sma20_ratio > 0
52
- THEN 1.0
53
- ELSE 0.0
54
- END
55
- ) AS us_pct_above_sma20,
56
-
57
- AVG(rsi_14) AS us_avg_rsi,
58
-
59
- AVG(distance_from_52w_high)
60
- AS us_avg_dist_52w_high
61
-
62
- FROM us_features
63
-
64
- WHERE
65
- timestamp >= '{start_date.date()}'
66
- AND timestamp <= '{end_date.date()}'
67
-
68
- GROUP BY timestamp
69
-
70
- ORDER BY timestamp
71
- """
72
-
73
- try:
74
- return con.execute(query).df()
75
- finally:
76
- con.close()
 
 
1
  import pandas as pd
2
 
3
 
4
+ def build_us_wide(us_features_df: pd.DataFrame) -> pd.DataFrame:
5
+ """Aggregate US stock features by session without DuckDB."""
6
+ return us_features_df.groupby("timestamp", as_index=False).agg(
7
+ us_ret_1d=("ret_1d", "mean"), us_ret_5d=("ret_5d", "mean"),
8
+ us_ret_20d=("ret_20d", "mean"), us_ret_60d=("ret_60d", "mean"),
9
+ us_breadth_1d=("ret_1d", lambda s: (s > 0).mean()),
10
+ us_breadth_5d=("ret_5d", lambda s: (s > 0).mean()),
11
+ us_breadth_20d=("ret_20d", lambda s: (s > 0).mean()),
12
+ us_dispersion_1d=("ret_1d", "std"), us_dispersion_5d=("ret_5d", "std"),
13
+ us_avg_vol_ratio=("vol_ratio", "mean"),
14
+ us_pct_above_sma20=("close_sma20_ratio", lambda s: (s > 0).mean()),
15
+ us_avg_rsi=("rsi_14", "mean"),
16
+ us_avg_dist_52w_high=("distance_from_52w_high", "mean"),
17
+ ).sort_values("timestamp")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feature_engine/vix.py DELETED
File without changes
jobs/healthcheck.py DELETED
@@ -1,34 +0,0 @@
1
- """
2
- Small process-level health check for container/orchestrator use.
3
- """
4
-
5
- from __future__ import annotations
6
-
7
- import os
8
- import sys
9
-
10
- from database.connection import connect
11
-
12
-
13
- def main() -> int:
14
- database_url = os.getenv("DATABASE_URL")
15
-
16
- if not database_url:
17
- print("UNHEALTHY: DATABASE_URL is not configured")
18
- return 1
19
-
20
- try:
21
- with connect(database_url) as conn:
22
- with conn.cursor() as cur:
23
- cur.execute("SELECT 1")
24
- cur.fetchone()
25
- except Exception as exc:
26
- print(f"UNHEALTHY: SQLite connection failed: {exc}")
27
- return 1
28
-
29
- print("HEALTHY")
30
- return 0
31
-
32
-
33
- if __name__ == "__main__":
34
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
jobs/market_ingestion.py CHANGED
@@ -61,7 +61,9 @@ def ingest_market_data(conn, lookback_days: int = 800, overlap_days: int = 7) ->
61
  inserted = 0
62
  for ticker in rows:
63
  latest_date = latest_dates.get(ticker["id"])
64
- start = (latest_date - timedelta(days=overlap_days)) if latest_date else (date.today() - timedelta(days=lookback_days))
 
 
65
  history = yf.Ticker(ticker["yfinance_symbol"]).history(start=start.isoformat(), auto_adjust=False)
66
  if history.empty:
67
  continue
@@ -132,9 +134,7 @@ def ingest_market_data(conn, lookback_days: int = 800, overlap_days: int = 7) ->
132
  volume_value = int(volume)
133
 
134
  upsert_ohlcv(
135
- conn,
136
- ticker["id"],
137
- {
138
  "trading_date": timestamp.date(),
139
  "open": open_price,
140
  "high": high_price,
 
61
  inserted = 0
62
  for ticker in rows:
63
  latest_date = latest_dates.get(ticker["id"])
64
+ # A populated ticker is fetched only from the first missing session;
65
+ # the initial history window is used once, not every day.
66
+ start = (latest_date + timedelta(days=1)) if latest_date else (date.today() - timedelta(days=lookback_days))
67
  history = yf.Ticker(ticker["yfinance_symbol"]).history(start=start.isoformat(), auto_adjust=False)
68
  if history.empty:
69
  continue
 
134
  volume_value = int(volume)
135
 
136
  upsert_ohlcv(
137
+ conn, ticker, {
 
 
138
  "trading_date": timestamp.date(),
139
  "open": open_price,
140
  "high": high_price,
jobs/rebuild_feature_stores.py CHANGED
@@ -7,6 +7,7 @@ from scrapper_service.logger import logger
7
  from app.config import settings
8
  from database.connection import connect
9
  from feature_engine.training_transforms import macro, nifty, ohlcv, vix
 
10
 
11
 
12
  def _load_candles(conn, market: str | None = None, symbols: tuple[str, ...] | None = None) -> pd.DataFrame:
@@ -20,7 +21,7 @@ def _load_candles(conn, market: str | None = None, symbols: tuple[str, ...] | No
20
  params.extend(symbols)
21
  query = f"""
22
  SELECT o.trading_date AS timestamp, t.symbol, o.open, o.high, o.low, o.close, o.volume
23
- FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id
24
  WHERE {" AND ".join(clauses)}
25
  ORDER BY t.symbol, o.trading_date
26
  """
@@ -40,6 +41,24 @@ def _write_table(df: pd.DataFrame, conn, table_name: str) -> None:
40
  raise
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def rebuild_feature_stores() -> dict[str, int]:
44
 
45
  logger.info(
@@ -211,6 +230,12 @@ def rebuild_feature_stores() -> dict[str, int]:
211
  settings.MACRO_FEATURES_TABLE,
212
  )
213
 
 
 
 
 
 
 
214
  conn.commit()
215
 
216
  except Exception:
@@ -251,6 +276,8 @@ def rebuild_feature_stores() -> dict[str, int]:
251
  settings.NIFTY_FEATURES_TABLE,
252
  settings.VIX_FEATURES_TABLE,
253
  settings.MACRO_FEATURES_TABLE,
 
 
254
  ]
255
 
256
  missing = [
@@ -272,6 +299,7 @@ def rebuild_feature_stores() -> dict[str, int]:
272
  "nifty": len(nifty_features),
273
  "vix": len(vix_features),
274
  "macro": len(macro_features),
 
275
  }
276
 
277
 
 
7
  from app.config import settings
8
  from database.connection import connect
9
  from feature_engine.training_transforms import macro, nifty, ohlcv, vix
10
+ from feature_engine.engine import FeatureEngine
11
 
12
 
13
  def _load_candles(conn, market: str | None = None, symbols: tuple[str, ...] | None = None) -> pd.DataFrame:
 
21
  params.extend(symbols)
22
  query = f"""
23
  SELECT o.trading_date AS timestamp, t.symbol, o.open, o.high, o.low, o.close, o.volume
24
+ FROM all_market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id
25
  WHERE {" AND ".join(clauses)}
26
  ORDER BY t.symbol, o.trading_date
27
  """
 
41
  raise
42
 
43
 
44
+ def _build_labels(conn) -> pd.DataFrame:
45
+ """Create auditable D+1-open / next-five-closes training labels in SQLite."""
46
+ raw = pd.read_sql_query("""
47
+ SELECT t.symbol, o.trading_date AS timestamp, o.open, o.close
48
+ FROM ohlcv o JOIN market_tickers t ON t.id = o.ticker_id
49
+ WHERE t.active ORDER BY t.symbol, o.trading_date
50
+ """, conn, parse_dates=["timestamp"])
51
+ raw["entry_date"] = raw.groupby("symbol")["timestamp"].shift(-1)
52
+ raw["entry_open"] = raw.groupby("symbol")["open"].shift(-1)
53
+ future_closes = [raw.groupby("symbol")["close"].shift(-i) for i in range(1, 6)]
54
+ raw["max_close_5d"] = pd.concat(future_closes, axis=1).max(axis=1)
55
+ complete = pd.concat(future_closes, axis=1).notna().all(axis=1) & raw["entry_open"].gt(0)
56
+ raw["actual_return"] = raw["max_close_5d"] / raw["entry_open"] - 1
57
+ raw["label"] = pd.NA
58
+ raw.loc[complete, "label"] = (raw.loc[complete, "actual_return"] >= settings.TARGET_THRESHOLD).astype(int)
59
+ return raw[["timestamp", "symbol", "entry_date", "entry_open", "max_close_5d", "actual_return", "label"]]
60
+
61
+
62
  def rebuild_feature_stores() -> dict[str, int]:
63
 
64
  logger.info(
 
230
  settings.MACRO_FEATURES_TABLE,
231
  )
232
 
233
+ # The merged matrix is also persisted in SQLite so serving never
234
+ # needs an in-memory DuckDB query.
235
+ merged_features = FeatureEngine().build_all_features(conn)
236
+ _write_table(merged_features, conn, "merged_features")
237
+ _write_table(_build_labels(conn), conn, "training_labels")
238
+
239
  conn.commit()
240
 
241
  except Exception:
 
276
  settings.NIFTY_FEATURES_TABLE,
277
  settings.VIX_FEATURES_TABLE,
278
  settings.MACRO_FEATURES_TABLE,
279
+ "merged_features",
280
+ "training_labels",
281
  ]
282
 
283
  missing = [
 
299
  "nifty": len(nifty_features),
300
  "vix": len(vix_features),
301
  "macro": len(macro_features),
302
+ "merged": len(merged_features),
303
  }
304
 
305
 
jobs/resolve_predictions.py CHANGED
@@ -1,4 +1,4 @@
1
- """Resolve mature predictions using the market_ohlcv table."""
2
 
3
  from __future__ import annotations
4
 
@@ -16,6 +16,3 @@ def main() -> None:
16
  resolved = resolve_pending_predictions(conn)
17
  print(f"Resolution complete: {resolved} predictions resolved")
18
 
19
-
20
- if __name__ == "__main__":
21
- main()
 
1
+ """Resolve mature predictions using the India-stock OHLCV table."""
2
 
3
  from __future__ import annotations
4
 
 
16
  resolved = resolve_pending_predictions(conn)
17
  print(f"Resolution complete: {resolved} predictions resolved")
18
 
 
 
 
jobs/run_once.py DELETED
@@ -1,13 +0,0 @@
1
- """
2
- Run the complete Phase 5 pipeline once.
3
-
4
- Useful for:
5
- - deployment smoke tests
6
- - manually replaying a production day
7
- - debugging without waiting for 16:00 IST
8
- """
9
-
10
- from jobs.daily_pipeline import run_pipeline
11
-
12
- if __name__ == "__main__":
13
- run_pipeline()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml DELETED
@@ -1,3 +0,0 @@
1
- [tool.pytest.ini_options]
2
- pythonpath = ["."]
3
- testpaths = ["tests"]
 
 
 
 
requirements.txt CHANGED
@@ -20,7 +20,6 @@ python-dotenv==1.2.2
20
 
21
  pandas
22
  numpy
23
- duckdb
24
  xgboost
25
  scikit-learn
26
 
@@ -80,4 +79,4 @@ lxml==6.1.0
80
  # ------------------------------------------------------------
81
 
82
  uvloop==0.22.1 ; sys_platform != "win32"
83
- huggingface_hub
 
20
 
21
  pandas
22
  numpy
 
23
  xgboost
24
  scikit-learn
25
 
 
79
  # ------------------------------------------------------------
80
 
81
  uvloop==0.22.1 ; sys_platform != "win32"
82
+ huggingface_hub
tests/manual_test.py DELETED
@@ -1,29 +0,0 @@
1
- def run_manual_test(prediction_date):
2
-
3
- # 1. Generate today's feature rows
4
- features = build_live_features(
5
- as_of_date=prediction_date
6
- )
7
-
8
- # 2. Validate against model metadata
9
- validate_features(features)
10
-
11
- # 3. Run XGBoost
12
- predictions = predict(features)
13
-
14
- # 4. Rank
15
- predictions = rank_predictions(predictions)
16
-
17
- # 5. Store
18
- save_predictions(predictions)
19
-
20
- # 6. Resolve using historical future data
21
- resolve_predictions(
22
- as_of_date=prediction_date
23
- )
24
-
25
- # 7. Calculate metrics
26
- calculate_metrics()
27
-
28
- # 8. Check retraining condition
29
- check_retrain()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_metrics.py DELETED
@@ -1,40 +0,0 @@
1
- from datetime import date
2
- import pytest
3
- from evaluation.metrics import (
4
- precision_at_threshold, hit_rate, positive_rate,
5
- average_return, binary_pr_auc, rank_deciles, calculate_metrics
6
- )
7
-
8
- def test_precision():
9
- assert precision_at_threshold([.9,.8,.4,.2], [1,0,1,0], .5) == .5
10
-
11
- def test_rates():
12
- assert hit_rate([1,0,1,0,0]) == .4
13
- assert positive_rate([1,0,1,0,0]) == .4
14
-
15
- def test_average_return():
16
- assert average_return([.03,.05,-.01]) == pytest.approx(.0233333333)
17
-
18
- def test_pr_auc():
19
- assert binary_pr_auc([.9,.8,.2,.1], [1,1,0,0]) == pytest.approx(1.0)
20
-
21
- def test_deciles():
22
- probs = list(reversed([i/100 for i in range(1,101)]))
23
- deciles = rank_deciles(probs, [1]*100, [.03]*100)
24
- assert len(deciles) == 10
25
- assert all(d["count"] == 10 for d in deciles)
26
-
27
- def test_complete_metrics():
28
- rows = [
29
- {"prediction_date":date(2026,7,1),"symbol":"AAA","predicted_probability":.9,"actual_label":1,"actual_return":.05},
30
- {"prediction_date":date(2026,7,1),"symbol":"BBB","predicted_probability":.4,"actual_label":0,"actual_return":-.02},
31
- {"prediction_date":date(2026,7,2),"symbol":"AAA","predicted_probability":.8,"actual_label":1,"actual_return":.04},
32
- {"prediction_date":date(2026,7,2),"symbol":"BBB","predicted_probability":.3,"actual_label":0,"actual_return":-.01},
33
- ]
34
- result = calculate_metrics(rows, threshold=.5)
35
- assert result.prediction_days == 2
36
- assert result.resolved_predictions == 4
37
- assert result.precision == 1.0
38
- assert result.hit_rate == .5
39
- assert result.average_return == pytest.approx(.015)
40
- assert len(result.deciles) == 10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_resolver.py DELETED
@@ -1,76 +0,0 @@
1
- from datetime import date
2
-
3
- # from evaluation.resolver import resolve_prediction_rows
4
-
5
- from jobs import resolver
6
-
7
-
8
- def test_positive_prediction():
9
- result = resolver.resolve_prediction_rows(
10
- prediction_date=date(2026, 8, 3),
11
- symbol="ABC",
12
- future_rows=[
13
- {"date": date(2026, 8, 4), "open": 100.0, "close": 101.0},
14
- {"date": date(2026, 8, 5), "open": 101.0, "close": 102.0},
15
- {"date": date(2026, 8, 6), "open": 102.0, "close": 103.0},
16
- {"date": date(2026, 8, 7), "open": 103.0, "close": 104.0},
17
- {"date": date(2026, 8, 10), "open": 104.0, "close": 104.0},
18
- ],
19
- )
20
-
21
- assert result is not None
22
- assert result.entry_date == date(2026, 8, 4)
23
- assert result.entry_open == 100.0
24
- assert result.max_close == 104.0
25
- assert result.actual_return == 0.04
26
- assert result.actual_label == 1
27
-
28
-
29
- def test_exact_three_percent_is_positive():
30
- result = resolver.resolve_prediction_rows(
31
- prediction_date=date(2026, 8, 3),
32
- symbol="ABC",
33
- future_rows=[
34
- {"date": date(2026, 8, 4), "open": 100.0, "close": 103.0},
35
- {"date": date(2026, 8, 5), "open": 103.0, "close": 101.0},
36
- {"date": date(2026, 8, 6), "open": 101.0, "close": 100.0},
37
- {"date": date(2026, 8, 7), "open": 100.0, "close": 99.0},
38
- {"date": date(2026, 8, 10), "open": 99.0, "close": 98.0},
39
- ],
40
- )
41
-
42
- assert result is not None
43
- assert result.actual_return == 0.03
44
- assert result.actual_label == 1
45
-
46
-
47
- def test_below_three_percent_is_negative():
48
- result = resolver.resolve_prediction_rows(
49
- prediction_date=date(2026, 8, 3),
50
- symbol="ABC",
51
- future_rows=[
52
- {"date": date(2026, 8, 4), "open": 100.0, "close": 102.99},
53
- {"date": date(2026, 8, 5), "open": 102.0, "close": 102.5},
54
- {"date": date(2026, 8, 6), "open": 102.0, "close": 101.0},
55
- {"date": date(2026, 8, 7), "open": 101.0, "close": 100.0},
56
- {"date": date(2026, 8, 10), "open": 100.0, "close": 99.0},
57
- ],
58
- )
59
-
60
- assert result is not None
61
- assert result.actual_return < 0.03
62
- assert result.actual_label == 0
63
-
64
-
65
- def test_incomplete_window_stays_unresolved():
66
- result = resolver.resolve_prediction_rows(
67
- prediction_date=date(2026, 8, 3),
68
- symbol="ABC",
69
- future_rows=[
70
- {"date": date(2026, 8, 4), "open": 100.0, "close": 105.0},
71
- {"date": date(2026, 8, 5), "open": 105.0, "close": 105.0},
72
- {"date": date(2026, 8, 6), "open": 105.0, "close": 105.0},
73
- ],
74
- )
75
-
76
- assert result is None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_retrain_trigger.py DELETED
@@ -1,27 +0,0 @@
1
- import pytest
2
- from evaluation.retrain_trigger import (
3
- calculate_deterioration, evaluate_retrain_trigger
4
- )
5
-
6
- def test_15_percent_deterioration_triggers():
7
- result = evaluate_retrain_trigger(0.60, 0.51)
8
- assert result.should_flag is True
9
- assert result.deterioration == pytest.approx(0.15)
10
-
11
- def test_less_than_15_percent_deterioration_does_not_trigger():
12
- result = evaluate_retrain_trigger(0.60, 0.52)
13
- assert result.should_flag is False
14
- assert result.deterioration == pytest.approx(0.1333333333)
15
-
16
- def test_improvement_does_not_trigger():
17
- result = evaluate_retrain_trigger(0.60, 0.65)
18
- assert result.should_flag is False
19
- assert result.deterioration < 0
20
-
21
- def test_no_metric_does_not_trigger():
22
- result = evaluate_retrain_trigger(0.60, None)
23
- assert result.should_flag is False
24
- assert result.deterioration is None
25
-
26
- def test_deterioration_formula():
27
- assert calculate_deterioration(0.50, 0.425) == pytest.approx(0.15)