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

369
portfolio.py Normal file
View File

@@ -0,0 +1,369 @@
"""
portfolio.py — Position tracker + ordre-forslag
Kommandoer:
python portfolio.py orders → dagens køb/sælg/hold forslag
python portfolio.py status → åbne positioner + stop/take
python portfolio.py buy TICKER N PRIS → registrer et køb
python portfolio.py sell TICKER PRIS → registrer et salg
Eksempel:
python portfolio.py buy VWS 11 195.00
python portfolio.py sell VWS 244.00
"""
import sys
import json
import time
import sqlite3
from pathlib import Path
from datetime import datetime, timezone
import yfinance as yf
from db import get_conn, DBConn
# ---------------------------------------------------------------------------
# Config — justér disse
# ---------------------------------------------------------------------------
CAPITAL = 10_000 # DKK total kapital
MAX_POSITIONS = 3 # maks åbne positioner ad gangen
STOP_LOSS_PCT = 0.08 # sælg hvis -8% fra indgang
TAKE_PROFIT_PCT = 0.20 # sælg hvis +20% fra indgang
MIN_SIGNAL = 0.35 # minimum signal_score for KØB
BUY_ANALYST = {"stærk køb", "køb"} # dansk label fra analyst_rec()
C25_PATH = Path(__file__).parent / "c25.json"
# ---------------------------------------------------------------------------
# DB setup
# ---------------------------------------------------------------------------
def get_db() -> DBConn:
"""Return a DBConn wrapper. Schema is managed by db.py."""
return get_conn()
# ---------------------------------------------------------------------------
# Hjælpere
# ---------------------------------------------------------------------------
def _c25_map() -> dict:
data = json.loads(C25_PATH.read_text())
# c25.json er en dict med ticker som nøgle (plus _meta)
return {k: v for k, v in data.items() if k != "_meta"}
def _current_price(ticker_yahoo: str) -> float | None:
try:
info = yf.Ticker(ticker_yahoo).fast_info
return round(float(info.last_price), 2)
except Exception:
return None
def _analyst_label(ticker_yahoo: str) -> tuple[str, int]:
"""Returnerer (dansk label, antal analytikere)."""
try:
info = yf.Ticker(ticker_yahoo).info
rec = (info.get("recommendationKey") or "").lower()
n = info.get("numberOfAnalystOpinions") or 0
label_map = {
"strong_buy": "stærk køb", "buy": "køb",
"hold": "hold", "sell": "sælg", "strong_sell": "stærk sælg",
}
return label_map.get(rec, rec or "ukendt"), int(n)
except Exception:
return "ukendt", 0
def _open_positions(db: DBConn) -> list:
return db.execute("SELECT * FROM positions ORDER BY entry_date").fetchall()
def _open_count(db: DBConn) -> int:
return db.execute("SELECT COUNT(*) AS cnt FROM positions").fetchone()["cnt"]
def _best_signals(db: DBConn) -> list:
"""Hent de bedste aktuelle signal-rækker (senest 7 dage)."""
cutoff = int(time.time()) - 7 * 86400
return db.execute("""
SELECT ticker, MAX(company_name) AS company_name,
MAX(signal_score) AS signal_score,
AVG(sentiment_score) AS sentiment_score,
SUM(CASE WHEN sentiment = 'positive' THEN 1 ELSE 0 END) AS pos_count,
SUM(CASE WHEN sentiment = 'negative' THEN 1 ELSE 0 END) AS neg_count,
COUNT(*) AS article_count,
MAX(momentum_dir) AS momentum_dir,
MAX(claude_magnitude) AS magnitude
FROM article_signals
WHERE analyzed_at > ?
GROUP BY ticker
ORDER BY signal_score DESC
""", (cutoff,)).fetchall()
# ---------------------------------------------------------------------------
# Ordre-logik
# ---------------------------------------------------------------------------
def _per_position(db: DBConn) -> int:
open_n = _open_count(db)
slots = MAX_POSITIONS - open_n
if slots <= 0:
return 0
# fordel restkapi tal ligeligt
used = sum(
p["shares"] * p["entry_price"]
for p in _open_positions(db)
)
remaining = max(0.0, CAPITAL - used)
return int(remaining // slots)
def evaluate_orders(db: DBConn) -> None:
c25 = _c25_map()
signals = _best_signals(db)
open_pos = {p["ticker"]: p for p in _open_positions(db)}
open_n = len(open_pos)
per_pos = _per_position(db)
used = sum(p["shares"] * p["entry_price"] for p in open_pos.values())
print()
print("" * 66)
print(f" MONEYMAKER ORDRE-FORSLAG · {datetime.now(timezone.utc).strftime('%d %b %Y %H:%M')} UTC")
print(f" Kapital: {CAPITAL:,.0f} kr │ Brugt: {used:,.0f} kr │ "
f"Åbne: {open_n}/{MAX_POSITIONS} │ Pr. pos: {per_pos:,.0f} kr")
print("" * 66)
buy_suggestions = []
hold_suggestions = []
watch_list = []
for row in signals:
ticker = row["ticker"]
meta = c25.get(ticker, {})
yahoo = meta.get("ticker_yahoo", f"{ticker}.CO")
price = _current_price(yahoo)
if price is None:
continue
analyst_lbl, analyst_n = _analyst_label(yahoo)
score = row["signal_score"] or 0.0
pos_ct = row["pos_count"] or 0
neg_ct = row["neg_count"] or 0
net_sentiment = pos_ct - neg_ct
# Er vi allerede inde?
if ticker in open_pos:
hold_suggestions.append((ticker, row, meta, price, analyst_lbl, analyst_n))
continue
# KØB-betingelser
analyst_ok = analyst_lbl in BUY_ANALYST
signal_ok = score >= MIN_SIGNAL
enige = (net_sentiment > 0 and analyst_ok) or (net_sentiment < 0 and analyst_lbl in {"sælg", "stærk sælg"})
if signal_ok and analyst_ok and enige:
buy_suggestions.append((ticker, row, meta, price, analyst_lbl, analyst_n, score))
elif signal_ok or analyst_ok:
watch_list.append((ticker, row, meta, price, analyst_lbl, analyst_n, score))
# --- KØB ---
if buy_suggestions and open_n < MAX_POSITIONS and per_pos > 0:
print()
print(" 🟢 KØB-SIGNALER")
print(" " + "" * 62)
slots_left = MAX_POSITIONS - open_n
for (ticker, row, meta, price, analyst_lbl, analyst_n, score) in buy_suggestions[:slots_left]:
shares = int(per_pos // price)
if shares == 0:
etps = meta.get("leveraged", [])
etp_buy = next((e["ticker"] for e in etps if e.get("direction") == "long"), "")
etp_txt = f" → brug ETP {etp_buy}" if etp_buy else " → pris for høj til direkte køb"
print(f" {ticker:<10} {price:>7,.0f} kr/stk{etp_txt}")
continue
total = shares * price
stop = round(price * (1 - STOP_LOSS_PCT), 2)
take = round(price * (1 + TAKE_PROFIT_PCT), 2)
max_loss = round(shares * (price - stop), 0)
max_gain = round(shares * (take - price), 0)
print(f" {ticker:<10} KØB {shares} stk à {price:,.0f} kr = {total:,.0f} kr")
print(f" Stop-loss: {stop:,.0f} kr (max tab: {max_loss:,.0f} kr)")
print(f" Take-profit: {take:,.0f} kr (max gevinst: {max_gain:,.0f} kr)")
print(f" Signal: {score:.2f} │ Analytikere ({analyst_n}): {analyst_lbl.upper()}")
print(f" Registrer: python portfolio.py buy {ticker} {shares} {price}")
print()
elif not buy_suggestions:
print()
print(" Ingen klare KØB-signaler lige nu.")
# --- ÅBNE POSITIONER (stop/take check) ---
if open_pos:
print()
print(" 📊 ÅBNE POSITIONER")
print(" " + "" * 62)
for ticker, pos in open_pos.items():
meta = c25.get(ticker, {})
yahoo = meta.get("ticker_yahoo", f"{ticker}.CO")
price = _current_price(yahoo) or pos["entry_price"]
entry = pos["entry_price"]
shares = pos["shares"]
pct_chg = (price - entry) / entry * 100
value = shares * price
pnl = shares * (price - entry)
stop_hit = price <= pos["stop_loss"]
take_hit = price >= pos["take_profit"]
action = "🔴 SÆLG (stop-loss!)" if stop_hit else ("🟡 SÆLG (take-profit!)" if take_hit else "⏳ HOLD")
print(f" {ticker:<10} {shares:.0f} stk │ ind: {entry:,.0f} kr │ nu: {price:,.0f} kr │ {pct_chg:+.1f}%")
print(f" Værdi: {value:,.0f} kr │ P&L: {pnl:+,.0f} kr │ {action}")
print(f" Stop: {pos['stop_loss']:,.0f} kr │ Take: {pos['take_profit']:,.0f} kr")
if stop_hit or take_hit:
print(f" Registrer: python portfolio.py sell {ticker} {price:.2f}")
print()
# --- HOLD/WATCH ---
if watch_list:
print()
print(" 🔍 FØLG MED (signal endnu ikke klart nok)")
print(" " + "" * 62)
for (ticker, row, meta, price, analyst_lbl, analyst_n, score) in watch_list[:4]:
why = []
if score < MIN_SIGNAL:
why.append(f"signal {score:.2f} < {MIN_SIGNAL}")
if analyst_lbl not in BUY_ANALYST:
why.append(f"analytikere: {analyst_lbl.upper()}")
print(f" {ticker:<10} {price:>7,.0f} kr │ {', '.join(why)}")
print()
print(" Forklaring:")
print(" Stop-loss = sælg automatisk hvis kurs falder 10% fra dit køb")
print(" Take-profit = sælg hvis kurs stiger 25% — tag gevinsten hjem")
print(" KØB-krav = signal≥0.25 + analytikere siger KØB + begge peger samme vej")
print()
# ---------------------------------------------------------------------------
# Portfolio CRUD
# ---------------------------------------------------------------------------
def cmd_buy(db: DBConn, args: list[str]) -> None:
if len(args) < 3:
print("Brug: python portfolio.py buy TICKER ANTAL PRIS")
print("Eks: python portfolio.py buy VWS 11 195.00")
return
ticker = args[0].upper()
shares = float(args[1])
price = float(args[2])
stop = round(price * (1 - STOP_LOSS_PCT), 2)
take = round(price * (1 + TAKE_PROFIT_PCT), 2)
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
total = shares * price
db.upsert(
"positions", "ticker",
["ticker", "shares", "entry_price", "entry_date", "stop_loss", "take_profit"],
(ticker, shares, price, today, stop, take),
)
db.execute("""
INSERT INTO position_events
(ticker, action, shares, price, total_dkk, event_date)
VALUES (?, 'buy', ?, ?, ?, ?)
""", (ticker, shares, price, total, today))
db.commit()
print(f"✅ KØB registreret: {shares:.0f} stk {ticker} à {price:.2f} kr = {total:.2f} kr")
print(f" Stop-loss: {stop:.2f} kr")
print(f" Take-profit: {take:.2f} kr")
def cmd_sell(db: DBConn, args: list[str]) -> None:
if len(args) < 2:
print("Brug: python portfolio.py sell TICKER PRIS")
print("Eks: python portfolio.py sell VWS 244.00")
return
ticker = args[0].upper()
price = float(args[1])
pos = db.execute("SELECT * FROM positions WHERE ticker = ?", (ticker,)).fetchone()
if not pos:
print(f"{ticker} — ingen åben position fundet")
return
shares = pos["shares"]
entry = pos["entry_price"]
pnl = shares * (price - entry)
total = shares * price
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
signal_correct = 1 if pnl > 0 else 0
db.execute("DELETE FROM positions WHERE ticker = ?", (ticker,))
db.execute("""
INSERT INTO position_events
(ticker, action, shares, price, total_dkk, pnl_dkk, signal_correct, event_date)
VALUES (?, 'sell', ?, ?, ?, ?, ?, ?)
""", (ticker, shares, price, total, pnl, signal_correct, today))
db.commit()
emoji = "🟢" if pnl >= 0 else "🔴"
print(f"{emoji} SALG registreret: {shares:.0f} stk {ticker} à {price:.2f} kr = {total:.2f} kr")
print(f" Købt til: {entry:.2f} kr │ P&L: {pnl:+.2f} kr │ Signal: {'✅ korrekt' if signal_correct else '❌ forkert'}")
def cmd_status(db: DBConn) -> None:
positions = _open_positions(db)
history = db.execute("""
SELECT * FROM position_events ORDER BY event_date DESC LIMIT 10
""").fetchall()
if not positions:
print("\n Ingen åbne positioner.\n")
else:
print("\n ÅBNE POSITIONER:")
for p in positions:
meta = _c25_map().get(p["ticker"], {})
yahoo = meta.get("ticker_yahoo", f"{p['ticker']}.CO")
price = _current_price(yahoo) or p["entry_price"]
pnl = p["shares"] * (price - p["entry_price"])
print(f" {p['ticker']:<10} {p['shares']:.0f} stk ind {p['entry_price']:.2f} nu {price:.2f} P&L {pnl:+.2f} kr")
print(f" Stop {p['stop_loss']:.2f} Take {p['take_profit']:.2f} (købt {p['entry_date']})")
if history:
print("\n SENESTE HANDLER:")
total_pnl = 0.0
for e in history:
pnl_txt = f" P&L {e['pnl_dkk']:+.2f} kr" if e["pnl_dkk"] is not None else ""
print(f" {e['event_date']} {e['action'].upper():<5} {e['ticker']:<10} "
f"{e['shares']:.0f} stk à {e['price']:.2f} kr{pnl_txt}")
if e["pnl_dkk"] is not None:
total_pnl += e["pnl_dkk"]
sold = [e for e in history if e["action"] == "sell"]
if sold:
print(f"\n Samlet realiseret P&L: {total_pnl:+.2f} kr")
print()
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
db = get_db()
args = sys.argv[1:]
cmd = args[0] if args else "orders"
if cmd == "orders":
evaluate_orders(db)
elif cmd == "status":
cmd_status(db)
elif cmd == "buy" and len(args) >= 4:
cmd_buy(db, args[1:])
elif cmd == "sell" and len(args) >= 3:
cmd_sell(db, args[1:])
else:
print(__doc__)
db.close()
if __name__ == "__main__":
main()