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 = """ + +
+ + +| Metric | +Value | +Note | +
|---|---|---|
| Model | claude-haiku-4-5 | Anthropic |
| 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 }} |