From 2294f5bc072d957e47948ca95c2fb835b88f08e7 Mon Sep 17 00:00:00 2001 From: Henrik Jess Nielsen Date: Thu, 28 May 2026 13:11:29 +0200 Subject: [PATCH] Metrics --- analyze.py | 46 ++++++- dashboard.py | 340 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 377 insertions(+), 9 deletions(-) diff --git a/analyze.py b/analyze.py index 6313121..9492ca1 100644 --- a/analyze.py +++ b/analyze.py @@ -29,6 +29,7 @@ import math import sqlite3 import logging import warnings +from datetime import datetime from pathlib import Path # Silence transformer noise before importing @@ -78,6 +79,37 @@ MIN_COVERAGE_SPREAD = 0.0 # disabled: signal_score naturally zeros out single- FINBERT_MIN_CONF = 0.70 # drop neutral articles below this FinBERT confidence ALERT_THRESHOLD = 0.35 # signal_score > this → alert +# --------------------------------------------------------------------------- +# Claude metrics +# --------------------------------------------------------------------------- + +METRICS_FILE = Path(__file__).parent / "metrics.json" + +# Pricing: Claude Haiku 4.5 — https://www.anthropic.com/pricing +_PRICE_INPUT_PER_TOKEN = 0.80 / 1_000_000 # $0.80 per MTok +_PRICE_OUTPUT_PER_TOKEN = 4.00 / 1_000_000 # $4.00 per MTok + + +def calc_cost(input_tokens: int, output_tokens: int) -> float: + return round(input_tokens * _PRICE_INPUT_PER_TOKEN + output_tokens * _PRICE_OUTPUT_PER_TOKEN, 6) + + +def update_metrics(input_tokens: int, output_tokens: int) -> None: + """Accumulate Claude token usage into metrics.json.""" + cost = calc_cost(input_tokens, output_tokens) + data: dict = {} + if METRICS_FILE.exists(): + try: + data = json.loads(METRICS_FILE.read_text()) + except Exception: + pass + data["total_calls"] = data.get("total_calls", 0) + 1 + data["total_input_tokens"] = data.get("total_input_tokens", 0) + input_tokens + data["total_output_tokens"] = data.get("total_output_tokens", 0) + output_tokens + data["total_cost_usd"] = round(data.get("total_cost_usd", 0.0) + cost, 6) + data["last_updated"] = datetime.now().isoformat(timespec="seconds") + METRICS_FILE.write_text(json.dumps(data, indent=2)) + # --------------------------------------------------------------------------- # Model loading @@ -195,16 +227,16 @@ def coverage_spread_score(row) -> float: # Claude structured extraction # --------------------------------------------------------------------------- -def claude_extract(title: str, text: str, tickers: list[str]) -> dict: +def claude_extract(title: str, text: str, tickers: list[str]) -> tuple[dict, int, int]: """ Use Claude Haiku to extract structured financial signal. - Returns {"confirmed_tickers", "magnitude", "timeframe", "reasoning"}. + Returns ({"confirmed_tickers", "magnitude", "timeframe", "reasoning"}, input_tokens, output_tokens). """ import anthropic api_key = os.environ.get("ANTHROPIC_API_KEY") if not api_key: - return {"confirmed_tickers": tickers, "magnitude": 5, "timeframe": "days", "reasoning": "(no API key)"} + return {"confirmed_tickers": tickers, "magnitude": 5, "timeframe": "days", "reasoning": "(no API key)"}, 0, 0 client = anthropic.Anthropic(api_key=api_key) ticker_ctx = "\n".join( @@ -245,10 +277,10 @@ Fields: raw = msg.content[0].text.strip() raw = re.sub(r"^```(?:json)?\n?", "", raw) raw = re.sub(r"\n?```$", "", raw) - return json.loads(raw) + return json.loads(raw), msg.usage.input_tokens, msg.usage.output_tokens except Exception as e: print(f" [warn] Claude failed: {e}") - return {"confirmed_tickers": tickers, "magnitude": 5, "timeframe": "days", "reasoning": str(e)[:120]} + return {"confirmed_tickers": tickers, "magnitude": 5, "timeframe": "days", "reasoning": str(e)[:120]}, 0, 0 # --------------------------------------------------------------------------- @@ -476,7 +508,9 @@ def analyze_articles( claude_data: dict = {} if use_claude and not dry_run and os.environ.get("ANTHROPIC_API_KEY"): print(f" [claude] {slug[:50]}") - claude_data = claude_extract(title, full_text, list(matches.keys())) + claude_data, _in_tok, _out_tok = claude_extract(title, full_text, list(matches.keys())) + if _in_tok: + update_metrics(_in_tok, _out_tok) # ------------------------------------------------------------------ # Phase 6 — yfinance momentum + scoring diff --git a/dashboard.py b/dashboard.py index cdc0178..c3c508c 100644 --- a/dashboard.py +++ b/dashboard.py @@ -26,12 +26,283 @@ from flask import Flask, jsonify, render_template_string from db import get_conn, DB_TYPE from report import _c25_day_return, _unrealised_pnl, _realised_pnl, _total_fees, _signal_accuracy -CAPITAL = 10_000 -LOG_DIR = Path(os.getenv("LOG_DIR", str(Path(__file__).parent / "logs"))) -REFRESH = 60 # seconds +CAPITAL = 10_000 +LOG_DIR = Path(os.getenv("LOG_DIR", str(Path(__file__).parent / "logs"))) +METRICS_FILE = Path(__file__).parent / "metrics.json" +REFRESH = 60 # seconds + +CAPITAL = 10_000 +LOG_DIR = Path(os.getenv("LOG_DIR", str(Path(__file__).parent / "logs"))) +METRICS_FILE = Path(__file__).parent / "metrics.json" +REFRESH = 60 # seconds app = Flask(__name__) +# ── Metrics dashboard template ──────────────────────────────────────────────── + +METRICS_TEMPLATE = """ + + + + + MoneyMaker · AI Costs + + + + + + + +
+
+

