First commit

This commit is contained in:
Henrik Jess Nielsen
2026-05-26 22:21:27 +02:00
parent 2743a236b2
commit 05eed51e7d
90 changed files with 8690 additions and 0 deletions

235
runner.py Normal file
View File

@@ -0,0 +1,235 @@
"""
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()