""" report.py — Daily P&L report for MoneyMaker dry run. Usage: python report.py # full P&L report python report.py --fees # fee drag analysis only python report.py --signals # signal accuracy summary only """ import sys import time import json import argparse from datetime import datetime, timezone from pathlib import Path import yfinance as yf from db import get_conn, DB_TYPE C25_PATH = Path(__file__).parent / "c25.json" _c25_raw = json.loads(C25_PATH.read_text()) C25: dict[str, dict] = {k: v for k, v in _c25_raw.items() if not k.startswith("_")} CAPITAL = 10_000 # DKK — must match portfolio.py def _c25_day_return() -> float | None: """Fetch today's return for OMX C25 CAP index from Yahoo Finance.""" try: info = yf.Ticker("^OMXC25").fast_info prev = info.get("previousClose") or info.get("regularMarketPreviousClose") last = info.get("lastPrice") or info.get("regularMarketPrice") if prev and last and prev > 0: return (last - prev) / prev * 100 except Exception: pass return None def _unrealised_pnl(db) -> tuple[float, list[dict]]: """ Calculate unrealised P&L for all open positions. Returns (total_unrealised_dkk, details_list). """ positions = db.execute("SELECT * FROM positions").fetchall() details = [] total_unreal = 0.0 for p in positions: ticker = p["ticker"] shares = float(p["shares"]) entry = float(p["entry_price"]) entry_date = p["entry_date"] yf_ticker = C25.get(ticker, {}).get("ticker_yahoo", ticker + ".CO") try: last = yf.Ticker(yf_ticker).fast_info.get("lastPrice") or entry except Exception: last = entry cost = shares * entry value = shares * last unreal = value - cost pct = (unreal / cost * 100) if cost else 0 total_unreal += unreal details.append({ "ticker": ticker, "shares": shares, "entry": entry, "last": last, "cost": cost, "value": value, "unreal": unreal, "pct": pct, "entry_date": entry_date, }) return total_unreal, details def _realised_pnl(db) -> float: """Sum all realised P&L from position_events.""" row = db.execute( "SELECT COALESCE(SUM(pnl_dkk),0) AS total FROM position_events WHERE action='sell'" ).fetchone() return float(row["total"] or 0) def _total_fees(db) -> float: """Sum all fees from saxo_orders.""" try: row = db.execute( "SELECT COALESCE(SUM(fee_dkk),0) AS total FROM saxo_orders" ).fetchone() return float(row["total"] or 0) except Exception: return 0.0 def _invested(positions_details: list[dict]) -> float: return sum(p["cost"] for p in positions_details) def _signal_accuracy(db) -> dict: """ Calculate signal accuracy from completed trades (position_events). A signal is 'correct' if the trade had P&L > 0 (signal_correct=1). Also returns overall signal scan stats from article_signals. """ # Accuracy from closed trades try: row = db.execute(""" SELECT COUNT(*) AS total_trades, SUM(CASE WHEN signal_correct=1 THEN 1 ELSE 0 END) AS correct, SUM(CASE WHEN signal_correct=0 THEN 1 ELSE 0 END) AS wrong, COALESCE(AVG(CASE WHEN signal_correct IS NOT NULL THEN signal_correct END), -1) AS accuracy FROM position_events WHERE action = 'sell' AND signal_correct IS NOT NULL """).fetchone() total_trades = int(row["total_trades"] or 0) correct = int(row["correct"] or 0) wrong = int(row["wrong"] or 0) accuracy_pct = (float(row["accuracy"]) * 100) if total_trades > 0 else None except Exception: total_trades = correct = wrong = 0 accuracy_pct = None # NLP scan stats try: srow = db.execute(""" SELECT COUNT(*) AS total_signals, SUM(CASE WHEN alert=1 THEN 1 ELSE 0 END) AS alerts, AVG(signal_score) AS avg_score FROM article_signals WHERE signal_score IS NOT NULL """).fetchone() return { "total": int(srow["total_signals"] or 0), "alerts": int(srow["alerts"] or 0), "avg_score": round(float(srow["avg_score"] or 0), 3), "total_trades": total_trades, "correct": correct, "wrong": wrong, "accuracy_pct": accuracy_pct, } except Exception: return { "total": 0, "alerts": 0, "avg_score": 0, "total_trades": total_trades, "correct": correct, "wrong": wrong, "accuracy_pct": accuracy_pct, } def print_report() -> None: db = get_conn() now = datetime.now(timezone.utc).strftime("%d %b %Y %H:%M UTC") unreal_total, positions = _unrealised_pnl(db) realised = _realised_pnl(db) fees = _total_fees(db) invested = _invested(positions) cash = CAPITAL - invested net_pnl = realised + unreal_total - fees net_pct = (net_pnl / CAPITAL * 100) c25_ret = _c25_day_return() vs_bench = (net_pct - c25_ret) if c25_ret is not None else None sig_stats = _signal_accuracy(db) # ── Header ────────────────────────────────────────────────────────────── print() print("═" * 48) print(f" MONEYMAKER P&L · {now}") print(f" DB: {DB_TYPE}") print("═" * 48) print(f" Kapital: {CAPITAL:>10,.0f} DKK") print(f" Investeret: {invested:>10,.0f} DKK ({len(positions)} pos.)") print(f" Kontant: {cash:>10,.0f} DKK") print() print(f" Urealiseret: {unreal_total:>+10,.0f} DKK") print(f" Realiseret: {realised:>+10,.0f} DKK") print(f" Total gebyrer: {-fees:>+8,.0f} DKK") print(f" Net P&L: {net_pnl:>+10,.0f} DKK ({net_pct:+.2f}%)") print() # ── Benchmark ──────────────────────────────────────────────────────────── if c25_ret is not None: bench_icon = "✅" if vs_bench and vs_bench >= 0 else "❌" print(f" C25 index: {c25_ret:+.2f}% (i dag)") print(f" vs benchmark: {vs_bench:+.2f}% {bench_icon}") else: print(" C25 index: N/A (marked lukket?)") print() # ── Open positions ──────────────────────────────────────────────────────── if positions: print(" ┌─ Åbne positioner ─────────────────────────────┐") for p in positions: icon = "📈" if p["unreal"] >= 0 else "📉" print( f" │ {icon} {p['ticker']:<8} {p['shares']:.0f} stk " f"købt {p['entry']:.0f} nu {p['last']:.0f} " f"P&L {p['unreal']:+.0f} ({p['pct']:+.1f}%)" ) print(" └────────────────────────────────────────────────┘") print() # ── Signal stats ───────────────────────────────────────────────────────── print(f" Signaler: {sig_stats['total']:>5} analyseret") print(f" Alerts: {sig_stats['alerts']:>5} ⚡ triggers") print(f" Gns score: {sig_stats['avg_score']:>5.3f}") if sig_stats["total_trades"] > 0: acc = sig_stats["accuracy_pct"] icon = "✅" if acc and acc >= 50 else "❌" print(f" Lukkede handler: {sig_stats['total_trades']} " f"({sig_stats['correct']} korrekte / {sig_stats['wrong']} forkerte) " f"→ {acc:.0f}% {icon}") else: print(" Lukkede handler: 0 (ingen salg endnu)") if fees > 0: drag_pct = (fees / max(abs(net_pnl + fees), 1)) * 100 print(f" Gebyr-drag: {drag_pct:.1f}% af brutto P&L") print("═" * 48) print() db.close() def main(): parser = argparse.ArgumentParser(description="MoneyMaker P&L rapport") parser.add_argument("--fees", action="store_true") parser.add_argument("--signals", action="store_true") args = parser.parse_args() print_report() if __name__ == "__main__": main()