Compare commits

...

2 Commits

Author SHA1 Message Date
Henrik Jess Nielsen
026b470b31 Fix metrics.json path to persistent data volume (/app/data)
All checks were successful
Build and Deploy MoneyMaker / build-and-deploy (push) Successful in 12m21s
2026-05-28 13:42:36 +02:00
Henrik Jess Nielsen
6a407cf216 Add /metrics + /metrics-dash endpoints, Claude cost tracking and safety caps
- Track Claude token usage + cost in metrics.json after each call
- Add /metrics JSON endpoint
- Add /metrics-dash visual dashboard (KPIs, charts, burn bars)
- Switch model to claude-3-haiku-20240307 (3.2x cheaper)
- Add per-run cap (50 calls) and daily spend cap ($2.00, env: CLAUDE_DAILY_CAP_USD)
2026-05-28 13:41:53 +02:00
2 changed files with 56 additions and 22 deletions

View File

@@ -83,11 +83,11 @@ ALERT_THRESHOLD = 0.35 # signal_score > this → alert
# Claude metrics # Claude metrics
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
METRICS_FILE = Path(__file__).parent / "metrics.json" METRICS_FILE = Path(os.getenv("DATA_DIR", str(Path(__file__).parent / "data"))) / "metrics.json"
# Pricing: Claude Haiku 4.5 — https://www.anthropic.com/pricing # Pricing: Claude 3 Haiku — https://www.anthropic.com/pricing
_PRICE_INPUT_PER_TOKEN = 0.80 / 1_000_000 # $0.80 per MTok _PRICE_INPUT_PER_TOKEN = 0.25 / 1_000_000 # $0.25 per MTok
_PRICE_OUTPUT_PER_TOKEN = 4.00 / 1_000_000 # $4.00 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: def calc_cost(input_tokens: int, output_tokens: int) -> float:
@@ -270,7 +270,7 @@ Fields:
try: try:
msg = client.messages.create( msg = client.messages.create(
model="claude-haiku-4-5", model="claude-3-haiku-20240307",
max_tokens=256, max_tokens=256,
messages=[{"role": "user", "content": prompt}], messages=[{"role": "user", "content": prompt}],
) )
@@ -372,6 +372,7 @@ def analyze_articles(
dry_run: bool = False, dry_run: bool = False,
use_claude: bool = True, use_claude: bool = True,
auto_fetch: bool = True, auto_fetch: bool = True,
max_claude_calls: int = 50,
) -> None: ) -> None:
db = get_db() db = get_db()
migrate_db(db) migrate_db(db)
@@ -486,6 +487,16 @@ def analyze_articles(
now = int(time.time()) now = int(time.time())
signals_written = 0 signals_written = 0
alerts_triggered = 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: for row, matches, cov, full_text in final:
slug = row["slug"] slug = row["slug"]
@@ -507,10 +518,19 @@ def analyze_articles(
# ------------------------------------------------------------------ # ------------------------------------------------------------------
claude_data: dict = {} claude_data: dict = {}
if use_claude and not dry_run and os.environ.get("ANTHROPIC_API_KEY"): if use_claude and not dry_run and os.environ.get("ANTHROPIC_API_KEY"):
print(f" [claude] {slug[:50]}") if claude_calls_this_run >= max_claude_calls:
claude_data, _in_tok, _out_tok = claude_extract(title, full_text, list(matches.keys())) print(f" [claude] ⚠️ per-run cap ({max_claude_calls}) reached — skipping remaining articles")
if _in_tok: use_claude = False
update_metrics(_in_tok, _out_tok) 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 # Phase 6 — yfinance momentum + scoring
@@ -587,9 +607,28 @@ def analyze_articles(
def main() -> None: def main() -> None:
import sys import sys
force = "--force" in sys.argv force = "--force" in sys.argv
dry_run = "--dry" in sys.argv dry_run = "--dry-run" in sys.argv or "--dry" in sys.argv
analyze_articles(force=force, dry_run=dry_run) 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__": if __name__ == "__main__":

View File

@@ -28,12 +28,7 @@ from report import _c25_day_return, _unrealised_pnl, _realised_pnl, _total_fees,
CAPITAL = 10_000 CAPITAL = 10_000
LOG_DIR = Path(os.getenv("LOG_DIR", str(Path(__file__).parent / "logs"))) LOG_DIR = Path(os.getenv("LOG_DIR", str(Path(__file__).parent / "logs")))
METRICS_FILE = Path(__file__).parent / "metrics.json" METRICS_FILE = Path(os.getenv("DATA_DIR", str(Path(__file__).parent / "data"))) / "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 REFRESH = 60 # seconds
app = Flask(__name__) app = Flask(__name__)
@@ -246,9 +241,9 @@ METRICS_TEMPLATE = """<!DOCTYPE html>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>Model</td><td class="mono">claude-haiku-4-5</td><td>Anthropic</td></tr> <tr><td>Model</td><td class="mono">claude-3-haiku-20240307</td><td>Anthropic</td></tr>
<tr><td>Input price</td><td class="mono">$0.80 / MTok</td><td>${{ "%.6f"|format(0.80/1_000_000) }} per token</td></tr> <tr><td>Input price</td><td class="mono">$0.25 / MTok</td><td>${{ "%.6f"|format(0.25/1_000_000) }} per token</td></tr>
<tr><td>Output price</td><td class="mono">$4.00 / MTok</td><td>${{ "%.6f"|format(4.00/1_000_000) }} per token</td></tr> <tr><td>Output price</td><td class="mono">$1.25 / MTok</td><td>${{ "%.6f"|format(1.25/1_000_000) }} per token</td></tr>
<tr><td>Total calls</td><td class="mono">{{ total_calls }}</td><td>articles sent to Claude</td></tr> <tr><td>Total calls</td><td class="mono">{{ total_calls }}</td><td>articles sent to Claude</td></tr>
<tr><td>Total input tokens</td><td class="mono">{{ "{:,}".format(total_input_tokens) }}</td><td>cost ${{ "%.5f"|format(cost_input) }}</td></tr> <tr><td>Total input tokens</td><td class="mono">{{ "{:,}".format(total_input_tokens) }}</td><td>cost ${{ "%.5f"|format(cost_input) }}</td></tr>
<tr><td>Total output tokens</td><td class="mono">{{ "{:,}".format(total_output_tokens) }}</td><td>cost ${{ "%.5f"|format(cost_output) }}</td></tr> <tr><td>Total output tokens</td><td class="mono">{{ "{:,}".format(total_output_tokens) }}</td><td>cost ${{ "%.5f"|format(cost_output) }}</td></tr>
@@ -298,7 +293,7 @@ window.addEventListener('resize', () => { barChart.resize(); donut.resize(); });
</script> </script>
{% endif %} {% endif %}
<div class="footer">MoneyMaker · claude-haiku-4-5 · metrics updated on each analyze run</div> <div class="footer">MoneyMaker · claude-3-haiku-20240307 · metrics updated on each analyze run</div>
</body> </body>
</html> </html>
""" """