sbasu2512 commited on
Commit
4adfcf2
·
1 Parent(s): e77e965

replace pandas to sql with sql alchemy

Browse files
database/sqlalchemy_store.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Transactional SQLAlchemy Core helpers for SQLite feature-store tables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import contextmanager
6
+ from datetime import date, datetime
7
+ import re
8
+
9
+ import pandas as pd
10
+ from sqlalchemy import Boolean, DateTime, Float, Integer, MetaData, Table, Text, Column, create_engine, event, text
11
+
12
+
13
+ def _engine(database_url: str):
14
+ engine = create_engine(database_url, connect_args={"timeout": 30})
15
+
16
+ @event.listens_for(engine, "connect")
17
+ def _configure_sqlite(dbapi_connection, _):
18
+ cursor = dbapi_connection.cursor()
19
+ cursor.execute("PRAGMA foreign_keys = ON")
20
+ cursor.execute("PRAGMA journal_mode = WAL")
21
+ cursor.execute("PRAGMA busy_timeout = 30000")
22
+ cursor.close()
23
+
24
+ return engine
25
+
26
+
27
+ @contextmanager
28
+ def read_connection(database_url: str):
29
+ engine = _engine(database_url)
30
+ try:
31
+ with engine.connect() as connection:
32
+ yield connection
33
+ finally:
34
+ engine.dispose()
35
+
36
+
37
+ def _column_type(values: pd.Series):
38
+ if pd.api.types.is_bool_dtype(values):
39
+ return Boolean()
40
+ if pd.api.types.is_integer_dtype(values):
41
+ return Integer()
42
+ if pd.api.types.is_float_dtype(values):
43
+ return Float()
44
+ if pd.api.types.is_datetime64_any_dtype(values):
45
+ return DateTime()
46
+ return Text()
47
+
48
+
49
+ def _records(frame: pd.DataFrame) -> list[dict]:
50
+ clean = frame.where(pd.notna(frame), None)
51
+ records = []
52
+ for row in clean.to_dict(orient="records"):
53
+ records.append({
54
+ key: (
55
+ value.to_pydatetime() if isinstance(value, pd.Timestamp)
56
+ else value.item() if hasattr(value, "item") and not isinstance(value, (str, bytes))
57
+ else value
58
+ )
59
+ for key, value in row.items()
60
+ })
61
+ return records
62
+
63
+
64
+ def replace_tables(database_url: str, frames: dict[str, pd.DataFrame]) -> None:
65
+ """Atomically replace dynamic feature tables without exposing an empty table.
66
+
67
+ SQLite serializes this `BEGIN IMMEDIATE` transaction across API and
68
+ scheduler processes. DDL and inserts are rolled back together on failure.
69
+ """
70
+ if not frames:
71
+ return
72
+ for table_name in frames:
73
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", table_name):
74
+ raise ValueError(f"Unsafe table name: {table_name}")
75
+
76
+ engine = _engine(database_url)
77
+ try:
78
+ with engine.connect() as connection:
79
+ connection.execute(text("BEGIN IMMEDIATE"))
80
+ try:
81
+ staging: dict[str, str] = {}
82
+ for name, frame in frames.items():
83
+ stage = f"__staging_{name}"
84
+ staging[name] = stage
85
+ connection.execute(text(f'DROP TABLE IF EXISTS "{stage}"'))
86
+ metadata = MetaData()
87
+ table = Table(stage, metadata, *[
88
+ Column(str(column), _column_type(frame[column])) for column in frame.columns
89
+ ])
90
+ table.create(connection)
91
+ rows = _records(frame)
92
+ for offset in range(0, len(rows), 1000):
93
+ connection.execute(table.insert(), rows[offset:offset + 1000])
94
+
95
+ for name, stage in staging.items():
96
+ backup = f"__backup_{name}"
97
+ connection.execute(text(f'DROP TABLE IF EXISTS "{backup}"'))
98
+ exists = connection.execute(text(
99
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = :name"
100
+ ), {"name": name}).scalar() is not None
101
+ if exists:
102
+ connection.execute(text(f'ALTER TABLE "{name}" RENAME TO "{backup}"'))
103
+ connection.execute(text(f'ALTER TABLE "{stage}" RENAME TO "{name}"'))
104
+ if exists:
105
+ connection.execute(text(f'DROP TABLE "{backup}"'))
106
+ connection.commit()
107
+ except Exception:
108
+ connection.rollback()
109
+ raise
110
+ finally:
111
+ engine.dispose()
jobs/rebuild_feature_stores.py CHANGED
@@ -3,51 +3,43 @@
3
  from __future__ import annotations
