auto: 일일 백업 2026-09-22 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -61,8 +61,14 @@ class SessionTests(unittest.TestCase):
|
||||
|
||||
def test_nxt_after(self):
|
||||
self.assertEqual(guards.session_at(t(15, 30)), 'NXT_AFTER')
|
||||
self.assertEqual(guards.session_at(t(18, 0)), 'NXT_AFTER')
|
||||
self.assertEqual(guards.session_at(t(19, 59, 59)), 'NXT_AFTER')
|
||||
self.assertEqual(guards.session_at(t(15, 59, 59)), 'NXT_AFTER')
|
||||
|
||||
def test_krx_aftermarket(self):
|
||||
"""16:00~20:00 = KRX 애프터마켓 + NXT 애프터마켓 (2026-09-14 시행)."""
|
||||
self.assertEqual(guards.session_at(t(16, 0)), 'AFTER')
|
||||
self.assertEqual(guards.session_at(t(18, 0)), 'AFTER')
|
||||
self.assertEqual(guards.session_at(t(19, 59, 59)), 'AFTER')
|
||||
self.assertEqual(guards.session_at(t(20, 0)), 'CLOSED')
|
||||
|
||||
def test_closed_after_after(self):
|
||||
self.assertEqual(guards.session_at(t(20, 0)), 'CLOSED')
|
||||
@@ -94,9 +100,13 @@ class TradingHoursTests(unittest.TestCase):
|
||||
self.assertEqual(r.code, 'NXT_NOT_ELIGIBLE')
|
||||
|
||||
def test_nxt_after_not_eligible(self):
|
||||
r = guards.validate_trading_hours(t(18, 0), False, False)
|
||||
r = guards.validate_trading_hours(t(15, 45), False, False)
|
||||
self.assertEqual(r.code, 'NXT_NOT_ELIGIBLE')
|
||||
|
||||
def test_aftermarket_allows_non_nxt(self):
|
||||
"""애프터마켓엔 KRX 가 NXT 미상장 종목을 받는다 — 여기서 막으면 안 된다."""
|
||||
self.assertTrue(guards.validate_trading_hours(t(18, 0), False, False).ok)
|
||||
|
||||
def test_krx_close_no_nxt_required(self):
|
||||
self.assertTrue(guards.validate_trading_hours(t(15, 25), False, False).ok)
|
||||
|
||||
@@ -114,7 +124,12 @@ class RoutingTests(unittest.TestCase):
|
||||
self.assertEqual(guards.determine_routing(t(8, 30), True, None), '_NX')
|
||||
|
||||
def test_nxt_after(self):
|
||||
self.assertEqual(guards.determine_routing(t(18, 0), True, None), '_NX')
|
||||
self.assertEqual(guards.determine_routing(t(15, 45), True, None), '_NX')
|
||||
|
||||
def test_aftermarket_routes_to_krx(self):
|
||||
"""애프터마켓은 검증된 KRX 경로만 쓴다 — SOR 주문 라우팅은 미검증."""
|
||||
self.assertEqual(guards.determine_routing(t(18, 0), True, None), '')
|
||||
self.assertEqual(guards.determine_routing(t(18, 0), False, None), '')
|
||||
|
||||
def test_krx_close(self):
|
||||
self.assertEqual(guards.determine_routing(t(15, 25), False, None), '')
|
||||
@@ -133,6 +148,31 @@ class RoutingTests(unittest.TestCase):
|
||||
guards.determine_routing(t(21, 0), True, None)
|
||||
|
||||
|
||||
class StopRoutingTests(unittest.TestCase):
|
||||
"""스톱 계열은 SOR(_AL) 로 못 낸다 — 키움 407022 (2026-09-21 삼성전자 실측)."""
|
||||
|
||||
def test_stop_types_downgrade_al_to_krx(self):
|
||||
for ot in ('STOP_LIMIT', 'TRAILING_STOP', 'TRAILING_ARM'):
|
||||
self.assertEqual(guards.determine_routing(t(10, 0), True, None, ot), '', ot)
|
||||
|
||||
def test_force_al_also_downgraded(self):
|
||||
# force 는 사람의 명시 지정이지만 SOR 자체가 스톱을 안 받는다
|
||||
self.assertEqual(guards.determine_routing(t(10, 0), True, 'AL', 'STOP_LIMIT'), '')
|
||||
|
||||
def test_nxt_session_kept(self):
|
||||
# NXT 단독 시간대는 KRX 가 닫혀 강등할 곳이 없다 — 시도는 그대로 둔다
|
||||
self.assertEqual(guards.determine_routing(t(8, 30), True, None, 'STOP_LIMIT'), '_NX')
|
||||
self.assertEqual(guards.determine_routing(t(15, 45), True, None, 'STOP_LIMIT'), '_NX')
|
||||
|
||||
def test_non_stop_types_unaffected(self):
|
||||
self.assertEqual(guards.determine_routing(t(10, 0), True, None, 'LIMIT'), '_AL')
|
||||
self.assertEqual(guards.determine_routing(t(10, 0), True, None, 'MARKET'), '_AL')
|
||||
self.assertEqual(guards.determine_routing(t(10, 0), True, None, None), '_AL')
|
||||
|
||||
def test_non_nxt_symbol_already_krx(self):
|
||||
self.assertEqual(guards.determine_routing(t(10, 0), False, None, 'STOP_LIMIT'), '')
|
||||
|
||||
|
||||
# ---------- 가격 가드 ----------
|
||||
|
||||
class PriceBandTests(unittest.TestCase):
|
||||
|
||||
@@ -98,3 +98,46 @@ class SubmitResponseTests(unittest.TestCase):
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
|
||||
class IdempotencyMatchTests(unittest.TestCase):
|
||||
"""멱등성은 '이중 발주 위험이 실재하는' 이벤트만 본다.
|
||||
|
||||
2026-09-21: SOR 스톱 거부 직후 같은 주문을 다시 냈더니 IDEMPOTENCY_DUP 으로 또 막혔다.
|
||||
키움이 거부한 주문은 접수되지 않은 게 확정이라 재시도를 막을 이유가 없다.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
self.tmp = Path(tempfile.mkdtemp()) / 'order_log.jsonl'
|
||||
mock.patch.object(ledger, '_ledger_path', return_value=self.tmp).start()
|
||||
self.addCleanup(mock.patch.stopall)
|
||||
|
||||
def _write(self, event: str, h: str = 'HASH1'):
|
||||
import json
|
||||
from datetime import datetime
|
||||
with self.tmp.open('a', encoding='utf-8') as f:
|
||||
f.write(json.dumps({'ts': datetime.now(ledger.KST).isoformat(),
|
||||
'event': event, 'payload': {'idem_hash': h}}) + '\n')
|
||||
|
||||
def test_submitted_blocks(self):
|
||||
self._write('submitted')
|
||||
self.assertIsNotNone(ledger.find_recent_idempotency('HASH1'))
|
||||
|
||||
def test_network_failure_blocks(self):
|
||||
"""나갔는지 모르는 상태 — 가장 위험하므로 반드시 차단."""
|
||||
self._write('failed')
|
||||
self.assertIsNotNone(ledger.find_recent_idempotency('HASH1'))
|
||||
|
||||
def test_broker_reject_does_not_block(self):
|
||||
self._write('rejected')
|
||||
self.assertIsNone(ledger.find_recent_idempotency('HASH1'))
|
||||
|
||||
def test_dryrun_does_not_block(self):
|
||||
self._write('dryrun')
|
||||
self.assertIsNone(ledger.find_recent_idempotency('HASH1'))
|
||||
|
||||
def test_other_hash_ignored(self):
|
||||
self._write('submitted', h='OTHER')
|
||||
self.assertIsNone(ledger.find_recent_idempotency('HASH1'))
|
||||
|
||||
@@ -723,7 +723,11 @@ class ArmMonitorTests(unittest.TestCase):
|
||||
tm.sidecar.is_disabled = lambda: self.state['disabled']
|
||||
tm.ledger.append = lambda ev, payload: None
|
||||
tm.datasource.nxt_eligible = lambda code: True
|
||||
tm.guards.determine_routing = lambda now, nxt, force: '_AL'
|
||||
# 발동 레그는 스톱지정가라 SOR(_AL) 로 나갈 수 없다 — 실제 determine_routing 이
|
||||
# KRX 로 강등한다. 스텁도 같은 값을 주고, order_type 이 실제로 넘어오는지 기록한다.
|
||||
self.routing_calls = []
|
||||
tm.guards.determine_routing = lambda now, nxt, force, order_type=None: (
|
||||
self.routing_calls.append(order_type) or '')
|
||||
|
||||
self.steps = t.normalize_steps([{'pct': 10, 'cum': 20}, {'pct': 20, 'cum': 50},
|
||||
{'pct': 30, 'cum': 100}])
|
||||
@@ -760,7 +764,9 @@ class ArmMonitorTests(unittest.TestCase):
|
||||
self.state['quote'] = 81000
|
||||
self.tm.check(force=True)
|
||||
self.assertEqual(len(self.submits), 3)
|
||||
self.assertEqual([s['routing'] for s in self.submits], ['_AL'] * 3)
|
||||
self.assertEqual([s['routing'] for s in self.submits], [''] * 3)
|
||||
# 발동 레그는 스톱지정가 — SOR 로 내면 키움이 407022 로 거부한다
|
||||
self.assertEqual(self.routing_calls, ['STOP_LIMIT'])
|
||||
self.assertEqual(t.list_arms(), [])
|
||||
self.assertEqual(len(t.list_active()), 1)
|
||||
self.assertEqual(t.list_active()[0]['peak'], 81000) # 고점 = 발동 시점 현재가
|
||||
|
||||
Reference in New Issue
Block a user