agent bourse, revue glissante, niveau VW estime depuis l'autonomie, alertes
This commit is contained in:
148
workspace-bourse/scripts/bourse.py
Normal file
148
workspace-bourse/scripts/bourse.py
Normal file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
# bourse.py — SEUL outil de l'agent bourse (10/10/2026, « GO agent bourse »). Le code calcule, l'agent relaie.
|
||||
# cours <nom|ticker>... → « ORA.PA (ORANGE) : 13,87 € (-2,50 % sur la séance) », une ligne par titre
|
||||
# graphe <nom|ticker>... [--periode P] [--zoom|--zero]
|
||||
# → un PNG par titre via le webhook N8N, imprime « MEDIA:<chemin> » (ligne seule = image jointe)
|
||||
# cloture → les graphes de close_bilan.images (cron 17:40), mêmes lignes MEDIA:
|
||||
# liste → la watchlist et le bilan de clôture
|
||||
# ajouter <nom|ticker> [--cloture] / retirer <nom|ticker> [--cloture]
|
||||
# → modifie bourse-config.json (seule écriture du script)
|
||||
# Périodes : jour (1d/15m), semaine (5d/60m), mois (1mo/1d), trimestre (3mo/1d), semestre (6mo/1d), an (1y/1d).
|
||||
# Un titre par graphe : le webhook n'a pas de normalisation, deux prix d'échelles différentes se lisent mal.
|
||||
import json, os, re, sys, time, urllib.request, urllib.error
|
||||
|
||||
WS = "/home/node/.openclaw/workspace-bourse"
|
||||
CONF = os.environ.get("BOURSE_CONF", os.path.join(WS, "bourse-config.json")) # tests : copie
|
||||
CHARTS = os.path.join(WS, "charts")
|
||||
YAHOO = "https://query1.finance.yahoo.com/v8/finance/chart/{}?interval=1d&range=1d"
|
||||
UA = {"User-Agent": "Mozilla/5.0"} # sans navigateur déclaré, Yahoo répond vide
|
||||
PERIODES = {"jour": ("1d", "15m"), "semaine": ("5d", "60m"), "mois": ("1mo", "1d"),
|
||||
"trimestre": ("3mo", "1d"), "semestre": ("6mo", "1d"), "an": ("1y", "1d")}
|
||||
DEVISES = {"EUR": "€", "USD": "$", "GBP": "£"}
|
||||
|
||||
def conf(): return json.load(open(CONF))
|
||||
def sauver(c): json.dump(c, open(CONF, "w"), indent=2, ensure_ascii=False)
|
||||
|
||||
def ticker(nom):
|
||||
"""Nom courant ou ticker → symbole Yahoo ; None si non coté ; str commençant par '?' si inconnu."""
|
||||
n = nom.strip().lower()
|
||||
al = conf().get("ticker_aliases", {})
|
||||
if n in al: return None if al[n] == "non_cote_en_bourse" else al[n]
|
||||
# Ticker écrit EN MAJUSCULES (NVDA, ORA.PA) ; un mot en minuscules hors alias (« truc ») est inconnu.
|
||||
if re.fullmatch(r"[A-Z0-9^=-]{1,10}(\.[A-Z]{1,3})?", nom.strip()): return nom.strip()
|
||||
return "?" + nom
|
||||
|
||||
def nombre(x, d=2): return f"{x:,.{d}f}".replace(",", " ").replace(".", ",")
|
||||
|
||||
def yahoo(sym):
|
||||
r = urllib.request.urlopen(urllib.request.Request(YAHOO.format(sym), headers=UA), timeout=20)
|
||||
res = json.load(r)["chart"]["result"]
|
||||
if not res: raise ValueError("aucune donnée")
|
||||
return res[0]["meta"]
|
||||
|
||||
def cours(noms):
|
||||
for nom in noms:
|
||||
s = ticker(nom)
|
||||
if s is None: print(f"{nom} : non coté en bourse."); continue
|
||||
if s.startswith("?"): print(f"{nom} : titre inconnu, donne son ticker (ex. ORA.PA, NVDA)."); continue
|
||||
try: m = yahoo(s)
|
||||
except Exception as e: print(f"{s} : cours indisponible ({e}). Aucun chiffre."); continue
|
||||
prix, dev = m.get("regularMarketPrice"), DEVISES.get(m.get("currency"), m.get("currency", ""))
|
||||
if prix is None: print(f"{s} : cours indisponible. Aucun chiffre."); continue
|
||||
pct = m.get("regularMarketChangePercent")
|
||||
if pct is None and m.get("chartPreviousClose"): pct = (prix / m["chartPreviousClose"] - 1) * 100
|
||||
heure = time.strftime("%d/%m %H:%M", time.localtime(m["regularMarketTime"])) if m.get("regularMarketTime") else "?"
|
||||
var = f" ({'+' if pct >= 0 else ''}{nombre(pct)} % sur la séance)" if pct is not None else ""
|
||||
print(f"{s} ({m.get('shortName') or m.get('longName') or s}) : {nombre(prix)} {dev}{var} — cotation de {heure}")
|
||||
|
||||
def un_graphe(sym, rng, itv, zero, fichier):
|
||||
body = json.dumps({"tickers": [sym], "range": rng, "interval": itv, "beginAtZero": zero, "title": sym}).encode()
|
||||
try:
|
||||
r = urllib.request.urlopen(urllib.request.Request(conf()["webhook"], data=body, method="POST",
|
||||
headers={"Content-Type": "application/json"}), timeout=120)
|
||||
data = r.read()
|
||||
except Exception as e:
|
||||
print(f"{sym} : graphe indisponible ({e})."); return
|
||||
if len(data) < 5000 or not data.startswith(b"\x89PNG"):
|
||||
print(f"{sym} : pas de données (marché fermé ou ticker invalide). Aucun graphe."); return
|
||||
os.makedirs(CHARTS, exist_ok=True)
|
||||
chemin = os.path.join(CHARTS, fichier); open(chemin, "wb").write(data)
|
||||
print(f"MEDIA:{chemin}") # ligne seule relayée telle quelle = image jointe (protocole OpenClaw)
|
||||
|
||||
def graphe(args):
|
||||
per, zero, noms = "semaine", False, []
|
||||
i = 0
|
||||
while i < len(args):
|
||||
a = args[i]
|
||||
if a == "--periode" and i + 1 < len(args): per = args[i + 1].lower(); i += 2; continue
|
||||
if a == "--zero": zero = True
|
||||
elif a == "--zoom": zero = False
|
||||
else: noms.append(a)
|
||||
i += 1
|
||||
if per not in PERIODES: print(f"Période inconnue « {per} » : {', '.join(PERIODES)}."); return
|
||||
if not noms: noms = conf().get("tickers", [])
|
||||
rng, itv = PERIODES[per]
|
||||
for nom in noms:
|
||||
s = ticker(nom)
|
||||
if s is None: print(f"{nom} : non coté en bourse."); continue
|
||||
if s.startswith("?"): print(f"{nom} : titre inconnu, donne son ticker (ex. ORA.PA, NVDA)."); continue
|
||||
un_graphe(s, rng, itv, zero, f"{re.sub(r'[^a-z0-9]', '_', s.lower())}_{per}.png")
|
||||
|
||||
def cloture():
|
||||
imgs = conf().get("close_bilan", {}).get("images", [])
|
||||
for im in imgs:
|
||||
for f in [im.get("file")]:
|
||||
if f:
|
||||
p = os.path.join(CHARTS, f)
|
||||
if os.path.exists(p): os.remove(p)
|
||||
n = 0
|
||||
for im in imgs:
|
||||
tk = im.get("tickers") or []
|
||||
if len(tk) != 1 or not im.get("file"): print(f"Entrée de clôture ignorée (un seul ticker et un fichier) : {im}"); continue
|
||||
un_graphe(tk[0], "1mo", "1d", False, im["file"]); n += 1
|
||||
if not n: print("Bilan de clôture vide : rien à envoyer.")
|
||||
|
||||
def liste():
|
||||
c = conf()
|
||||
print("Watchlist : " + ", ".join(c.get("tickers", [])))
|
||||
print("Bilan de clôture (17:40, jours ouvrés) : " + ", ".join(t for im in c.get("close_bilan", {}).get("images", []) for t in im.get("tickers", [])))
|
||||
|
||||
def modifier(args, ajout):
|
||||
clot = "--cloture" in args; noms = [a for a in args if a != "--cloture"]
|
||||
if len(noms) != 1: print("Un seul titre à la fois."); return
|
||||
s = ticker(noms[0])
|
||||
if s is None: print(f"{noms[0]} : non coté en bourse, rien de changé."); return
|
||||
if s.startswith("?"): print(f"{noms[0]} : titre inconnu, donne son ticker. Rien de changé."); return
|
||||
if ajout:
|
||||
try: yahoo(s)
|
||||
except Exception: print(f"{s} : Yahoo ne connaît pas ce ticker. Rien de changé."); return
|
||||
c = conf()
|
||||
if clot:
|
||||
imgs = c.setdefault("close_bilan", {}).setdefault("images", [])
|
||||
dedans = [im for im in imgs if im.get("tickers") == [s]]
|
||||
if ajout and dedans: print(f"{s} est déjà dans le bilan de clôture."); return
|
||||
if not ajout and not dedans: print(f"{s} n'est pas dans le bilan de clôture."); return
|
||||
if ajout: imgs.append({"type": "prix", "tickers": [s], "note": "zoom variations",
|
||||
"file": f"prix_{re.sub(r'[^a-z0-9]', '_', s.lower())}.png"})
|
||||
else: c["close_bilan"]["images"] = [im for im in imgs if im.get("tickers") != [s]]
|
||||
lieu = "bilan de clôture"
|
||||
else:
|
||||
tk = c.setdefault("tickers", [])
|
||||
if ajout and s in tk: print(f"{s} est déjà dans la watchlist."); return
|
||||
if not ajout and s not in tk: print(f"{s} n'est pas dans la watchlist."); return
|
||||
if ajout: tk.append(s)
|
||||
else: tk.remove(s)
|
||||
lieu = "watchlist"
|
||||
sauver(c)
|
||||
art = ("au " if ajout else "du ") if lieu.startswith("bilan") else ("à la " if ajout else "de la ")
|
||||
print(f"C'est noté : {s} {'ajouté' if ajout else 'retiré'} {art}{lieu}.")
|
||||
|
||||
a = sys.argv[1:]
|
||||
cmd, reste = (a[0], a[1:]) if a else ("", [])
|
||||
if cmd == "cours" and reste: cours(reste)
|
||||
elif cmd == "graphe": graphe(reste)
|
||||
elif cmd == "cloture": cloture()
|
||||
elif cmd == "liste": liste()
|
||||
elif cmd == "ajouter": modifier(reste, True)
|
||||
elif cmd == "retirer": modifier(reste, False)
|
||||
else: print("usage : bourse.py cours <titre>... | graphe <titre>... [--periode jour|semaine|mois|trimestre|semestre|an] [--zoom|--zero] | cloture | liste | ajouter|retirer <titre> [--cloture]")
|
||||
Reference in New Issue
Block a user