diff --git a/analyze.py b/analyze.py index 9492ca1..5f5aa76 100644 --- a/analyze.py +++ b/analyze.py @@ -85,9 +85,9 @@ ALERT_THRESHOLD = 0.35 # signal_score > this → alert 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 +# Pricing: Claude 3 Haiku — https://www.anthropic.com/pricing +_PRICE_INPUT_PER_TOKEN = 0.25 / 1_000_000 # $0.25 per MTok +_PRICE_OUTPUT_PER_TOKEN = 1.25 / 1_000_000 # $1.25 per MTok def calc_cost(input_tokens: int, output_tokens: int) -> float: @@ -270,7 +270,7 @@ Fields: try: msg = client.messages.create( - model="claude-haiku-4-5", + model="claude-3-haiku-20240307", max_tokens=256, messages=[{"role": "user", "content": prompt}], ) @@ -372,6 +372,7 @@ def analyze_articles( dry_run: bool = False, use_claude: bool = True, auto_fetch: bool = True, + max_claude_calls: int = 50, ) -> None: db = get_db() migrate_db(db) @@ -486,6 +487,16 @@ def analyze_articles( now = int(time.time()) signals_written = 0 alerts_triggered = 0 + claude_calls_this_run = 0 + + # Daily spend guard — read current metrics before starting + _existing = {} + if METRICS_FILE.exists(): + try: + _existing = json.loads(METRICS_FILE.read_text()) + except Exception: + pass + DAILY_COST_CAP = float(os.getenv("CLAUDE_DAILY_CAP_USD", "2.0")) for row, matches, cov, full_text in final: slug = row["slug"] @@ -507,10 +518,19 @@ 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, _in_tok, _out_tok = claude_extract(title, full_text, list(matches.keys())) - if _in_tok: - update_metrics(_in_tok, _out_tok) + if claude_calls_this_run >= max_claude_calls: + print(f" [claude] ⚠️ per-run cap ({max_claude_calls}) reached — skipping remaining articles") + use_claude = False + elif _existing.get("total_cost_usd", 0.0) >= DAILY_COST_CAP: + print(f" [claude] ⚠️ daily spend cap ${DAILY_COST_CAP} reached — skipping Claude for remaining articles") + use_claude = False + else: + print(f" [claude] {slug[:50]}") + claude_data, _in_tok, _out_tok = claude_extract(title, full_text, list(matches.keys())) + if _in_tok: + update_metrics(_in_tok, _out_tok) + _existing["total_cost_usd"] = _existing.get("total_cost_usd", 0.0) + calc_cost(_in_tok, _out_tok) + claude_calls_this_run += 1 # ------------------------------------------------------------------ # Phase 6 — yfinance momentum + scoring @@ -587,9 +607,28 @@ def analyze_articles( def main() -> None: import sys - force = "--force" in sys.argv - dry_run = "--dry" in sys.argv - analyze_articles(force=force, dry_run=dry_run) + force = "--force" in sys.argv + dry_run = "--dry-run" in sys.argv or "--dry" in sys.argv + no_claude = "--no-claude" in sys.argv + + # --max-claude=N override per-run cap + max_calls = 50 + for arg in sys.argv: + if arg.startswith("--max-claude="): + try: + max_calls = int(arg.split("=", 1)[1]) + except ValueError: + pass + + if force: + print(f"[analyze] ⚠️ --force mode: re-analyzing ALL articles (claude cap={max_calls})") + + analyze_articles( + force=force, + dry_run=dry_run, + use_claude=not no_claude, + max_claude_calls=max_calls, + ) if __name__ == "__main__": diff --git a/dashboard.py b/dashboard.py index c3c508c..029e599 100644 --- a/dashboard.py +++ b/dashboard.py @@ -31,11 +31,6 @@ 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 ──────────────────────────────────────────────── @@ -246,9 +241,9 @@ METRICS_TEMPLATE = """
-