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
3.6 KiB
Python
93 lines
3.6 KiB
Python
import pytest
|
||
|
||
from trademind.broker import InsufficientFunds, OrderRejected, PaperBroker
|
||
from trademind.config import PaperConfig
|
||
from trademind.models import Side
|
||
|
||
|
||
@pytest.fixture
|
||
def broker(paper_config) -> PaperBroker:
|
||
return PaperBroker(paper_config)
|
||
|
||
|
||
async def test_buy_applies_slippage_and_fee(broker):
|
||
fill = await broker.execute("BTC/USDT", Side.BUY, 0.1, 30_000.0)
|
||
assert fill.price == pytest.approx(30_000.0 * 1.0005) # 5 bps Slippage
|
||
assert fill.fee_quote == pytest.approx(fill.notional * 0.001)
|
||
assert await broker.cash() == pytest.approx(10_000.0 - fill.notional - fill.fee_quote)
|
||
assert await broker.holdings("BTC/USDT") == pytest.approx(0.1)
|
||
|
||
|
||
async def test_sell_applies_slippage_in_the_other_direction(broker):
|
||
await broker.execute("BTC/USDT", Side.BUY, 0.1, 30_000.0)
|
||
fill = await broker.execute("BTC/USDT", Side.SELL, 0.1, 31_000.0)
|
||
assert fill.price == pytest.approx(31_000.0 * 0.9995)
|
||
assert await broker.holdings("BTC/USDT") == 0.0
|
||
|
||
|
||
async def test_round_trip_at_constant_price_loses_exactly_the_costs(broker):
|
||
price = 30_000.0
|
||
await broker.execute("BTC/USDT", Side.BUY, 0.1, price)
|
||
await broker.execute("BTC/USDT", Side.SELL, 0.1, price)
|
||
# 2 × 0,1 % Gebühr + 2 × 5 bps Slippage auf ein Volumen von ~3000
|
||
assert await broker.cash() == pytest.approx(10_000.0 - 9.0, abs=0.2)
|
||
|
||
|
||
async def test_buy_is_scaled_down_to_available_cash(broker):
|
||
fill = await broker.execute("BTC/USDT", Side.BUY, 10.0, 30_000.0) # 300k gewünscht
|
||
assert fill.amount < 10.0
|
||
assert await broker.cash() == pytest.approx(0.0, abs=1e-6)
|
||
|
||
|
||
async def test_buy_without_any_cash_raises():
|
||
broker = PaperBroker(PaperConfig(starting_balance=1.0, fee_rate=0.001))
|
||
broker.market_info = {"BTC/USDT": {"amount_precision": 4}}
|
||
with pytest.raises(InsufficientFunds):
|
||
await broker.execute("BTC/USDT", Side.BUY, 1.0, 30_000.0)
|
||
|
||
|
||
async def test_sell_without_position_raises(broker):
|
||
with pytest.raises(OrderRejected):
|
||
await broker.execute("BTC/USDT", Side.SELL, 1.0, 30_000.0)
|
||
|
||
|
||
async def test_sell_is_capped_at_held_amount(broker):
|
||
await broker.execute("BTC/USDT", Side.BUY, 0.1, 30_000.0)
|
||
fill = await broker.execute("BTC/USDT", Side.SELL, 5.0, 30_000.0)
|
||
assert fill.amount == pytest.approx(0.1)
|
||
|
||
|
||
async def test_volume_participation_caps_the_order(broker):
|
||
fill = await broker.execute("BTC/USDT", Side.BUY, 0.1, 30_000.0, bar_volume=0.2)
|
||
assert fill.amount == pytest.approx(0.02) # 10 % von 0,2
|
||
assert fill.requested_amount == pytest.approx(0.1)
|
||
|
||
|
||
async def test_amount_precision_rounds_down(broker):
|
||
broker.market_info = {"BTC/USDT": {"amount_precision": 3}}
|
||
fill = await broker.execute("BTC/USDT", Side.BUY, 0.123456, 30_000.0)
|
||
assert fill.amount == pytest.approx(0.123)
|
||
|
||
|
||
async def test_step_size_precision_is_supported(broker):
|
||
broker.market_info = {"BTC/USDT": {"amount_precision": 0.05}}
|
||
fill = await broker.execute("BTC/USDT", Side.BUY, 0.17, 100.0)
|
||
assert fill.amount == pytest.approx(0.15)
|
||
|
||
|
||
async def test_invalid_inputs_are_rejected(broker):
|
||
with pytest.raises(OrderRejected):
|
||
await broker.execute("BTC/USDT", Side.BUY, 0.0, 30_000.0)
|
||
with pytest.raises(OrderRejected):
|
||
await broker.execute("BTC/USDT", Side.BUY, 1.0, 0.0)
|
||
|
||
|
||
async def test_state_round_trip(broker):
|
||
await broker.execute("BTC/USDT", Side.BUY, 0.05, 30_000.0)
|
||
state = broker.state()
|
||
|
||
restored = PaperBroker(broker.config)
|
||
restored.restore(state["cash"], state["holdings"], state["total_fees"])
|
||
assert await restored.cash() == pytest.approx(await broker.cash())
|
||
assert await restored.holdings("BTC/USDT") == pytest.approx(0.05)
|