feat: isolate strategy-owned KIS balances

This commit is contained in:
jordan
2026-07-27 11:39:07 +09:00
parent 646ece93cb
commit b72bf9f394
7 changed files with 345 additions and 2 deletions
+17
View File
@@ -110,3 +110,20 @@ def test_get_balance_sanitizes_token_issuance_failure(monkeypatch):
b.get_balance()
assert raised.value.code == "token_issuance_failed"
assert "secret" not in str(raised.value)
def test_get_available_cash_uses_cash_only_amount(monkeypatch):
b = KISBroker("12345678-01", app_key="k", app_secret="s", base_url=_MOCK_URL)
monkeypatch.setattr(b, "_headers", lambda _tr: {})
monkeypatch.setattr(b, "get_current_price", lambda _symbol: None)
class Response:
def raise_for_status(self): pass
def json(self):
return {
"rt_cd": "0",
"output": {"nrcvb_buy_amt": "695591", "max_buy_amt": "999999"},
}
monkeypatch.setattr("trex_core.adapters.broker.kis.httpx.get", lambda *a, **kw: Response())
assert b.get_available_cash() == 695_591
+90
View File
@@ -0,0 +1,90 @@
from decimal import Decimal
import pytest
from trex_core.adapters.broker.ownership import (
OwnershipAwareBroker,
StrategyOwnershipStore,
)
from trex_core.application.broker_port import (
BalanceItem,
BrokerBalanceError,
FillStatus,
OrderAck,
)
class FakeBroker:
def __init__(self, balance, fill=None):
self.balance = balance
self.fill = fill or FillStatus(state="accepted")
def name(self):
return "fake"
def submit_order(self, req):
return OrderAck(accepted=True, broker_order_id="1")
def poll_fill(self, broker_order_id, *, side, symbol):
return self.fill
def cancel_order(self, broker_order_id, *, side, symbol, qty, venue="KRX"):
return True
def get_balance(self):
return self.balance
def get_current_price(self, symbol, market="J"):
return Decimal("1")
def test_manual_balance_is_hidden_until_trex_fill(tmp_path):
store = StrategyOwnershipStore(tmp_path / "ownership.json")
broker = OwnershipAwareBroker(
FakeBroker([BalanceItem("005930", 19), BalanceItem("010120", 5)]),
store,
)
assert broker.get_balance() == []
def test_confirmed_fill_is_persisted_once_and_manual_remainder_stays_hidden(tmp_path):
store = StrategyOwnershipStore(tmp_path / "ownership.json")
fill = FillStatus(
state="filled",
filled_qty=2,
execution_id="kis:1:2",
)
delegate = FakeBroker([BalanceItem("005930", 21)], fill)
broker = OwnershipAwareBroker(delegate, store)
broker.poll_fill("1", side="buy", symbol="005930")
broker.poll_fill("1", side="buy", symbol="005930")
assert broker.get_balance() == [BalanceItem("005930", 2)]
assert StrategyOwnershipStore(store.path).snapshot()[0] == {"005930": 2}
def test_strategy_quantity_above_raw_broker_total_fails_closed(tmp_path):
store = StrategyOwnershipStore(tmp_path / "ownership.json")
store.apply_fill(
execution_id="buy-1",
symbol="005930",
side="buy",
quantity=4,
)
broker = OwnershipAwareBroker(FakeBroker([BalanceItem("005930", 3)]), store)
with pytest.raises(BrokerBalanceError) as exc:
broker.get_balance()
assert exc.value.code == "ownership_mismatch"
def test_sell_cannot_reduce_below_strategy_owned_zero(tmp_path):
store = StrategyOwnershipStore(tmp_path / "ownership.json")
with pytest.raises(BrokerBalanceError):
store.apply_fill(
execution_id="sell-1",
symbol="005930",
side="sell",
quantity=1,
)