First commit
This commit is contained in:
241
report.py
Normal file
241
report.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user