MoneyMaker

+ AI Costs +
+
+ {{ now }}
+ ← Dashboard +
+
+ +{% if no_data %} +
+
+
📊
+
Ingen data endnu — kør analyze.py med Claude for at begynde at tracke forbrug.
+
+
+{% else %} + +
+
+
Total Cost
+
${{ "%.4f"|format(total_cost_usd) }}
+
USD spent
+
+
+
Claude Calls
+
{{ total_calls }}
+
articles analyzed
+
+
+
Input Tokens
+
{{ "{:,}".format(total_input_tokens) }}
+
${{ "%.4f"|format(cost_input) }} · {{ input_pct }}%
+
+
+
Output Tokens
+
{{ "{:,}".format(total_output_tokens) }}
+
${{ "%.4f"|format(cost_output) }} · {{ output_pct }}%
+
+
+
Cost / Article
+
${{ "%.5f"|format(cost_per_call) }}
+
avg per Claude call
+
+
+ +
+
+
Token Distribution — Input vs Output
+
+
+
+
Cost Split
+
+
+
+ +
+
Token Breakdown
+
+
Input tokens
+
+
{{ "{:,}".format(total_input_tokens) }}
+
+
+
Output tokens
+
+
{{ "{:,}".format(total_output_tokens) }}
+
+
+
Total tokens
+
+
{{ "{:,}".format(total_input_tokens + total_output_tokens) }}
+
+
+ +
+
Model Details
+ + + + + + + + + + + + + + + + + + +
MetricValueNote
Modelclaude-haiku-4-5Anthropic
Input price$0.80 / MTok${{ "%.6f"|format(0.80/1_000_000) }} per token
Output price$4.00 / MTok${{ "%.6f"|format(4.00/1_000_000) }} per token
Total calls{{ total_calls }}articles sent to Claude
Total input tokens{{ "{:,}".format(total_input_tokens) }}cost ${{ "%.5f"|format(cost_input) }}
Total output tokens{{ "{:,}".format(total_output_tokens) }}cost ${{ "%.5f"|format(cost_output) }}
Total cost${{ "%.6f"|format(total_cost_usd) }}cumulative
Last updated{{ last_updated }}
+
+ + +{% endif %} + + + + +""" + # ── HTML template ───────────────────────────────────────────────────────────── TEMPLATE = """ @@ -762,6 +1033,69 @@ def health(): return {"status": "ok", "db": DB_TYPE, "ts": time.time()} +@app.route("/metrics") +def metrics(): + data: dict = {} + if METRICS_FILE.exists(): + try: + data = json.loads(METRICS_FILE.read_text()) + except Exception: + pass + + # Total articles with Claude signal from DB + try: + db = get_conn() + row = db.execute( + "SELECT COUNT(DISTINCT article_slug) AS cnt FROM article_signals WHERE claude_reasoning != '' AND claude_reasoning IS NOT NULL" + ).fetchone() + data["total_articles_with_claude"] = row[0] if row else 0 + db.close() + except Exception: + pass + + return jsonify(data) + + +@app.route("/metrics-dash") +def metrics_dash(): + data: dict = {} + if METRICS_FILE.exists(): + try: + data = json.loads(METRICS_FILE.read_text()) + except Exception: + pass + + total_calls = data.get("total_calls", 0) + total_input_tokens = data.get("total_input_tokens", 0) + total_output_tokens = data.get("total_output_tokens", 0) + total_cost_usd = data.get("total_cost_usd", 0.0) + last_updated = data.get("last_updated", "—") + + total_tokens = total_input_tokens + total_output_tokens + input_pct = round(total_input_tokens / total_tokens * 100) if total_tokens else 0 + output_pct = round(total_output_tokens / total_tokens * 100) if total_tokens else 0 + + cost_input = round(total_input_tokens * 0.80 / 1_000_000, 6) + cost_output = round(total_output_tokens * 4.00 / 1_000_000, 6) + cost_per_call = round(total_cost_usd / total_calls, 6) if total_calls else 0.0 + + return render_template_string( + METRICS_TEMPLATE, + no_data=total_calls == 0, + now=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"), + total_calls=total_calls, + total_input_tokens=total_input_tokens, + total_output_tokens=total_output_tokens, + total_cost_usd=total_cost_usd, + last_updated=last_updated, + input_pct=input_pct, + output_pct=output_pct, + cost_input=cost_input, + cost_output=cost_output, + cost_per_call=cost_per_call, + ) + + # ── Main ────────────────────────────────────────────────────────────────────── def main():