4
 
5
  import pandas as pd
 
6
  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
  from feature_engine.engine import FeatureEngine
11
 
12
 
13
  def _load_candles(conn, market: str | None = None, symbols: tuple[str, ...] | None = None) -> pd.DataFrame:
14
  clauses = ["t.active"]
15
- params: list[object] = []
16
  if market is not None:
17
- clauses.append("t.market = ?")
18
- params.append(market)
19
  if symbols is not None:
20
- clauses.append(f"t.symbol IN ({', '.join('?' * len(symbols))})")
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
  """
28
- with conn.cursor() as cur:
29
- cur.execute(query, params)
30
- return pd.DataFrame(cur.fetchall())
31
-
32
-
33
- def _write_table(df: pd.DataFrame, conn, table_name: str) -> None:
34
- try:
35
- df.to_sql(table_name, conn, if_exists="replace", index=False)
36
- logger.info(
37
- f"Successfully wrote feature table {table_name}: {len(df)} rows"
38
- )
39
- except Exception:
40
- logger.exception(f"Failed to write feature table {table_name}")
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
  # SQLite data imported from older schema versions may retain numeric
52
  # values as TEXT. Labels must never compare those strings to a number.
53
  raw["open"] = pd.to_numeric(raw["open"], errors="coerce")
@@ -69,31 +61,7 @@ def rebuild_feature_stores() -> dict[str, int]:
69
  "Rebuilding feature stores from SQLite yfinance OHLCV"
70
  )
71
 
72
- with connect(settings.DATABASE_URL) as conn:
73
-
74
- # -------------------------------------------------
75
- # Database identity
76
- # -------------------------------------------------
77
-
78
- with conn.cursor() as cur:
79
-
80
- cur.execute("PRAGMA database_list")
81
-
82
- logger.info(
83
- f"SQLite database: {cur.fetchall()}"
84
- )
85
-
86
- cur.execute("""
87
- SELECT name
88
- FROM sqlite_master
89
- WHERE type = 'table'
90
- ORDER BY name
91
- """)
92
-
93
- logger.info(
94
- f"Tables BEFORE rebuild: "
95
- f"{[row['name'] for row in cur.fetchall()]}"
96
- )
97
 
98
  # -------------------------------------------------
99
  # Load raw data
@@ -204,43 +172,23 @@ def rebuild_feature_stores() -> dict[str, int]:
204
 
205
  try:
206
 
207
- _write_table(
208
- nse_features,
209
- conn,
210
- settings.OHLCV_FEATURES_TABLE,
211
- )
212
-
213
- _write_table(
214
- us_features,
215
- conn,
216
- settings.US_FEATURES_TABLE,
217
- )
218
-
219
- _write_table(
220
- nifty_features,
221
- conn,
222
- settings.NIFTY_FEATURES_TABLE,
223
- )
224
-
225
- _write_table(
226
- vix_features,
227
- conn,
228
- settings.VIX_FEATURES_TABLE,
229
- )
230
-
231
- _write_table(
232
- macro_features,
233
- conn,
234
- settings.MACRO_FEATURES_TABLE,
235
- )
236
-
237
- # The merged matrix is also persisted in SQLite so serving never
238
- # needs an in-memory DuckDB query.
239
- merged_features = FeatureEngine().build_all_features(conn)
240
- _write_table(merged_features, conn, "merged_features")
241
- _write_table(_build_labels(conn), conn, "training_labels")
242
 
243
- conn.commit()
 
 
 
 
 
 
 
 
244
 
245
  except Exception:
246
 
@@ -256,19 +204,7 @@ def rebuild_feature_stores() -> dict[str, int]:
256
  # Verify AFTER commit
257
  # -------------------------------------------------
258
 
259
- with conn.cursor() as cur:
260
-
261
- cur.execute("""
262
- SELECT name
263
- FROM sqlite_master
264
- WHERE type = 'table'
265
- ORDER BY name
266
- """)
267
-
268
- tables = [
269
- row["name"]
270
- for row in cur.fetchall()
271
- ]
272
 
273
  logger.info(
274
  f"Tables AFTER rebuild: {tables}"
 
3
  from __future__ import annotations
4
 
5
  import pandas as pd
6
+ from sqlalchemy import text
7
  from scrapper_service.logger import logger
8
  from app.config import settings
9
+ from database.sqlalchemy_store import read_connection, replace_tables
10
  from feature_engine.training_transforms import macro, nifty, ohlcv, vix
11
  from feature_engine.engine import FeatureEngine
12
 
13
 
14
  def _load_candles(conn, market: str | None = None, symbols: tuple[str, ...] | None = None) -> pd.DataFrame:
15
  clauses = ["t.active"]
16
+ params: dict[str, object] = {}
17
  if market is not None:
18
+ clauses.append("t.market = :market")
19
+ params["market"] = market
20
  if symbols is not None:
21
+ placeholders = []
22
+ for index, symbol in enumerate(symbols):
23
+ key = f"symbol_{index}"
24
+ placeholders.append(f":{key}")
25
+ params[key] = symbol
26
+ clauses.append(f"t.symbol IN ({', '.join(placeholders)})")
27
  query = f"""
