aitrader/tests/test_portfolio.py
2026-05-26 14:05:21 +00:00

173 lines
5.0 KiB
Python

import pytest
from unittest.mock import MagicMock
from aitrader.config import Settings
from aitrader.storage import db as dbm
from aitrader.storage.models import Trade
from aitrader.trader import portfolio
@pytest.fixture
def settings(tmp_path):
s = Settings(starting_equity_eur=10000.0, db_path=str(tmp_path / "t.db"))
# Initialize DB
dbm.get_engine(s.db_path)
return s
@pytest.fixture
def mock_kraken():
kraken = MagicMock()
kraken.create_market_order.return_value = {"id": "mock_order_123"}
return kraken
def test_check_stop_take_profit_buy_sl_hit(settings, mock_kraken):
# Setup open buy trade
with dbm.session(settings.db_path) as s:
trade = Trade(
symbol="BTC/USD:USD",
side="buy",
qty=1.5,
entry_price=60000.0,
stop_loss=59000.0,
take_profit=62000.0,
status="open"
)
s.add(trade)
s.commit()
s.refresh(trade)
trade_id = trade.id
# Under SL (58500 <= 59000)
closed = portfolio.check_stop_take_profit(settings, mock_kraken, "BTC/USD:USD", 58500.0)
assert closed == [trade_id]
mock_kraken.create_market_order.assert_called_once_with("BTC/USD:USD", "sell", 1.5)
# Check DB update
with dbm.session(settings.db_path) as s:
t = s.get(Trade, trade_id)
assert t.status == "closed"
assert t.exit_price == 58500.0
assert t.pnl_eur == (58500.0 - 60000.0) * 1.5
def test_check_stop_take_profit_buy_tp_hit(settings, mock_kraken):
with dbm.session(settings.db_path) as s:
trade = Trade(
symbol="BTC/USD:USD",
side="buy",
qty=1.0,
entry_price=60000.0,
stop_loss=59000.0,
take_profit=62000.0,
status="open"
)
s.add(trade)
s.commit()
s.refresh(trade)
trade_id = trade.id
# Above TP (62500 >= 62000)
closed = portfolio.check_stop_take_profit(settings, mock_kraken, "BTC/USD:USD", 62500.0)
assert closed == [trade_id]
mock_kraken.create_market_order.assert_called_once_with("BTC/USD:USD", "sell", 1.0)
# Check DB update
with dbm.session(settings.db_path) as s:
t = s.get(Trade, trade_id)
assert t.status == "closed"
assert t.exit_price == 62500.0
assert t.pnl_eur == (62500.0 - 60000.0) * 1.0
def test_check_stop_take_profit_no_hit(settings, mock_kraken):
with dbm.session(settings.db_path) as s:
trade = Trade(
symbol="BTC/USD:USD",
side="buy",
qty=1.0,
entry_price=60000.0,
stop_loss=59000.0,
take_profit=62000.0,
status="open"
)
s.add(trade)
s.commit()
s.refresh(trade)
trade_id = trade.id
# Price between SL and TP
closed = portfolio.check_stop_take_profit(settings, mock_kraken, "BTC/USD:USD", 60500.0)
assert closed == []
mock_kraken.create_market_order.assert_not_called()
# Check DB update
with dbm.session(settings.db_path) as s:
t = s.get(Trade, trade_id)
assert t.status == "open"
def test_check_stop_take_profit_sell_sl_hit(settings, mock_kraken):
# Setup open sell trade
with dbm.session(settings.db_path) as s:
trade = Trade(
symbol="ETH/USD:USD",
side="sell",
qty=2.0,
entry_price=3000.0,
stop_loss=3100.0,
take_profit=2800.0,
status="open"
)
s.add(trade)
s.commit()
s.refresh(trade)
trade_id = trade.id
# Above SL for Sell (3150 >= 3100)
closed = portfolio.check_stop_take_profit(settings, mock_kraken, "ETH/USD:USD", 3150.0)
assert closed == [trade_id]
mock_kraken.create_market_order.assert_called_once_with("ETH/USD:USD", "buy", 2.0)
# Check DB update
with dbm.session(settings.db_path) as s:
t = s.get(Trade, trade_id)
assert t.status == "closed"
assert t.exit_price == 3150.0
assert t.pnl_eur == -1 * (3150.0 - 3000.0) * 2.0
def test_check_stop_take_profit_exchange_fails(settings, mock_kraken):
mock_kraken.create_market_order.side_effect = Exception("Kraken API down")
with dbm.session(settings.db_path) as s:
trade = Trade(
symbol="BTC/USD:USD",
side="buy",
qty=1.0,
entry_price=60000.0,
stop_loss=59000.0,
take_profit=62000.0,
status="open"
)
s.add(trade)
s.commit()
s.refresh(trade)
trade_id = trade.id
# Under SL, but exchange order fails
closed = portfolio.check_stop_take_profit(settings, mock_kraken, "BTC/USD:USD", 58500.0)
assert closed == []
mock_kraken.create_market_order.assert_called_once_with("BTC/USD:USD", "sell", 1.0)
# DB record must still be open
with dbm.session(settings.db_path) as s:
t = s.get(Trade, trade_id)
assert t.status == "open"