Compare commits
2 Commits
feature/is
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce5ea5abde | ||
| 5ae302b083 |
36
src/main.py
36
src/main.py
@@ -182,6 +182,9 @@ async def sync_positions_from_broker(
|
|||||||
qty = _extract_held_qty_from_balance(
|
qty = _extract_held_qty_from_balance(
|
||||||
balance_data, stock_code, is_domestic=market.is_domestic
|
balance_data, stock_code, is_domestic=market.is_domestic
|
||||||
)
|
)
|
||||||
|
avg_price = _extract_avg_price_from_balance(
|
||||||
|
balance_data, stock_code, is_domestic=market.is_domestic
|
||||||
|
)
|
||||||
log_trade(
|
log_trade(
|
||||||
conn=db_conn,
|
conn=db_conn,
|
||||||
stock_code=stock_code,
|
stock_code=stock_code,
|
||||||
@@ -189,7 +192,7 @@ async def sync_positions_from_broker(
|
|||||||
confidence=0,
|
confidence=0,
|
||||||
rationale="[startup-sync] Position detected from broker at startup",
|
rationale="[startup-sync] Position detected from broker at startup",
|
||||||
quantity=qty,
|
quantity=qty,
|
||||||
price=0.0,
|
price=avg_price,
|
||||||
market=log_market,
|
market=log_market,
|
||||||
exchange_code=market.exchange_code,
|
exchange_code=market.exchange_code,
|
||||||
mode=settings.MODE,
|
mode=settings.MODE,
|
||||||
@@ -321,6 +324,37 @@ def _extract_held_qty_from_balance(
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_avg_price_from_balance(
|
||||||
|
balance_data: dict[str, Any],
|
||||||
|
stock_code: str,
|
||||||
|
*,
|
||||||
|
is_domestic: bool,
|
||||||
|
) -> float:
|
||||||
|
"""Extract the broker-reported average purchase price for a stock.
|
||||||
|
|
||||||
|
Uses ``pchs_avg_pric`` (매입평균가격) from the balance response (output1).
|
||||||
|
Returns 0.0 when absent so callers can use ``if price > 0`` as sentinel.
|
||||||
|
|
||||||
|
Domestic fields (VTTC8434R output1): pdno, pchs_avg_pric
|
||||||
|
Overseas fields (VTTS3012R output1): ovrs_pdno, pchs_avg_pric
|
||||||
|
"""
|
||||||
|
output1 = balance_data.get("output1", [])
|
||||||
|
if isinstance(output1, dict):
|
||||||
|
output1 = [output1]
|
||||||
|
if not isinstance(output1, list):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
for holding in output1:
|
||||||
|
if not isinstance(holding, dict):
|
||||||
|
continue
|
||||||
|
code_key = "pdno" if is_domestic else "ovrs_pdno"
|
||||||
|
held_code = str(holding.get(code_key, "")).strip().upper()
|
||||||
|
if held_code != stock_code.strip().upper():
|
||||||
|
continue
|
||||||
|
return safe_float(holding.get("pchs_avg_pric"), 0.0)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
def _determine_order_quantity(
|
def _determine_order_quantity(
|
||||||
*,
|
*,
|
||||||
action: str,
|
action: str,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from src.logging.decision_logger import DecisionLogger
|
|||||||
from src.main import (
|
from src.main import (
|
||||||
_apply_dashboard_flag,
|
_apply_dashboard_flag,
|
||||||
_determine_order_quantity,
|
_determine_order_quantity,
|
||||||
|
_extract_avg_price_from_balance,
|
||||||
_extract_held_codes_from_balance,
|
_extract_held_codes_from_balance,
|
||||||
_extract_held_qty_from_balance,
|
_extract_held_qty_from_balance,
|
||||||
_handle_market_close,
|
_handle_market_close,
|
||||||
@@ -76,6 +77,81 @@ def _make_sell_match(stock_code: str = "005930") -> ScenarioMatch:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractAvgPriceFromBalance:
|
||||||
|
"""Tests for _extract_avg_price_from_balance() (issue #249)."""
|
||||||
|
|
||||||
|
def test_domestic_returns_pchs_avg_pric(self) -> None:
|
||||||
|
"""Domestic balance with pchs_avg_pric returns the correct float."""
|
||||||
|
balance = {"output1": [{"pdno": "005930", "pchs_avg_pric": "68000.00"}]}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "005930", is_domestic=True)
|
||||||
|
assert result == 68000.0
|
||||||
|
|
||||||
|
def test_overseas_returns_pchs_avg_pric(self) -> None:
|
||||||
|
"""Overseas balance with pchs_avg_pric returns the correct float."""
|
||||||
|
balance = {"output1": [{"ovrs_pdno": "AAPL", "pchs_avg_pric": "170.50"}]}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "AAPL", is_domestic=False)
|
||||||
|
assert result == 170.5
|
||||||
|
|
||||||
|
def test_returns_zero_when_field_absent(self) -> None:
|
||||||
|
"""Returns 0.0 when pchs_avg_pric key is missing entirely."""
|
||||||
|
balance = {"output1": [{"pdno": "005930", "ord_psbl_qty": "5"}]}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "005930", is_domestic=True)
|
||||||
|
assert result == 0.0
|
||||||
|
|
||||||
|
def test_returns_zero_when_field_empty_string(self) -> None:
|
||||||
|
"""Returns 0.0 when pchs_avg_pric is an empty string."""
|
||||||
|
balance = {"output1": [{"pdno": "005930", "pchs_avg_pric": ""}]}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "005930", is_domestic=True)
|
||||||
|
assert result == 0.0
|
||||||
|
|
||||||
|
def test_returns_zero_when_stock_not_found(self) -> None:
|
||||||
|
"""Returns 0.0 when the requested stock_code is not in output1."""
|
||||||
|
balance = {"output1": [{"pdno": "000660", "pchs_avg_pric": "100000.0"}]}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "005930", is_domestic=True)
|
||||||
|
assert result == 0.0
|
||||||
|
|
||||||
|
def test_returns_zero_when_output1_empty(self) -> None:
|
||||||
|
"""Returns 0.0 when output1 is an empty list."""
|
||||||
|
balance = {"output1": []}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "005930", is_domestic=True)
|
||||||
|
assert result == 0.0
|
||||||
|
|
||||||
|
def test_returns_zero_when_output1_key_absent(self) -> None:
|
||||||
|
"""Returns 0.0 when output1 key is missing from balance_data."""
|
||||||
|
balance: dict = {}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "005930", is_domestic=True)
|
||||||
|
assert result == 0.0
|
||||||
|
|
||||||
|
def test_handles_output1_as_dict(self) -> None:
|
||||||
|
"""Handles the edge case where output1 is a dict instead of a list."""
|
||||||
|
balance = {"output1": {"pdno": "005930", "pchs_avg_pric": "55000.0"}}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "005930", is_domestic=True)
|
||||||
|
assert result == 55000.0
|
||||||
|
|
||||||
|
def test_case_insensitive_code_matching(self) -> None:
|
||||||
|
"""Stock code comparison is case-insensitive."""
|
||||||
|
balance = {"output1": [{"ovrs_pdno": "aapl", "pchs_avg_pric": "170.0"}]}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "AAPL", is_domestic=False)
|
||||||
|
assert result == 170.0
|
||||||
|
|
||||||
|
def test_returns_zero_for_non_numeric_string(self) -> None:
|
||||||
|
"""Returns 0.0 when pchs_avg_pric contains a non-numeric value."""
|
||||||
|
balance = {"output1": [{"pdno": "005930", "pchs_avg_pric": "N/A"}]}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "005930", is_domestic=True)
|
||||||
|
assert result == 0.0
|
||||||
|
|
||||||
|
def test_returns_correct_stock_among_multiple(self) -> None:
|
||||||
|
"""Returns only the avg price of the requested stock when output1 has multiple holdings."""
|
||||||
|
balance = {
|
||||||
|
"output1": [
|
||||||
|
{"pdno": "000660", "pchs_avg_pric": "150000.0"},
|
||||||
|
{"pdno": "005930", "pchs_avg_pric": "68000.0"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
result = _extract_avg_price_from_balance(balance, "005930", is_domestic=True)
|
||||||
|
assert result == 68000.0
|
||||||
|
|
||||||
|
|
||||||
class TestExtractHeldQtyFromBalance:
|
class TestExtractHeldQtyFromBalance:
|
||||||
"""Tests for _extract_held_qty_from_balance()."""
|
"""Tests for _extract_held_qty_from_balance()."""
|
||||||
|
|
||||||
@@ -3818,6 +3894,70 @@ class TestSyncPositionsFromBroker:
|
|||||||
# Two distinct exchange codes (NASD, NYSE) → 2 calls
|
# Two distinct exchange codes (NASD, NYSE) → 2 calls
|
||||||
assert overseas_broker.get_overseas_balance.call_count == 2
|
assert overseas_broker.get_overseas_balance.call_count == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_syncs_domestic_position_with_correct_avg_price(self) -> None:
|
||||||
|
"""Domestic position is stored with pchs_avg_pric as price (issue #249)."""
|
||||||
|
settings = self._make_settings("KR")
|
||||||
|
db_conn = init_db(":memory:")
|
||||||
|
|
||||||
|
balance = {
|
||||||
|
"output1": [{"pdno": "005930", "ord_psbl_qty": "5", "pchs_avg_pric": "68000.0"}],
|
||||||
|
"output2": [{"tot_evlu_amt": "1000000", "dnca_tot_amt": "500000", "pchs_amt_smtl_amt": "500000"}],
|
||||||
|
}
|
||||||
|
broker = MagicMock()
|
||||||
|
broker.get_balance = AsyncMock(return_value=balance)
|
||||||
|
overseas_broker = MagicMock()
|
||||||
|
|
||||||
|
await sync_positions_from_broker(broker, overseas_broker, db_conn, settings)
|
||||||
|
|
||||||
|
from src.db import get_open_position
|
||||||
|
pos = get_open_position(db_conn, "005930", "KR")
|
||||||
|
assert pos is not None
|
||||||
|
assert pos["price"] == 68000.0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_syncs_overseas_position_with_correct_avg_price(self) -> None:
|
||||||
|
"""Overseas position is stored with pchs_avg_pric as price (issue #249)."""
|
||||||
|
settings = self._make_settings("US_NASDAQ")
|
||||||
|
db_conn = init_db(":memory:")
|
||||||
|
|
||||||
|
balance = {
|
||||||
|
"output1": [{"ovrs_pdno": "AAPL", "ovrs_cblc_qty": "10", "pchs_avg_pric": "170.0"}],
|
||||||
|
"output2": [{"frcr_evlu_tota": "50000", "frcr_dncl_amt_2": "10000", "frcr_buy_amt_smtl": "40000"}],
|
||||||
|
}
|
||||||
|
broker = MagicMock()
|
||||||
|
overseas_broker = MagicMock()
|
||||||
|
overseas_broker.get_overseas_balance = AsyncMock(return_value=balance)
|
||||||
|
|
||||||
|
await sync_positions_from_broker(broker, overseas_broker, db_conn, settings)
|
||||||
|
|
||||||
|
from src.db import get_open_position
|
||||||
|
pos = get_open_position(db_conn, "AAPL", "US_NASDAQ")
|
||||||
|
assert pos is not None
|
||||||
|
assert pos["price"] == 170.0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_syncs_position_with_zero_price_when_pchs_avg_pric_absent(self) -> None:
|
||||||
|
"""Fallback to price=0.0 when pchs_avg_pric is absent (issue #249)."""
|
||||||
|
settings = self._make_settings("KR")
|
||||||
|
db_conn = init_db(":memory:")
|
||||||
|
|
||||||
|
# No pchs_avg_pric in output1
|
||||||
|
balance = {
|
||||||
|
"output1": [{"pdno": "005930", "ord_psbl_qty": "5"}],
|
||||||
|
"output2": [{"tot_evlu_amt": "1000000", "dnca_tot_amt": "500000", "pchs_amt_smtl_amt": "500000"}],
|
||||||
|
}
|
||||||
|
broker = MagicMock()
|
||||||
|
broker.get_balance = AsyncMock(return_value=balance)
|
||||||
|
overseas_broker = MagicMock()
|
||||||
|
|
||||||
|
await sync_positions_from_broker(broker, overseas_broker, db_conn, settings)
|
||||||
|
|
||||||
|
from src.db import get_open_position
|
||||||
|
pos = get_open_position(db_conn, "005930", "KR")
|
||||||
|
assert pos is not None
|
||||||
|
assert pos["price"] == 0.0
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Domestic BUY double-prevention (issue #206) — trading_cycle integration
|
# Domestic BUY double-prevention (issue #206) — trading_cycle integration
|
||||||
|
|||||||
Reference in New Issue
Block a user