Initial commit: TradeMind – Krypto-Trading-Bot mit Lernmodus

Per Podman deploybarer Bot, der Käufe und Verkäufe simuliert ausführt und sich
aus den Ergebnissen weiter antrainiert.

Aufbau
- Einheitliche Bar-Verarbeitung für paper, backtest und live; ausgetauscht werden
  nur Datenquelle und Broker.
- Börsenanbindung über ccxt: rund 100 Börsen allein über exchange.id erreichbar.
  Zugangsdaten kommen über ENV-Platzhalter, der Live-Modus ist doppelt abgesichert.
- Paper-Broker mit Gebühren, Slippage, Börsenpräzision und Volumengrenzen.
- Online trainierte logistische Regression bewertet jedes Einstiegssignal. Sie lernt
  aus realen Trade-Ergebnissen, aus Shadow-Labels aller Kandidaten – auch der
  abgelehnten – und aus Hintergrund-Stichproben; beim Kaltstart wird sie aus der
  Kurshistorie vorgelernt.
- Risikomanagement: Positions- und Exposure-Grenzen, ATR-Stops, Cooldown sowie
  Tagesverlust- und Drawdown-Notbremsen.
- SQLite-Persistenz, HTTP-Status mit Prometheus-Metriken und Dashboard, Webhooks.

Deployment
- Containerfile (zweistufig, non-root UID 10001), podman-compose, systemd-Quadlet.
- Modell und Datenbank liegen im Volume /data und überleben Neustarts.

128 Tests, ruff sauber. Verifiziert gegen echte Marktdaten sowie im gebauten
Container inklusive Healthcheck und Zustandswiederherstellung.
This commit is contained in:
Tobias Zimmermann
2026-08-22 08:53:04 +02:00
commit 65ed73977e
40 changed files with 7033 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
import pytest
from pydantic import ValidationError
from trademind.config import LIVE_CONFIRMATION_PHRASE, Config, Mode, load_config
MINIMAL = """
mode: paper
market:
symbols: [btc/usdt]
"""
def write(tmp_path, text: str):
path = tmp_path / "config.yaml"
path.write_text(text, encoding="utf-8")
return path
def test_minimal_config_uses_defaults(tmp_path):
config = load_config(write(tmp_path, MINIMAL))
assert config.mode is Mode.PAPER
assert config.market.symbols == ["BTC/USDT"] # normalisiert
assert config.exchange.id == "binance"
assert config.risk.max_open_positions == 3
def test_env_placeholders_are_resolved(tmp_path, monkeypatch):
monkeypatch.setenv("MY_KEY", "abc123")
config = load_config(
write(tmp_path, "mode: paper\nexchange:\n api_key: ${MY_KEY}\n api_secret: ${MISSING}\n")
)
assert config.exchange.api_key == "abc123"
assert config.exchange.api_secret is None
def test_env_placeholder_default_value(tmp_path, monkeypatch):
monkeypatch.delenv("TRADEMIND_EXCHANGE", raising=False)
config = load_config(write(tmp_path, "exchange:\n id: ${TRADEMIND_EXCHANGE:-kraken}\n"))
assert config.exchange.id == "kraken"
def test_double_underscore_env_override(tmp_path, monkeypatch):
monkeypatch.setenv("TRADEMIND__RISK__MAX_OPEN_POSITIONS", "7")
monkeypatch.setenv("TRADEMIND__MARKET__SYMBOLS", "BTC/USDT,ETH/USDT")
monkeypatch.setenv("TRADEMIND__EXCHANGE__SANDBOX", "false")
config = load_config(write(tmp_path, MINIMAL))
assert config.risk.max_open_positions == 7
assert config.market.symbols == ["BTC/USDT", "ETH/USDT"]
assert config.exchange.sandbox is False
def test_live_mode_requires_confirmation(tmp_path):
text = "mode: live\nexchange:\n api_key: k\n api_secret: s\n"
with pytest.raises(ValueError, match="live_confirmation"):
load_config(write(tmp_path, text))
def test_live_mode_requires_credentials(tmp_path):
text = f"mode: live\nlive_confirmation: {LIVE_CONFIRMATION_PHRASE}\n"
with pytest.raises(ValueError, match="api_key"):
load_config(write(tmp_path, text))
def test_live_mode_accepted_when_complete(tmp_path):
text = (
f"mode: live\nlive_confirmation: {LIVE_CONFIRMATION_PHRASE}\n"
"exchange:\n api_key: k\n api_secret: s\n"
)
config = load_config(write(tmp_path, text))
assert config.mode is Mode.LIVE
assert config.is_simulated is False
def test_unknown_key_is_rejected(tmp_path):
with pytest.raises(ValidationError):
load_config(write(tmp_path, "mode: paper\nrisk:\n typo_here: 5\n"))
def test_ema_periods_must_be_ordered(tmp_path):
with pytest.raises(ValueError, match="fast_ema"):
load_config(write(tmp_path, "strategy:\n rules:\n fast_ema: 30\n slow_ema: 10\n"))
def test_missing_file_raises(tmp_path):
with pytest.raises(FileNotFoundError):
load_config(tmp_path / "nope.yaml")
def test_empty_symbols_rejected(tmp_path):
with pytest.raises(ValidationError):
load_config(write(tmp_path, "market:\n symbols: []\n"))
def test_config_is_simulated_flag():
assert Config(mode=Mode.PAPER).is_simulated
assert Config(mode=Mode.BACKTEST).is_simulated