Training über das Dashboard anstoßen
Neuer Abschnitt "Training" im Dashboard mit zwei Bedienelementen und den zugehörigen Endpunkten /control/train/history, /control/train/live und /control/training. Historisches Nachtraining - Kerzenanzahl je Symbol wählbar (500 bis 50 000), Fortschritt und Ergebnis werden im Dashboard angezeigt. - Läuft mit derselben Logik wie ein Backtest, aber ohne zu handeln, und speichert das Modell anschließend. - Handelsdurchlauf und Nachtraining teilen sich einen Mutex, damit sie nicht gleichzeitig auf Modell und Portfolio zugreifen. Die rechenintensive Schleife läuft in einem Worker-Thread, damit der Status-Server antwortbereit bleibt. - Ein zweiter Start wird abgelehnt, solange einer eingereiht ist oder läuft. Kontinuierliches Lernen - Schalter für das Online-Lernen im laufenden Betrieb. Ausgeschaltet handelt der Bot weiter, verändert das Modell aber nicht mehr. Label-Trennung - Vorgemerkte Labels tragen jetzt ein Tag. Ein Nachtraining darf die offenen Labels des Live-Betriebs weder auflösen noch verwerfen; ohne die Trennung würden sie gegen historische Kurse ausgewertet und das Modell mit falschen Ergebnissen gefüttert. - score() liest die Gewichte unter dem Lock, damit ein parallel laufendes Training keinen halb aktualisierten Vektor sichtbar macht. Absicherung - server.control_token (TRADEMIND_CONTROL_TOKEN) schützt alle Steuerbefehle über den Header X-TradeMind-Token; lesende Endpunkte bleiben offen. Ohne Token warnt der Bot beim Start, wenn der Port nicht nur lokal erreichbar ist. - server.enable_control: false entfernt die Routen vollständig. 153 Tests (25 neue), ruff sauber. Im gebauten Container geprüft: 202/409 beim Anstoßen, 401 ohne und mit falschem Token, +1099 Beobachtungen in 4,3 s bei weiterlaufendem Handels-Loop ohne Fehler.
This commit is contained in:
+27
-1
@@ -80,6 +80,24 @@ class Runtime:
|
||||
self.storage.close()
|
||||
|
||||
|
||||
def _warn_about_exposure(config: Config) -> None:
|
||||
"""Warnen, wenn Steuerbefehle ungeschützt aus dem Netz erreichbar sind."""
|
||||
if not config.server.enable_control:
|
||||
return
|
||||
if config.server.control_token:
|
||||
log.info("Steuerbefehle sind aktiv und durch ein Token geschützt")
|
||||
return
|
||||
if config.server.publicly_reachable:
|
||||
log.warning(
|
||||
"Steuerbefehle (Training anstoßen, Lernen umschalten) sind ohne Token auf %s:%d "
|
||||
"erreichbar. Jeder, der den Port erreicht, kann sie auslösen – bitte "
|
||||
"server.control_token setzen oder den Zugriff per Firewall/Reverse Proxy begrenzen.",
|
||||
config.server.host, config.server.port,
|
||||
)
|
||||
else:
|
||||
log.info("Steuerbefehle sind aktiv (nur lokal erreichbar, kein Token gesetzt)")
|
||||
|
||||
|
||||
def _quote_currency(market_info: dict[str, dict[str, Any]], symbols: list[str], fallback: str) -> str:
|
||||
quotes = {market_info.get(s, {}).get("quote") for s in symbols}
|
||||
quotes.discard(None)
|
||||
@@ -147,7 +165,10 @@ async def build_runtime(
|
||||
notifier=notifier,
|
||||
)
|
||||
|
||||
server = StatusServer(config.server, engine.status) if (with_server and config.server.enabled) else None
|
||||
server = None
|
||||
if with_server and config.server.enabled:
|
||||
server = StatusServer(config.server, engine.status, controller=engine)
|
||||
_warn_about_exposure(config)
|
||||
|
||||
storage.start_run(
|
||||
mode=config.mode.value,
|
||||
@@ -203,4 +224,9 @@ def describe_config(config: Config) -> str:
|
||||
)
|
||||
if config.server.enabled:
|
||||
lines.append(f"Status-Server http://{config.server.host}:{config.server.port}/")
|
||||
if config.server.enable_control:
|
||||
guard = "Token gesetzt" if config.server.control_token else "OHNE Token"
|
||||
lines.append(f"Steuerung Training über das Dashboard möglich ({guard})")
|
||||
else:
|
||||
lines.append("Steuerung deaktiviert (nur lesende Endpunkte)")
|
||||
return "\n".join(" " + line for line in lines)
|
||||
|
||||
Reference in New Issue
Block a user