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.
93 lines
2.7 KiB
Python
93 lines
2.7 KiB
Python
import numpy as np
|
|
import pytest
|
|
|
|
from trademind.indicators import atr, bollinger, donchian_position, ema, macd, roc, rsi, sma, true_range
|
|
|
|
|
|
def test_ema_of_constant_series_is_constant():
|
|
values = np.full(50, 42.0)
|
|
assert np.allclose(ema(values, 10), 42.0)
|
|
|
|
|
|
def test_ema_reacts_faster_than_sma():
|
|
values = np.concatenate([np.full(30, 100.0), np.full(30, 110.0)])
|
|
fast = ema(values, 10)[35]
|
|
slow = sma(values, 10)[35]
|
|
assert fast > slow # EMA hat den Sprung stärker eingepreist
|
|
|
|
|
|
def test_sma_matches_manual_mean():
|
|
values = np.arange(1.0, 11.0)
|
|
result = sma(values, 3)
|
|
assert np.isnan(result[:2]).all()
|
|
assert result[2] == pytest.approx(2.0)
|
|
assert result[-1] == pytest.approx(9.0)
|
|
|
|
|
|
def test_rsi_bounds_and_extremes():
|
|
rising = np.arange(1.0, 60.0)
|
|
values = rsi(rising, 14)
|
|
finite = values[np.isfinite(values)]
|
|
assert finite.min() >= 0.0 and finite.max() <= 100.0
|
|
assert finite[-1] == pytest.approx(100.0) # nur Gewinne
|
|
|
|
falling = rising[::-1].copy()
|
|
assert rsi(falling, 14)[-1] == pytest.approx(0.0)
|
|
|
|
|
|
def test_rsi_of_flat_series_is_neutral():
|
|
values = rsi(np.full(60, 25.0), 14)
|
|
assert values[-1] == pytest.approx(50.0)
|
|
|
|
|
|
def test_true_range_covers_gaps():
|
|
high = np.array([10.0, 20.0])
|
|
low = np.array([9.0, 19.0])
|
|
close = np.array([9.5, 19.5])
|
|
tr = true_range(high, low, close)
|
|
assert tr[0] == pytest.approx(1.0)
|
|
assert tr[1] == pytest.approx(10.5) # Lücke gegenüber dem Vortagesschluss
|
|
|
|
|
|
def test_atr_is_positive_and_warm():
|
|
n = 100
|
|
rng = np.random.default_rng(3)
|
|
close = 100 + np.cumsum(rng.normal(0, 1, n))
|
|
high = close + 1.0
|
|
low = close - 1.0
|
|
values = atr(high, low, close, 14)
|
|
assert np.isnan(values[:13]).all()
|
|
assert (values[13:] > 0).all()
|
|
|
|
|
|
def test_macd_histogram_is_difference():
|
|
values = 100 + np.cumsum(np.random.default_rng(1).normal(0, 1, 200))
|
|
line, signal, hist = macd(values)
|
|
assert np.allclose(hist, line - signal)
|
|
|
|
|
|
def test_bollinger_bands_are_ordered():
|
|
values = 100 + np.cumsum(np.random.default_rng(2).normal(0, 1, 200))
|
|
lower, mid, upper = bollinger(values, 20, 2.0)
|
|
valid = ~np.isnan(mid)
|
|
assert (lower[valid] <= mid[valid]).all()
|
|
assert (mid[valid] <= upper[valid]).all()
|
|
|
|
|
|
def test_roc_is_relative_change():
|
|
values = np.array([100.0] * 10 + [110.0])
|
|
assert roc(values, 10)[-1] == pytest.approx(0.10)
|
|
|
|
|
|
def test_donchian_position_hits_extremes():
|
|
close = np.array([float(i) for i in range(1, 41)])
|
|
high = close + 0.0
|
|
low = close - 0.0
|
|
pos = donchian_position(high, low, close, 20)
|
|
assert pos[-1] == pytest.approx(1.0) # Schluss auf dem Hoch der Range
|
|
|
|
|
|
def test_indicators_reject_wrong_dimensions():
|
|
with pytest.raises(ValueError):
|
|
ema(np.zeros((5, 2)), 3)
|