Files
Trademind/tests/test_strategy.py
T
Tobias Zimmermann 65ed73977e 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.
2026-08-22 08:53:04 +02:00

219 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import numpy as np
import pytest
from trademind.config import LearnerConfig, RuleConfig, StrategyConfig
from trademind.features import FEATURE_NAMES, N_FEATURES, FeatureSnapshot
from trademind.learner import AdaptiveLearner, NullLearner
from trademind.models import Action, Position
from trademind.strategy import AdaptiveStrategy, RuleStrategy, build_strategy
RULES = RuleConfig(rsi_overbought=70.0, rsi_oversold=35.0, min_holding_bars=3, trend_filter_period=100)
def snap(
*, price=100.0, rsi=50.0, rsi_prev=50.0, ema_fast=101.0, ema_slow=100.0,
ema_fast_prev=99.0, ema_slow_prev=100.0, trend_ema=95.0, atr=2.0,
) -> FeatureSnapshot:
return FeatureSnapshot(
values=np.zeros(N_FEATURES),
names=FEATURE_NAMES,
index=200,
price=price,
atr=atr,
rsi=rsi,
rsi_prev=rsi_prev,
ema_fast=ema_fast,
ema_slow=ema_slow,
ema_fast_prev=ema_fast_prev,
ema_slow_prev=ema_slow_prev,
trend_ema=trend_ema,
timestamp=1_700_000_000_000,
)
def position(bars_held: int = 10) -> Position:
return Position("BTC/USDT", 1.0, 100.0, 1_700_000_000_000, bars_held=bars_held)
# ------------------------------------------------------------------- Regeln
def test_ema_cross_up_in_uptrend_is_an_entry():
signal = RuleStrategy(RULES).evaluate("BTC/USDT", snap(), None)
assert signal.action is Action.ENTER_LONG
assert signal.reason == "ema_cross_up"
assert signal.features is not None
def test_no_entry_below_the_trend_filter():
signal = RuleStrategy(RULES).evaluate("BTC/USDT", snap(trend_ema=120.0), None)
assert signal.action is Action.HOLD
def test_no_entry_when_already_overbought():
signal = RuleStrategy(RULES).evaluate("BTC/USDT", snap(rsi=75.0), None)
assert signal.action is Action.HOLD
def test_oversold_pullback_is_an_entry():
signal = RuleStrategy(RULES).evaluate(
"BTC/USDT", snap(rsi=30.0, ema_fast_prev=101.0, ema_slow_prev=100.0), None
)
assert signal.action is Action.ENTER_LONG
assert signal.reason == "pullback_oversold"
def test_trend_filter_can_be_switched_off():
rules = RuleConfig(trend_filter_period=0)
signal = RuleStrategy(rules).evaluate("BTC/USDT", snap(trend_ema=120.0), None)
assert signal.action is Action.ENTER_LONG
def test_ema_cross_down_exits():
strategy = RuleStrategy(RULES)
s = snap(ema_fast=99.0, ema_slow=100.0, ema_fast_prev=101.0, ema_slow_prev=100.0)
assert strategy.evaluate("BTC/USDT", s, position()).action is Action.EXIT_LONG
def test_high_rsi_alone_does_not_exit():
"""Ein hoher RSI ist im Aufwärtstrend normal er darf keinen Ausstieg auslösen."""
strategy = RuleStrategy(RULES)
s = snap(rsi=85.0, rsi_prev=80.0)
assert strategy.evaluate("BTC/USDT", s, position()).action is Action.HOLD
def test_rsi_turning_down_out_of_overbought_exits():
strategy = RuleStrategy(RULES)
s = snap(rsi=68.0, rsi_prev=74.0)
signal = strategy.evaluate("BTC/USDT", s, position())
assert signal.action is Action.EXIT_LONG
assert signal.reason == "rsi_momentum_fade"
def test_minimum_holding_period_blocks_early_signal_exits():
strategy = RuleStrategy(RULES)
s = snap(ema_fast=99.0, ema_slow=100.0, ema_fast_prev=101.0, ema_slow_prev=100.0)
assert strategy.evaluate("BTC/USDT", s, position(bars_held=1)).action is Action.HOLD
assert strategy.evaluate("BTC/USDT", s, position(bars_held=3)).action is Action.EXIT_LONG
# ----------------------------------------------------------------- Adaptive
def make_adaptive(threshold=0.55, exploration=0.0, warmup=5) -> AdaptiveStrategy:
config = StrategyConfig(
name="adaptive",
rules=RULES,
learner=LearnerConfig(
entry_threshold=threshold,
exploration_rate=exploration,
warmup_samples=warmup,
background_sample_every_n_bars=0,
),
)
return AdaptiveStrategy(config, AdaptiveLearner(config.learner, N_FEATURES, seed=1))
def test_warmup_lets_every_rule_signal_through():
strategy = make_adaptive(threshold=0.99, warmup=1_000)
signal = strategy.evaluate("BTC/USDT", snap(), None)
assert signal.action is Action.ENTER_LONG
assert "warmup" in signal.reason
def test_model_can_veto_a_rule_signal():
strategy = make_adaptive(threshold=0.99, warmup=1)
strategy.learner.observe(np.zeros(N_FEATURES), 0.0)
signal = strategy.evaluate("BTC/USDT", snap(), None)
assert signal.action is Action.HOLD
assert "abgelehnt" in signal.reason
def test_low_threshold_lets_signals_pass():
strategy = make_adaptive(threshold=0.0, warmup=1)
strategy.learner.observe(np.zeros(N_FEATURES), 0.0)
assert strategy.evaluate("BTC/USDT", snap(), None).action is Action.ENTER_LONG
def test_exploration_overrides_a_veto():
strategy = make_adaptive(threshold=0.99, exploration=1.0, warmup=1)
strategy.learner.observe(np.zeros(N_FEATURES), 0.0)
signal = strategy.evaluate("BTC/USDT", snap(), None)
assert signal.action is Action.ENTER_LONG
assert signal.exploratory is True
def test_every_candidate_is_registered_for_labelling():
"""Auch abgelehnte Signale müssen gelabelt werden sonst lernt der Bot nichts dazu."""
strategy = make_adaptive(threshold=0.99, warmup=1)
strategy.learner.observe(np.zeros(N_FEATURES), 0.0)
strategy.evaluate("BTC/USDT", snap(), None)
assert strategy.learner.pending_count == 1
def test_no_candidate_no_pending_label():
strategy = make_adaptive()
strategy.evaluate("BTC/USDT", snap(trend_ema=120.0), None) # kein Setup
assert strategy.learner.pending_count == 0
def test_background_samples_are_registered_on_schedule():
config = StrategyConfig(
name="adaptive",
rules=RULES,
learner=LearnerConfig(background_sample_every_n_bars=5, background_sample_weight=0.5),
)
strategy = AdaptiveStrategy(config, AdaptiveLearner(config.learner, N_FEATURES, seed=1))
for bar in range(10):
strategy.on_bar("BTC/USDT", snap(), bar, 101.0, 99.0, 100.0)
assert strategy.background_samples == 2 # Bar 0 und Bar 5
def test_background_sampling_can_be_disabled():
strategy = make_adaptive() # background_sample_every_n_bars=0
for bar in range(20):
strategy.on_bar("BTC/USDT", snap(), bar, 101.0, 99.0, 100.0)
assert strategy.background_samples == 0
def test_closed_trade_feeds_the_learner():
strategy = make_adaptive()
strategy.learner.autosave = False
pos = position()
pos.entry_features = np.ones(N_FEATURES)
strategy.on_trade_closed(pos, pnl_quote=25.0)
assert strategy.learner.stats.trade_samples == 1
def test_snapshot_reports_acceptance():
strategy = make_adaptive(threshold=0.0, warmup=1)
for _ in range(3):
strategy.evaluate("BTC/USDT", snap(), None)
data = strategy.snapshot()
assert data["candidates_seen"] == 3
assert data["acceptance_rate"] == pytest.approx(1.0)
assert "learner" in data
# ------------------------------------------------------------------- Factory
def test_build_strategy_rules_variant():
strategy = build_strategy(StrategyConfig(name="rules"), N_FEATURES)
assert isinstance(strategy, RuleStrategy)
def test_build_strategy_without_learning_uses_null_learner(tmp_path):
config = StrategyConfig(
name="adaptive", learner=LearnerConfig(enabled=False, model_path=str(tmp_path / "m.npz"))
)
strategy = build_strategy(config, N_FEATURES)
assert isinstance(strategy.learner, NullLearner)
def test_build_strategy_can_skip_loading(tmp_path):
config = StrategyConfig(name="adaptive", learner=LearnerConfig(model_path=str(tmp_path / "m.npz")))
strategy = build_strategy(config, N_FEATURES, load_model=False)
assert isinstance(strategy.learner, AdaptiveLearner)
assert strategy.learner.stats.samples_seen == 0