First commit
This commit is contained in:
235
runner.py
Normal file
235
runner.py
Normal 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()
|
||||
Reference in New Issue
Block a user