Spaces:
Running
Running
| """ | |
| Helper script: appends new DB functions to modules/db.py for actual-trade tracking. | |
| Run from project root: python scripts/append_db_functions.py | |
| """ | |
| import os | |
| DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "modules", "db.py") | |
| APPEND = r''' | |
| def snapshot_holdings(owner_id, snapshot_date: str): | |
| """ | |
| Saves current portfolio_holdings as a holdings_snapshot before they are overwritten. | |
| Idempotent — only snapshots once per date. | |
| """ | |
| conn = get_db_connection() | |
| try: | |
| with conn.cursor() as cur: | |
| cur.execute( | |
| "SELECT COUNT(*) FROM holdings_snapshots WHERE user_id = %s AND snapshot_date = %s;", | |
| (owner_id, snapshot_date) | |
| ) | |
| count = cur.fetchone()[0] | |
| if count > 0: | |
| print(f"[db] Snapshot for {snapshot_date} already exists, skipping.") | |
| return | |
| cur.execute( | |
| "SELECT symbol, qty, avg_cost, current_price FROM portfolio_holdings WHERE user_id = %s;", | |
| (owner_id,) | |
| ) | |
| rows = cur.fetchall() | |
| if not rows: | |
| print("[db] No holdings to snapshot.") | |
| return | |
| for r in rows: | |
| cur.execute( | |
| """ | |
| INSERT INTO holdings_snapshots (user_id, snapshot_date, symbol, qty, avg_cost, ltp) | |
| VALUES (%s, %s, %s, %s, %s, %s) | |
| ON CONFLICT (user_id, snapshot_date, symbol) DO NOTHING; | |
| """, | |
| (owner_id, snapshot_date, r[0], r[1], r[2], r[3]) | |
| ) | |
| conn.commit() | |
| print(f"[db] Saved holdings snapshot for {snapshot_date} ({len(rows)} symbols).") | |
| except Exception as e: | |
| conn.rollback() | |
| print(f"[db] snapshot_holdings failed: {e}") | |
| finally: | |
| conn.close() | |
| def detect_and_save_actual_trades(owner_id, new_holdings: list, trade_date: str): | |
| """ | |
| Compares new_holdings against the most recent holdings snapshot. | |
| Symbols where new_qty > snapshot_qty = new purchase. Saves deltas to actual_trades. | |
| new_holdings: list of dicts with keys: symbol, qty, avg_cost, ltp | |
| """ | |
| conn = get_db_connection() | |
| try: | |
| with conn.cursor() as cur: | |
| cur.execute( | |
| """ | |
| SELECT DISTINCT ON (symbol) symbol, qty | |
| FROM holdings_snapshots | |
| WHERE user_id = %s | |
| ORDER BY symbol, snapshot_date DESC; | |
| """, | |
| (owner_id,) | |
| ) | |
| snap_rows = cur.fetchall() | |
| if not snap_rows: | |
| print("[db] No snapshot found - cannot detect trades. Snapshot saved next upload.") | |
| return 0 | |
| snap_map = {r[0]: float(r[1]) for r in snap_rows} | |
| new_map = { | |
| h["symbol"]: (float(h["qty"]), float(h.get("avg_cost", 0)), float(h.get("ltp") or 0)) | |
| for h in new_holdings | |
| } | |
| nifty_price = None | |
| try: | |
| from modules.price_fetcher import fetch_nifty50_current | |
| nifty_price = fetch_nifty50_current() | |
| except Exception as nifty_err: | |
| print(f"[db] Could not fetch Nifty: {nifty_err}") | |
| trades_inserted = 0 | |
| for symbol, (new_qty, avg_cost, ltp) in new_map.items(): | |
| old_qty = snap_map.get(symbol, 0.0) | |
| delta_qty = new_qty - old_qty | |
| if delta_qty > 0.001 and avg_cost > 0: | |
| cur.execute( | |
| """ | |
| INSERT INTO actual_trades (user_id, trade_date, symbol, qty_bought, buy_price, nifty_on_trade_day) | |
| VALUES (%s, %s, %s, %s, %s, %s) | |
| ON CONFLICT (user_id, trade_date, symbol) DO UPDATE | |
| SET qty_bought = EXCLUDED.qty_bought, | |
| buy_price = EXCLUDED.buy_price, | |
| nifty_on_trade_day = EXCLUDED.nifty_on_trade_day; | |
| """, | |
| (owner_id, trade_date, symbol, delta_qty, avg_cost, nifty_price) | |
| ) | |
| trades_inserted += 1 | |
| print(f"[db] Actual trade: {symbol} +{delta_qty:.2f} @ Rs {avg_cost:.2f}") | |
| conn.commit() | |
| print(f"[db] Saved {trades_inserted} actual trade(s) for {trade_date}.") | |
| return trades_inserted | |
| except Exception as e: | |
| conn.rollback() | |
| print(f"[db] detect_and_save_actual_trades failed: {e}") | |
| return 0 | |
| finally: | |
| conn.close() | |
| def load_actual_trades_from_db(owner_id) -> list: | |
| """Returns all actual trades for the owner, newest first.""" | |
| conn = get_db_connection() | |
| try: | |
| with conn.cursor() as cur: | |
| cur.execute( | |
| """ | |
| SELECT trade_date::text, symbol, qty_bought, buy_price, nifty_on_trade_day | |
| FROM actual_trades | |
| WHERE user_id = %s | |
| ORDER BY trade_date DESC, symbol ASC; | |
| """, | |
| (owner_id,) | |
| ) | |
| rows = cur.fetchall() | |
| return [ | |
| { | |
| "trade_date": r[0], | |
| "symbol": r[1], | |
| "qty_bought": float(r[2]), | |
| "buy_price": float(r[3]), | |
| "nifty_on_trade_day": float(r[4]) if r[4] is not None else None, | |
| } | |
| for r in rows | |
| ] | |
| except Exception as e: | |
| print(f"[db] load_actual_trades_from_db failed: {e}") | |
| return [] | |
| finally: | |
| conn.close() | |
| def get_ai_suggestions_for_date(owner_id, trade_date: str) -> list: | |
| """Returns AI BUY signals from the briefing on trade_date for comparison table.""" | |
| conn = get_db_connection() | |
| try: | |
| with conn.cursor() as cur: | |
| cur.execute( | |
| """ | |
| SELECT s.symbol, s.signal, s.price_on_day | |
| FROM stock_signals s | |
| JOIN daily_briefings db ON s.briefing_id = db.id | |
| WHERE db.user_id = %s AND db.date = %s | |
| ORDER BY s.signal, s.symbol; | |
| """, | |
| (owner_id, trade_date) | |
| ) | |
| rows = cur.fetchall() | |
| return [ | |
| {"symbol": r[0], "signal": r[1], "price_on_day": float(r[2])} | |
| for r in rows | |
| ] | |
| except Exception as e: | |
| print(f"[db] get_ai_suggestions_for_date failed: {e}") | |
| return [] | |
| finally: | |
| conn.close() | |
| ''' | |
| with open(DB_PATH, "r", encoding="utf-8") as f: | |
| existing = f.read() | |
| # Only append if functions not already present | |
| if "def snapshot_holdings" not in existing: | |
| with open(DB_PATH, "a", encoding="utf-8") as f: | |
| f.write(APPEND) | |
| print(f"SUCCESS: Appended new functions to {DB_PATH}") | |
| else: | |
| print("SKIPPED: Functions already present in db.py") | |