Compare commits
3 Commits
feature/is
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9267f1fb77 | ||
|
|
08607eaa56 | ||
| 13a6d6612a |
@@ -60,6 +60,7 @@ class Settings(BaseSettings):
|
||||
# This value is used as a fallback when the balance API returns 0 in paper mode.
|
||||
PAPER_OVERSEAS_CASH: float = Field(default=50000.0, ge=0.0)
|
||||
USD_BUFFER_MIN: float = Field(default=1000.0, ge=0.0)
|
||||
US_MIN_PRICE: float = Field(default=5.0, ge=0.0)
|
||||
OVERNIGHT_EXCEPTION_ENABLED: bool = True
|
||||
|
||||
# Trading frequency mode (daily = batch API calls, realtime = per-stock calls)
|
||||
|
||||
36
src/main.py
36
src/main.py
@@ -1291,6 +1291,24 @@ async def trading_cycle(
|
||||
stock_code,
|
||||
market.name,
|
||||
)
|
||||
elif market.code.startswith("US"):
|
||||
min_price = float(getattr(settings, "US_MIN_PRICE", 5.0) if settings else 5.0)
|
||||
if current_price <= min_price:
|
||||
decision = TradeDecision(
|
||||
action="HOLD",
|
||||
confidence=decision.confidence,
|
||||
rationale=(
|
||||
f"US minimum price filter blocked BUY "
|
||||
f"(price={current_price:.4f} <= {min_price:.4f})"
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
"BUY suppressed for %s (%s): US min price filter %.4f <= %.4f",
|
||||
stock_code,
|
||||
market.name,
|
||||
current_price,
|
||||
min_price,
|
||||
)
|
||||
|
||||
if decision.action == "HOLD":
|
||||
open_position = get_open_position(db_conn, stock_code, market.code)
|
||||
@@ -2442,6 +2460,24 @@ async def run_daily_session(
|
||||
stock_code,
|
||||
market.name,
|
||||
)
|
||||
elif market.code.startswith("US"):
|
||||
min_price = float(getattr(settings, "US_MIN_PRICE", 5.0))
|
||||
if stock_data["current_price"] <= min_price:
|
||||
decision = TradeDecision(
|
||||
action="HOLD",
|
||||
confidence=decision.confidence,
|
||||
rationale=(
|
||||
f"US minimum price filter blocked BUY "
|
||||
f"(price={stock_data['current_price']:.4f} <= {min_price:.4f})"
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
"BUY suppressed for %s (%s): US min price filter %.4f <= %.4f",
|
||||
stock_code,
|
||||
market.name,
|
||||
stock_data["current_price"],
|
||||
min_price,
|
||||
)
|
||||
if decision.action == "HOLD":
|
||||
daily_open = get_open_position(db_conn, stock_code, market.code)
|
||||
if not daily_open:
|
||||
|
||||
@@ -5654,6 +5654,149 @@ async def test_order_policy_rejection_skips_order_execution() -> None:
|
||||
broker.send_order.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("price", "should_block"),
|
||||
[
|
||||
(4.99, True),
|
||||
(5.00, True),
|
||||
(5.01, False),
|
||||
],
|
||||
)
|
||||
async def test_us_min_price_filter_boundary(price: float, should_block: bool) -> None:
|
||||
db_conn = init_db(":memory:")
|
||||
decision_logger = DecisionLogger(db_conn)
|
||||
|
||||
broker = MagicMock()
|
||||
broker.get_balance = AsyncMock(return_value={"output1": [], "output2": [{}]})
|
||||
|
||||
overseas_broker = MagicMock()
|
||||
overseas_broker.get_overseas_price = AsyncMock(
|
||||
return_value={"output": {"last": str(price), "rate": "0.0"}}
|
||||
)
|
||||
overseas_broker.get_overseas_balance = AsyncMock(
|
||||
return_value={"output1": [], "output2": [{"frcr_evlu_tota": "10000", "frcr_buy_amt_smtl": "0"}]}
|
||||
)
|
||||
overseas_broker.get_overseas_buying_power = AsyncMock(
|
||||
return_value={"output": {"ovrs_ord_psbl_amt": "10000"}}
|
||||
)
|
||||
overseas_broker.send_overseas_order = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
|
||||
|
||||
market = MagicMock()
|
||||
market.name = "NASDAQ"
|
||||
market.code = "US_NASDAQ"
|
||||
market.exchange_code = "NASD"
|
||||
market.is_domestic = False
|
||||
|
||||
telegram = MagicMock()
|
||||
telegram.notify_trade_execution = AsyncMock()
|
||||
telegram.notify_fat_finger = AsyncMock()
|
||||
telegram.notify_circuit_breaker = AsyncMock()
|
||||
telegram.notify_scenario_matched = AsyncMock()
|
||||
|
||||
settings = MagicMock()
|
||||
settings.POSITION_SIZING_ENABLED = False
|
||||
settings.CONFIDENCE_THRESHOLD = 80
|
||||
settings.MODE = "paper"
|
||||
settings.PAPER_OVERSEAS_CASH = 50000
|
||||
settings.US_MIN_PRICE = 5.0
|
||||
settings.USD_BUFFER_MIN = 1000.0
|
||||
|
||||
await trading_cycle(
|
||||
broker=broker,
|
||||
overseas_broker=overseas_broker,
|
||||
scenario_engine=MagicMock(evaluate=MagicMock(return_value=_make_buy_match("AAPL"))),
|
||||
playbook=_make_playbook("US_NASDAQ"),
|
||||
risk=MagicMock(validate_order=MagicMock(), check_circuit_breaker=MagicMock()),
|
||||
db_conn=db_conn,
|
||||
decision_logger=decision_logger,
|
||||
context_store=MagicMock(
|
||||
get_latest_timeframe=MagicMock(return_value=None),
|
||||
set_context=MagicMock(),
|
||||
),
|
||||
criticality_assessor=MagicMock(
|
||||
assess_market_conditions=MagicMock(return_value=MagicMock(value="NORMAL")),
|
||||
get_timeout=MagicMock(return_value=5.0),
|
||||
),
|
||||
telegram=telegram,
|
||||
market=market,
|
||||
stock_code="AAPL",
|
||||
scan_candidates={},
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
if should_block:
|
||||
overseas_broker.send_overseas_order.assert_not_called()
|
||||
else:
|
||||
overseas_broker.send_overseas_order.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_us_min_price_filter_not_applied_to_kr_market() -> None:
|
||||
db_conn = init_db(":memory:")
|
||||
decision_logger = DecisionLogger(db_conn)
|
||||
|
||||
broker = MagicMock()
|
||||
broker.get_current_price = AsyncMock(return_value=(4.0, 0.0, 0.0))
|
||||
broker.get_balance = AsyncMock(
|
||||
return_value={
|
||||
"output1": [],
|
||||
"output2": [
|
||||
{
|
||||
"tot_evlu_amt": "100000",
|
||||
"dnca_tot_amt": "50000",
|
||||
"pchs_amt_smtl_amt": "50000",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
broker.send_order = AsyncMock(return_value={"msg1": "OK"})
|
||||
|
||||
market = MagicMock()
|
||||
market.name = "Korea"
|
||||
market.code = "KR"
|
||||
market.exchange_code = "KRX"
|
||||
market.is_domestic = True
|
||||
|
||||
telegram = MagicMock()
|
||||
telegram.notify_trade_execution = AsyncMock()
|
||||
telegram.notify_fat_finger = AsyncMock()
|
||||
telegram.notify_circuit_breaker = AsyncMock()
|
||||
telegram.notify_scenario_matched = AsyncMock()
|
||||
|
||||
settings = MagicMock()
|
||||
settings.POSITION_SIZING_ENABLED = False
|
||||
settings.CONFIDENCE_THRESHOLD = 80
|
||||
settings.MODE = "paper"
|
||||
settings.US_MIN_PRICE = 5.0
|
||||
settings.USD_BUFFER_MIN = 1000.0
|
||||
|
||||
await trading_cycle(
|
||||
broker=broker,
|
||||
overseas_broker=MagicMock(),
|
||||
scenario_engine=MagicMock(evaluate=MagicMock(return_value=_make_buy_match("005930"))),
|
||||
playbook=_make_playbook(),
|
||||
risk=MagicMock(validate_order=MagicMock(), check_circuit_breaker=MagicMock()),
|
||||
db_conn=db_conn,
|
||||
decision_logger=decision_logger,
|
||||
context_store=MagicMock(
|
||||
get_latest_timeframe=MagicMock(return_value=None),
|
||||
set_context=MagicMock(),
|
||||
),
|
||||
criticality_assessor=MagicMock(
|
||||
assess_market_conditions=MagicMock(return_value=MagicMock(value="NORMAL")),
|
||||
get_timeout=MagicMock(return_value=5.0),
|
||||
),
|
||||
telegram=telegram,
|
||||
market=market,
|
||||
stock_code="005930",
|
||||
scan_candidates={},
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
broker.send_order.assert_called_once()
|
||||
|
||||
|
||||
def test_overnight_policy_prioritizes_killswitch_over_exception() -> None:
|
||||
market = MagicMock()
|
||||
with patch("src.main.get_session_info", return_value=MagicMock(session_id="US_AFTER")):
|
||||
|
||||
Reference in New Issue
Block a user