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
+83
View File
@@ -0,0 +1,83 @@
import numpy as np
from trademind.features import (
FEATURE_NAMES,
N_FEATURES,
build_feature_matrix,
compute_features,
required_bars,
)
from .conftest import make_candles
def test_matrix_has_expected_shape(candles, rules):
matrix = build_feature_matrix(candles, rules)
assert matrix is not None
assert matrix.values.shape == (len(candles), N_FEATURES)
assert matrix.first_valid == required_bars(rules) - 1
def test_all_feature_values_are_finite_and_bounded(candles, rules):
matrix = build_feature_matrix(candles, rules)
valid = matrix.values[matrix.first_valid :]
assert np.isfinite(valid).all()
assert np.abs(valid).max() <= 8.0
def test_too_short_history_returns_none(rules):
short = make_candles(n=50)
assert build_feature_matrix(short, rules) is None
assert compute_features(short, rules) is None
def test_snapshot_exposes_raw_indicators(candles, rules):
snapshot = compute_features(candles, rules)
assert snapshot is not None
assert snapshot.price == float(candles.close[-1])
assert snapshot.atr > 0
assert 0.0 <= snapshot.rsi <= 100.0
assert len(snapshot.values) == len(FEATURE_NAMES)
assert set(snapshot.as_dict()) == set(FEATURE_NAMES)
def test_snapshot_before_warmup_is_none(candles, rules):
matrix = build_feature_matrix(candles, rules)
assert matrix.snapshot(matrix.first_valid - 1) is None
assert matrix.snapshot(matrix.first_valid) is not None
def test_uptrend_produces_positive_trend_distance(rules):
up = make_candles(n=500, trend=0.001, noise=0.0005, seed=11)
snapshot = compute_features(up, rules)
features = snapshot.as_dict()
assert features["trend_dist"] > 0
assert features["ema_spread"] > 0
def test_downtrend_produces_negative_trend_distance(rules):
down = make_candles(n=500, trend=-0.001, noise=0.0005, seed=12)
features = compute_features(down, rules).as_dict()
assert features["trend_dist"] < 0
assert features["ema_spread"] < 0
def test_features_are_scale_invariant(rules):
"""Ein zehnfach höherer Kurs darf die normierten Merkmale kaum verändern."""
cheap = make_candles(n=400, start_price=100.0, seed=5)
expensive = make_candles(n=400, start_price=1_000.0, seed=5)
a = compute_features(cheap, rules).values
b = compute_features(expensive, rules).values
assert np.allclose(a, b, atol=1e-8)
def test_time_features_are_on_the_unit_circle(candles, rules):
snapshot = compute_features(candles, rules).as_dict()
radius = snapshot["time_sin"] ** 2 + snapshot["time_cos"] ** 2
assert radius == 1.0 or abs(radius - 1.0) < 1e-9
def test_matrix_rows_match_pointwise_snapshots(candles, rules):
matrix = build_feature_matrix(candles, rules)
index = matrix.first_valid + 25
assert np.allclose(matrix.snapshot(index).values, matrix.values[index])