""" runner.py — MoneyMaker pipeline orchestrator. Usage: python runner.py # full pipeline (fetch → analyze → evaluate → execute) python runner.py --dry-run # simulate everything, no real orders python runner.py --analyze-only # only fetch news + run NLP, no orders python runner.py --report # print P&L report only Pipeline flow (each run): 1. Fetch Ground News + Danish RSS feeds 2. Run NLP+Claude analysis on new articles 3. Evaluate buy/sell signals vs open positions 4. Execute orders on Saxo SIM (skip if --dry-run) 5. Print status summary Scheduled by cron: 06:00 CET --analyze-only (full NLP before market opens) 09:30 CET (default) trade window 1 12:00 CET (default) trade window 2 14:30 CET (default) trade window 3 16:30 CET (default) trade window 4 19:00 CET --report daily P&L summary """ import sys import argparse import traceback from datetime import datetime, timezone from db import get_conn, DB_TYPE, init_schema from ground_news import fetch_all from rss_feeds import fetch_all_rss from analyze import analyze_articles from portfolio import evaluate_orders, get_db as portfolio_db, _open_positions from report import print_report def run_pipeline(dry_run: bool = False, analyze_only: bool = False) -> None: ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") print(f"\n{'═'*60}") print(f" MONEYMAKER RUNNER · {ts}") print(f" Mode: {'DRY RUN' if dry_run else 'LIVE'} · DB: {DB_TYPE}") print(f"{'═'*60}\n") # ── 1. Ensure schema is up-to-date ────────────────────────────────────── try: init_schema() except Exception as e: print(f"[runner] WARNING: init_schema() failed: {e}") # ── 2. Fetch news ──────────────────────────────────────────────────────── db = get_conn() try: print("[runner] Henter Ground News …") n_gn = fetch_all(db) print(f" +{n_gn} nye artikler fra Ground News") print("[runner] Henter danske RSS feeds …") n_rss = fetch_all_rss(db) print(f" +{n_rss} nye artikler fra RSS feeds") finally: db.close() # ── 3. NLP analysis ─────────────────────────────────────────────────────── print("\n[runner] Kører NLP analyse …") analyze_articles( force=False, dry_run=dry_run, use_claude=True, auto_fetch=False, # already fetched above ) if analyze_only: print("\n[runner] --analyze-only flag sat. Stopper efter NLP.") return # ── 4. Evaluate orders ──────────────────────────────────────────────────── print("\n[runner] Evaluerer ordre-forslag …") pdb = portfolio_db() try: evaluate_orders(pdb) finally: pdb.close() # ── 5. Auto-execute (skip if dry-run) ──────────────────────────────────── if not dry_run: _auto_execute() else: print("\n[runner] DRY RUN — ingen ordre placeret.") print("\n[runner] Pipeline færdig.\n") def _auto_execute() -> None: """ Auto-place orders on Saxo SIM based on portfolio recommendations. 1. AUTO-SELL: close positions that have hit stop-loss or take-profit. 2. AUTO-BUY: open new positions when signal + analyst consensus align. Orders are placed via saxo_broker.place_order() and logged to saxo_orders. Sells record signal_correct in position_events. """ from portfolio import ( CAPITAL, MAX_POSITIONS, MIN_SIGNAL, BUY_ANALYST, _open_positions, _open_count, _best_signals, _per_position, _c25_map, cmd_sell, ) from saxo_broker import place_order, find_uic import yfinance as yf pdb = portfolio_db() c25 = _c25_map() # ── 1. Auto-sell: stop-loss / take-profit ───────────────────────────── for pos in _open_positions(pdb): ticker = pos["ticker"] yf_ticker = c25.get(ticker, {}).get("ticker_yahoo", ticker + ".CO") try: price = yf.Ticker(yf_ticker).fast_info.get("lastPrice") except Exception as e: print(f" SKIP SELL {ticker} — kurs fejl: {e}") continue if not price: continue stop_hit = price <= pos["stop_loss"] take_hit = price >= pos["take_profit"] if stop_hit or take_hit: reason = "stop-loss" if stop_hit else "take-profit" print(f" → SÆLGER {pos['shares']:.0f} stk {ticker} à {price:.2f} kr ({reason})") try: resp = place_order( ticker, pos["shares"], "Sell", price_dkk=price, note=f"runner auto-sell {reason}", ) print(f" Saxo svar: {resp}") except Exception as e: print(f" FEJL ved Saxo ordre: {e}") # Record sell in portfolio DB (sets signal_correct) cmd_sell(pdb, [ticker, str(price)]) # ── 2. Auto-buy: new positions ──────────────────────────────────────── signals = _best_signals(pdb) open_pos = {p["ticker"]: p for p in _open_positions(pdb)} for sig in signals: ticker = sig["ticker"] score = sig["signal_score"] or 0 momentum = sig["momentum_dir"] or "unknown" # Only buy if score is strong + not already in position if score < MIN_SIGNAL: continue if ticker in open_pos: continue if len(open_pos) >= MAX_POSITIONS: break # Check analyst recommendation try: from signals import analyst_rec rec = analyst_rec(ticker) rec_label_lower = rec["label"].lower().replace("🟢", "").strip() if rec_label_lower not in BUY_ANALYST: print(f" SKIP {ticker} — analyst={rec['label']}") continue except Exception as e: print(f" SKIP {ticker} — analyst check failed: {e}") continue # Get current price via yfinance try: yf_ticker = c25.get(ticker, {}).get("ticker_yahoo", ticker + ".CO") price = yf.Ticker(yf_ticker).fast_info.get("lastPrice") if not price: print(f" SKIP {ticker} — ingen kurs fra yfinance") continue except Exception as e: print(f" SKIP {ticker} — kurs fejl: {e}") continue per_pos = _per_position(pdb) if per_pos < 500: print(f" SKIP {ticker} — for lidt kapital: {per_pos} DKK") continue shares = int(per_pos // price) if shares < 1: print(f" SKIP {ticker} — 0 aktier til {price:.2f} DKK/stk") continue print(f" → KØBER {shares} stk {ticker} à ~{price:.2f} DKK (sig={score:.3f})") try: resp = place_order( ticker, shares, "Buy", price_dkk=price, signal_score=score, analyst_rec=rec["label"], note=f"runner auto-buy sig={score:.3f} mom={momentum}", ) print(f" Saxo svar: {resp}") except Exception as e: print(f" FEJL ved ordre: {e}") open_pos[ticker] = {"ticker": ticker} # prevent double-buy in same run pdb.close() def main(): parser = argparse.ArgumentParser(description="MoneyMaker pipeline runner") parser.add_argument("--dry-run", action="store_true", help="Simulate, no real orders") parser.add_argument("--analyze-only", action="store_true", help="Only fetch + NLP, no orders") parser.add_argument("--report", action="store_true", help="Print P&L report and exit") args = parser.parse_args() if args.report: print_report() return try: run_pipeline(dry_run=args.dry_run, analyze_only=args.analyze_only) except KeyboardInterrupt: print("\n[runner] Afbrudt.") except Exception: print("[runner] FEJL:") traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()