Initial commit with translated description

This commit is contained in:
2026-03-29 10:21:46 +08:00
commit 18e90b0b09
67 changed files with 20609 additions and 0 deletions

500
scripts/alerts.py Normal file
View File

@@ -0,0 +1,500 @@
#!/usr/bin/env python3
"""
Price Target Alerts - Track buy zone alerts for stocks.
Features:
- Set price target alerts (buy zone triggers)
- Check alerts against current prices
- Snooze, update, delete alerts
- Multi-currency support (USD, EUR, JPY, SGD, MXN)
Usage:
alerts.py list # Show all alerts
alerts.py set CRWD 400 --note 'Kaufzone' # Set alert
alerts.py check # Check triggered alerts
alerts.py delete CRWD # Delete alert
alerts.py snooze CRWD --days 7 # Snooze for 7 days
alerts.py update CRWD 380 # Update target price
"""
import argparse
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
from utils import ensure_venv
ensure_venv()
# Lazy import to avoid numpy issues at module load
fetch_market_data = None
def get_fetch_market_data():
global fetch_market_data
if fetch_market_data is None:
from fetch_news import fetch_market_data as fmd
fetch_market_data = fmd
return fetch_market_data
SCRIPT_DIR = Path(__file__).parent
CONFIG_DIR = SCRIPT_DIR.parent / "config"
ALERTS_FILE = CONFIG_DIR / "alerts.json"
SUPPORTED_CURRENCIES = ["USD", "EUR", "JPY", "SGD", "MXN"]
def load_alerts() -> dict:
"""Load alerts from JSON file."""
if not ALERTS_FILE.exists():
return {"_meta": {"version": 1, "supported_currencies": SUPPORTED_CURRENCIES}, "alerts": []}
return json.loads(ALERTS_FILE.read_text())
def save_alerts(data: dict) -> None:
"""Save alerts to JSON file."""
data["_meta"]["updated_at"] = datetime.now().isoformat()
ALERTS_FILE.write_text(json.dumps(data, indent=2))
def get_alert_by_ticker(alerts: list, ticker: str) -> dict | None:
"""Find alert by ticker."""
ticker = ticker.upper()
for alert in alerts:
if alert["ticker"] == ticker:
return alert
return None
def format_price(price: float, currency: str) -> str:
"""Format price with currency symbol."""
symbols = {"USD": "$", "EUR": "", "JPY": "¥", "SGD": "S$", "MXN": "MX$"}
symbol = symbols.get(currency, currency + " ")
if currency == "JPY":
return f"{symbol}{price:,.0f}"
return f"{symbol}{price:,.2f}"
def cmd_list(args) -> None:
"""List all alerts."""
data = load_alerts()
alerts = data.get("alerts", [])
if not alerts:
print("📭 No price alerts set")
return
print(f"📊 Price Alerts ({len(alerts)} total)\n")
now = datetime.now()
active = []
snoozed = []
for alert in alerts:
snooze_until = alert.get("snooze_until")
if snooze_until and datetime.fromisoformat(snooze_until) > now:
snoozed.append(alert)
else:
active.append(alert)
if active:
print("### Active Alerts")
for a in active:
target = format_price(a["target_price"], a.get("currency", "USD"))
note = f'"{a["note"]}"' if a.get("note") else ""
user = f" (by {a['set_by']})" if a.get("set_by") else ""
print(f"{a['ticker']}: {target}{note}{user}")
print()
if snoozed:
print("### Snoozed")
for a in snoozed:
target = format_price(a["target_price"], a.get("currency", "USD"))
until = datetime.fromisoformat(a["snooze_until"]).strftime("%Y-%m-%d")
print(f"{a['ticker']}: {target} (until {until})")
print()
def cmd_set(args) -> None:
"""Set a new alert."""
data = load_alerts()
alerts = data.get("alerts", [])
ticker = args.ticker.upper()
# Check if alert exists
existing = get_alert_by_ticker(alerts, ticker)
if existing:
print(f"⚠️ Alert for {ticker} already exists. Use 'update' to change target.")
return
# Validate target price
if args.target <= 0:
print(f"❌ Target price must be greater than 0")
return
currency = args.currency.upper() if args.currency else "USD"
if currency not in SUPPORTED_CURRENCIES:
print(f"❌ Currency {currency} not supported. Use: {', '.join(SUPPORTED_CURRENCIES)}")
return
# Warn about currency mismatch based on ticker suffix
ticker_currency_map = {
".T": "JPY", # Tokyo
".SI": "SGD", # Singapore
".MX": "MXN", # Mexico
".DE": "EUR", ".F": "EUR", ".PA": "EUR", # Europe
}
expected_currency = "USD" # Default for US stocks
for suffix, curr in ticker_currency_map.items():
if ticker.endswith(suffix):
expected_currency = curr
break
if currency != expected_currency:
print(f"⚠️ Warning: {ticker} trades in {expected_currency}, but alert set in {currency}")
# Fetch current price (optional - may fail if numpy broken)
current_price = None
try:
quotes = get_fetch_market_data()([ticker], timeout=10)
if ticker in quotes and quotes[ticker].get("price"):
current_price = quotes[ticker]["price"]
except Exception as e:
print(f"⚠️ Could not fetch current price: {e}", file=sys.stderr)
alert = {
"ticker": ticker,
"target_price": args.target,
"currency": currency,
"note": args.note or "",
"set_by": args.user or "",
"set_date": datetime.now().strftime("%Y-%m-%d"),
"status": "active",
"snooze_until": None,
"triggered_count": 0,
"last_triggered": None,
}
alerts.append(alert)
data["alerts"] = alerts
save_alerts(data)
target_str = format_price(args.target, currency)
print(f"✅ Alert set: {ticker} under {target_str}")
if current_price:
pct_diff = ((current_price - args.target) / current_price) * 100
current_str = format_price(current_price, currency)
print(f" Current: {current_str} ({pct_diff:+.1f}% to target)")
def cmd_delete(args) -> None:
"""Delete an alert."""
data = load_alerts()
alerts = data.get("alerts", [])
ticker = args.ticker.upper()
new_alerts = [a for a in alerts if a["ticker"] != ticker]
if len(new_alerts) == len(alerts):
print(f"❌ No alert found for {ticker}")
return
data["alerts"] = new_alerts
save_alerts(data)
print(f"🗑️ Alert deleted: {ticker}")
def cmd_snooze(args) -> None:
"""Snooze an alert."""
data = load_alerts()
alerts = data.get("alerts", [])
ticker = args.ticker.upper()
alert = get_alert_by_ticker(alerts, ticker)
if not alert:
print(f"❌ No alert found for {ticker}")
return
days = args.days or 7
snooze_until = datetime.now() + timedelta(days=days)
alert["snooze_until"] = snooze_until.isoformat()
save_alerts(data)
print(f"😴 Alert snoozed: {ticker} until {snooze_until.strftime('%Y-%m-%d')}")
def cmd_update(args) -> None:
"""Update alert target price."""
data = load_alerts()
alerts = data.get("alerts", [])
ticker = args.ticker.upper()
alert = get_alert_by_ticker(alerts, ticker)
if not alert:
print(f"❌ No alert found for {ticker}")
return
# Validate target price
if args.target <= 0:
print(f"❌ Target price must be greater than 0")
return
old_target = alert["target_price"]
alert["target_price"] = args.target
if args.note:
alert["note"] = args.note
save_alerts(data)
currency = alert.get("currency", "USD")
old_str = format_price(old_target, currency)
new_str = format_price(args.target, currency)
print(f"✏️ Alert updated: {ticker} {old_str}{new_str}")
def cmd_check(args) -> None:
"""Check alerts against current prices."""
data = load_alerts()
alerts = data.get("alerts", [])
if not alerts:
if args.json:
print(json.dumps({"triggered": [], "watching": []}))
else:
print("📭 No alerts to check")
return
now = datetime.now()
active_alerts = []
for alert in alerts:
snooze_until = alert.get("snooze_until")
if snooze_until and datetime.fromisoformat(snooze_until) > now:
continue
active_alerts.append(alert)
if not active_alerts:
if args.json:
print(json.dumps({"triggered": [], "watching": []}))
else:
print("📭 All alerts snoozed")
return
# Fetch prices for all active alerts
tickers = [a["ticker"] for a in active_alerts]
quotes = get_fetch_market_data()(tickers, timeout=30)
triggered = []
watching = []
for alert in active_alerts:
ticker = alert["ticker"]
target = alert["target_price"]
currency = alert.get("currency", "USD")
quote = quotes.get(ticker, {})
price = quote.get("price")
if price is None:
continue
# Divide-by-zero protection
if target == 0:
pct_diff = 0
else:
pct_diff = ((price - target) / target) * 100
result = {
"ticker": ticker,
"target_price": target,
"current_price": price,
"currency": currency,
"pct_from_target": round(pct_diff, 2),
"note": alert.get("note", ""),
"set_by": alert.get("set_by", ""),
}
if price <= target:
triggered.append(result)
# Update triggered count (only once per day to avoid inflation)
last_triggered = alert.get("last_triggered")
today = now.strftime("%Y-%m-%d")
if not last_triggered or not last_triggered.startswith(today):
alert["triggered_count"] = alert.get("triggered_count", 0) + 1
alert["last_triggered"] = now.isoformat()
else:
watching.append(result)
save_alerts(data)
if args.json:
print(json.dumps({"triggered": triggered, "watching": watching}, indent=2))
return
# Translations
lang = getattr(args, 'lang', 'en')
if lang == "de":
labels = {
"title": "PREISWARNUNGEN",
"in_zone": "IN KAUFZONE",
"buy": "KAUFEN!",
"target": "Ziel",
"watching": "BEOBACHTUNG",
"to_target": "noch",
"no_data": "Keine Preisdaten für Alerts verfügbar",
}
else:
labels = {
"title": "PRICE ALERTS",
"in_zone": "IN BUY ZONE",
"buy": "BUY SIGNAL",
"target": "target",
"watching": "WATCHING",
"to_target": "to target",
"no_data": "No price data available for alerts",
}
# Date header
date_str = datetime.now().strftime("%b %d, %Y") if lang == "en" else datetime.now().strftime("%d. %b %Y")
print(f"📊 {labels['title']}{date_str}\n")
# Human-readable output
if triggered:
print(f"🟢 {labels['in_zone']}:\n")
for t in triggered:
target_str = format_price(t["target_price"], t["currency"])
current_str = format_price(t["current_price"], t["currency"])
note = f'\n "{t["note"]}"' if t.get("note") else ""
user = f"{t['set_by']}" if t.get("set_by") else ""
print(f"{t['ticker']}: {current_str} ({labels['target']}: {target_str}) ← {labels['buy']}{note}{user}")
print()
if watching:
print(f"{labels['watching']}:\n")
for w in sorted(watching, key=lambda x: x["pct_from_target"]):
target_str = format_price(w["target_price"], w["currency"])
current_str = format_price(w["current_price"], w["currency"])
print(f"{w['ticker']}: {current_str} ({labels['target']}: {target_str}) — {labels['to_target']} {abs(w['pct_from_target']):.1f}%")
print()
if not triggered and not watching:
print(f"📭 {labels['no_data']}")
def check_alerts() -> dict:
"""
Check alerts and return results for briefing integration.
Returns: {"triggered": [...], "watching": [...]}
"""
data = load_alerts()
alerts = data.get("alerts", [])
if not alerts:
return {"triggered": [], "watching": []}
now = datetime.now()
active_alerts = [
a for a in alerts
if not a.get("snooze_until") or datetime.fromisoformat(a["snooze_until"]) <= now
]
if not active_alerts:
return {"triggered": [], "watching": []}
tickers = [a["ticker"] for a in active_alerts]
quotes = get_fetch_market_data()(tickers, timeout=30)
triggered = []
watching = []
for alert in active_alerts:
ticker = alert["ticker"]
target = alert["target_price"]
currency = alert.get("currency", "USD")
quote = quotes.get(ticker, {})
price = quote.get("price")
if price is None:
continue
# Divide-by-zero protection
if target == 0:
pct_diff = 0
else:
pct_diff = ((price - target) / target) * 100
result = {
"ticker": ticker,
"target_price": target,
"current_price": price,
"currency": currency,
"pct_from_target": round(pct_diff, 2),
"note": alert.get("note", ""),
"set_by": alert.get("set_by", ""),
}
if price <= target:
triggered.append(result)
# Update triggered count (only once per day to avoid inflation)
last_triggered = alert.get("last_triggered")
today = now.strftime("%Y-%m-%d")
if not last_triggered or not last_triggered.startswith(today):
alert["triggered_count"] = alert.get("triggered_count", 0) + 1
alert["last_triggered"] = now.isoformat()
else:
watching.append(result)
save_alerts(data)
return {"triggered": triggered, "watching": watching}
def main():
parser = argparse.ArgumentParser(description="Price target alerts")
subparsers = parser.add_subparsers(dest="command", required=True)
# list
subparsers.add_parser("list", help="List all alerts")
# set
set_parser = subparsers.add_parser("set", help="Set new alert")
set_parser.add_argument("ticker", help="Stock ticker")
set_parser.add_argument("target", type=float, help="Target price")
set_parser.add_argument("--note", help="Note/reason")
set_parser.add_argument("--user", help="Who set the alert")
set_parser.add_argument("--currency", default="USD", help="Currency (USD, EUR, JPY, SGD, MXN)")
# delete
del_parser = subparsers.add_parser("delete", help="Delete alert")
del_parser.add_argument("ticker", help="Stock ticker")
# snooze
snooze_parser = subparsers.add_parser("snooze", help="Snooze alert")
snooze_parser.add_argument("ticker", help="Stock ticker")
snooze_parser.add_argument("--days", type=int, default=7, help="Days to snooze")
# update
update_parser = subparsers.add_parser("update", help="Update alert target")
update_parser.add_argument("ticker", help="Stock ticker")
update_parser.add_argument("target", type=float, help="New target price")
update_parser.add_argument("--note", help="Update note")
# check
check_parser = subparsers.add_parser("check", help="Check alerts against prices")
check_parser.add_argument("--json", action="store_true", help="JSON output")
check_parser.add_argument("--lang", default="en", help="Output language (en, de)")
args = parser.parse_args()
if args.command == "list":
cmd_list(args)
elif args.command == "set":
cmd_set(args)
elif args.command == "delete":
cmd_delete(args)
elif args.command == "snooze":
cmd_snooze(args)
elif args.command == "update":
cmd_update(args)
elif args.command == "check":
cmd_check(args)
if __name__ == "__main__":
main()