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:
@@ -0,0 +1,218 @@
|
||||
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
|
||||
Reference in New Issue
Block a user