173 lines
5.0 KiB
Python
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"
|