236 lines
8.6 KiB
Python
236 lines
8.6 KiB
Python
|
|
"""
|
||
|
|
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()
|