65ed73977e
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.
97 lines
3.2 KiB
Python
97 lines
3.2 KiB
Python
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
|