28
  SELECT o.trading_date AS timestamp, t.symbol, o.open, o.high, o.low, o.close, o.volume
29
  FROM all_market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id
30
  WHERE {" AND ".join(clauses)}
31
  ORDER BY t.symbol, o.trading_date
32
  """
33
+ return pd.read_sql(text(query), conn, params=params)
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
 
36
  def _build_labels(conn) -> pd.DataFrame:
37
  """Create auditable D+1-open / next-five-closes training labels in SQLite."""
38
+ raw = pd.read_sql(text("""
39
  SELECT t.symbol, o.trading_date AS timestamp, o.open, o.close
40
  FROM ohlcv o JOIN market_tickers t ON t.id = o.ticker_id
41
  WHERE t.active ORDER BY t.symbol, o.trading_date
42
+ """), conn, parse_dates=["timestamp"])
43
  # SQLite data imported from older schema versions may retain numeric
44
  # values as TEXT. Labels must never compare those strings to a number.
45
  raw["open"] = pd.to_numeric(raw["open"], errors="coerce")
 
61
  "Rebuilding feature stores from SQLite yfinance OHLCV"
62
  )
63
 
64
+ with read_connection(settings.DATABASE_URL) as conn:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  # -------------------------------------------------
67
  # Load raw data
 
172
 
173
  try:
174
 
175
+ replace_tables(settings.DATABASE_URL, {
176
+ settings.OHLCV_FEATURES_TABLE: nse_features,
177
+ settings.US_FEATURES_TABLE: us_features,
178
+ settings.NIFTY_FEATURES_TABLE: nifty_features,
179
+ settings.VIX_FEATURES_TABLE: vix_features,
180
+ settings.MACRO_FEATURES_TABLE: macro_features,
181
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
+ # These read the committed source stores, then replace the two
184
+ # derived tables in their own all-or-nothing transaction.
185
+ merged_features = FeatureEngine().build_all_features()
186
+ with read_connection(settings.DATABASE_URL) as label_conn:
187
+ labels = _build_labels(label_conn)
188
+ replace_tables(settings.DATABASE_URL, {
189
+ "merged_features": merged_features,
190
+ "training_labels": labels,
191
+ })
192
 
193
  except Exception:
194
 
 
204
  # Verify AFTER commit
205
  # -------------------------------------------------
206
 
207
+ tables = pd.read_sql(text("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name"), conn)["name"].tolist()
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
  logger.info(
210
  f"Tables AFTER rebuild: {tables}"