136 lines
4.6 KiB
Python
136 lines
4.6 KiB
Python
"""
|
|
Unit tests for TradingPortfolio.
|
|
"""
|
|
from decimal import Decimal
|
|
from datetime import date
|
|
|
|
from app.services.backtest.trading_portfolio import TradingPortfolio
|
|
|
|
|
|
def test_initial_state():
|
|
tp = TradingPortfolio(Decimal("10000000"))
|
|
assert tp.cash == Decimal("10000000")
|
|
assert tp.investable_capital == Decimal("7000000")
|
|
assert len(tp.positions) == 0
|
|
|
|
|
|
def test_enter_position():
|
|
tp = TradingPortfolio(Decimal("10000000"))
|
|
txn = tp.enter_position(
|
|
ticker="005930",
|
|
price=Decimal("70000"),
|
|
date=date(2024, 1, 2),
|
|
commission_rate=Decimal("0.00015"),
|
|
slippage_rate=Decimal("0.001"),
|
|
)
|
|
assert txn is not None
|
|
assert txn.action == "buy"
|
|
assert "005930" in tp.positions
|
|
pos = tp.positions["005930"]
|
|
assert pos.entry_price == Decimal("70000")
|
|
assert pos.stop_loss == Decimal("67900") # -3%
|
|
assert pos.target1 == Decimal("73500") # +5%
|
|
assert pos.target2 == Decimal("77000") # +10%
|
|
|
|
|
|
def test_max_positions():
|
|
tp = TradingPortfolio(Decimal("10000000"), max_positions=2)
|
|
tp.enter_position("A", Decimal("1000"), date(2024, 1, 2), Decimal("0"), Decimal("0"))
|
|
tp.enter_position("B", Decimal("1000"), date(2024, 1, 2), Decimal("0"), Decimal("0"))
|
|
txn = tp.enter_position("C", Decimal("1000"), date(2024, 1, 2), Decimal("0"), Decimal("0"))
|
|
assert txn is None
|
|
assert len(tp.positions) == 2
|
|
|
|
|
|
def test_stop_loss_exit():
|
|
tp = TradingPortfolio(Decimal("10000000"))
|
|
tp.enter_position("005930", Decimal("70000"), date(2024, 1, 2), Decimal("0"), Decimal("0"))
|
|
exits = tp.check_exits(
|
|
date=date(2024, 1, 3),
|
|
prices={"005930": Decimal("67000")},
|
|
commission_rate=Decimal("0"),
|
|
slippage_rate=Decimal("0"),
|
|
)
|
|
assert len(exits) == 1
|
|
assert exits[0].action == "sell"
|
|
assert "005930" not in tp.positions
|
|
|
|
|
|
def test_partial_take_profit():
|
|
tp = TradingPortfolio(Decimal("10000000"))
|
|
tp.enter_position("005930", Decimal("70000"), date(2024, 1, 2), Decimal("0"), Decimal("0"))
|
|
pos = tp.positions["005930"]
|
|
initial_shares = pos.shares
|
|
|
|
exits = tp.check_exits(
|
|
date=date(2024, 1, 10),
|
|
prices={"005930": Decimal("73500")},
|
|
commission_rate=Decimal("0"),
|
|
slippage_rate=Decimal("0"),
|
|
)
|
|
assert len(exits) == 1
|
|
assert exits[0].action == "partial_sell"
|
|
assert exits[0].shares == initial_shares // 2
|
|
assert "005930" in tp.positions
|
|
assert tp.positions["005930"].stop_loss == Decimal("70000")
|
|
|
|
|
|
def test_full_take_profit():
|
|
tp = TradingPortfolio(Decimal("10000000"))
|
|
tp.enter_position("005930", Decimal("70000"), date(2024, 1, 2), Decimal("0"), Decimal("0"))
|
|
tp.check_exits(
|
|
date=date(2024, 1, 10),
|
|
prices={"005930": Decimal("73500")},
|
|
commission_rate=Decimal("0"),
|
|
slippage_rate=Decimal("0"),
|
|
)
|
|
exits = tp.check_exits(
|
|
date=date(2024, 1, 15),
|
|
prices={"005930": Decimal("77000")},
|
|
commission_rate=Decimal("0"),
|
|
slippage_rate=Decimal("0"),
|
|
)
|
|
assert len(exits) == 1
|
|
assert exits[0].action == "sell"
|
|
assert "005930" not in tp.positions
|
|
|
|
|
|
def test_trailing_stop_after_partial():
|
|
tp = TradingPortfolio(Decimal("10000000"))
|
|
tp.enter_position("005930", Decimal("70000"), date(2024, 1, 2), Decimal("0"), Decimal("0"))
|
|
tp.check_exits(
|
|
date=date(2024, 1, 10),
|
|
prices={"005930": Decimal("73500")},
|
|
commission_rate=Decimal("0"),
|
|
slippage_rate=Decimal("0"),
|
|
)
|
|
# After partial sell, stop should be at entry (70000)
|
|
assert tp.positions["005930"].stop_loss == Decimal("70000")
|
|
# Price rises to +8%, stop stays at entry
|
|
tp.check_exits(
|
|
date=date(2024, 1, 12),
|
|
prices={"005930": Decimal("75600")},
|
|
commission_rate=Decimal("0"),
|
|
slippage_rate=Decimal("0"),
|
|
)
|
|
assert tp.positions["005930"].stop_loss == Decimal("70000")
|
|
|
|
|
|
def test_cash_reserve():
|
|
tp = TradingPortfolio(Decimal("10000000"), cash_reserve_ratio=Decimal("0.3"))
|
|
assert tp.investable_capital == Decimal("7000000")
|
|
|
|
|
|
def test_get_portfolio_value():
|
|
tp = TradingPortfolio(Decimal("10000000"))
|
|
tp.enter_position("005930", Decimal("70000"), date(2024, 1, 2), Decimal("0"), Decimal("0"))
|
|
value = tp.get_value({"005930": Decimal("75000")})
|
|
assert value > Decimal("10000000")
|
|
|
|
|
|
def test_duplicate_entry_rejected():
|
|
tp = TradingPortfolio(Decimal("10000000"))
|
|
tp.enter_position("005930", Decimal("70000"), date(2024, 1, 2), Decimal("0"), Decimal("0"))
|
|
txn = tp.enter_position("005930", Decimal("70000"), date(2024, 1, 3), Decimal("0"), Decimal("0"))
|
|
assert txn is None
|