feat: propagate intent venue through execution
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# BAT-059 intent venue fan-in
|
||||
|
||||
- Profile: standard
|
||||
- Status: release-candidate
|
||||
- Discovery: dcy-039
|
||||
- Included REQ: REQ-124
|
||||
|
||||
전략 wire의 venue를 슬롯과 브로커 주문·취소까지 additive로 연결한다.
|
||||
배포와 실주문은 이 batch 범위가 아니다.
|
||||
@@ -0,0 +1,8 @@
|
||||
# BAT-059 Design
|
||||
|
||||
`Slot.venue`는 현재 pending 주문의 거래소 구분값이다. admission이 허용값을
|
||||
검증하고 pending 전이에 저장한다. HOLDING에는 마지막 주문값이 남지만 sell intent가
|
||||
현재 청산 venue로 덮어쓴다. INACTIVE에서는 KRX로 초기화한다.
|
||||
|
||||
기본 KRX는 기존 전략의 wire·stub·운영 동작을 보존한다. NXT 취소만 venue keyword를
|
||||
명시해 이전 테스트 double과의 하위 호환도 유지한다.
|
||||
@@ -0,0 +1,7 @@
|
||||
# BAT-059 Implementation
|
||||
|
||||
- `BuyIntent`/`SellIntent`와 handler venue 추가
|
||||
- `Slot.venue`, migration 017, PostgreSQL mapping 추가
|
||||
- `ExecutionService` 주문·취소 venue 전달
|
||||
- invalid venue 사전 거부
|
||||
- 전용 NXT flow 테스트 추가
|
||||
@@ -0,0 +1,7 @@
|
||||
# BAT-059 Planning
|
||||
|
||||
1. venue wire·domain 계약 추가
|
||||
2. 슬롯 persistence와 migration 추가
|
||||
3. execution submit/cancel 전달
|
||||
4. NXT 신규 테스트와 전체 KRX 회귀
|
||||
5. interface/data/runtime 문서 동기화
|
||||
@@ -0,0 +1,10 @@
|
||||
# BAT-059 Verification
|
||||
|
||||
- Status: PASS
|
||||
- Targeted: 32 passed
|
||||
- Full: 837 passed, 47 skipped
|
||||
- 실계좌·실주문: 실행하지 않음
|
||||
- 기존 runtime: 변경하지 않음
|
||||
|
||||
검증 범위는 wire→slot→broker submit/cancel, invalid venue, INACTIVE reset,
|
||||
PostgreSQL row mapping, 전체 회귀다.
|
||||
@@ -64,3 +64,4 @@
|
||||
| BAT-054 | standard | in-progress | REQ-113 (AC-2/AC-4 보강), REQ-115 (AC-1) | dcy-036 | [bat-054](./bat-054_20260628_daystart-credential-kis-inject/) |
|
||||
| BAT-057 | standard | release-candidate | REQ-119, REQ-120, REQ-121, REQ-122 | dcy-037 | [bat-057](./bat-057_20260714_operator-broker-truth-hotfix/) |
|
||||
| BAT-058 | standard | release-candidate | REQ-123 | dcy-038 | [bat-058](./bat-058_20260716_cost-basis-safe-sync-seed/) |
|
||||
| BAT-059 | standard | release-candidate | REQ-124 | dcy-039 | [bat-059](./bat-059_20260727_intent-venue-fanin/) |
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
- limit_price :: numeric? :: 현재 pending 주문 지정가(side 무관, dcy-012/REQ-002 — 과거 `entry_price`에서 rename). pending 진입(매수/매도 지정가) 시 set, pending 이탈(→holding/→closed) 시 None 으로 clear → 매수 지정가가 매도로 누수되지 않음(issue #10). None 이면 dispatch 시 시장가.
|
||||
- allocated_capital :: numeric? :: 배정 자본
|
||||
- order_id / broker_order_id :: str? :: 주문 식별자(자유 주석 / 브로커 핸들)
|
||||
- venue :: TEXT NOT NULL DEFAULT 'KRX' :: 현재 주문 거래소 구분(KRX/NXT/SOR). pending 주문·취소까지 보존하고 INACTIVE 진입 시 KRX 초기화.
|
||||
- strategy_id / sell_strategy_id :: str? :: 진입/청산 전략 식별
|
||||
- portfolio_id :: str? :: 포트폴리오 식별
|
||||
- forced_close_at :: datetime? :: 강제 청산 마감선
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# dcy-039: intent venue end-to-end propagation
|
||||
|
||||
- Status: confirmed
|
||||
- Date: 2026-07-27
|
||||
- Owner: trex-core-dev
|
||||
|
||||
## Problem
|
||||
|
||||
`OrderRequest`와 KIS adapter는 KRX/NXT venue를 지원하지만 전략 intent의
|
||||
`venue`가 슬롯에 저장되지 않아 execution이 항상 기본 KRX로 주문한다.
|
||||
08:00 NXT 전략은 이 단절 때문에 계정 연결 준비 상태가 아니다.
|
||||
|
||||
## Decision
|
||||
|
||||
buy/sell wire에 선택적 `venue`를 추가하고 기본값은 KRX로 유지한다. 허용값은
|
||||
KRX/NXT/SOR다. admission에서 검증한 venue를 pending 슬롯에 저장해 주문·취소에
|
||||
전달하고, INACTIVE 복귀 시 KRX로 초기화한다.
|
||||
|
||||
## Scope
|
||||
|
||||
- intent DTO와 RabbitMQ handler
|
||||
- slot 필드·PostgreSQL migration
|
||||
- execution submit/cancel 전달
|
||||
- 기존 KRX 회귀와 NXT 왕복 테스트
|
||||
|
||||
실계좌 연결, 실주문, 배포, 기존 runtime 변경은 제외한다.
|
||||
|
||||
## Evidence
|
||||
|
||||
- 신규 venue 흐름 테스트 4건
|
||||
- 전체 회귀 `837 passed, 47 skipped`
|
||||
- 별도 worktree `codex/top50-venue-aware-intents`
|
||||
@@ -43,3 +43,4 @@
|
||||
| dcy-036_20260628_daystart-credential-fetch-failsafe-trading-load | 20260628 | 그날-시작 KIS 인증정보 fetch(portfolio-hub) + fail-safe 거래 로드 — 진실원천 부재 시 신규 거래 미개시, 장중 무중단 (교차 발원 portfolio-hub dcy-010) | [Discovery](./dcy-036_20260628_daystart-credential-fetch-failsafe-trading-load.md) | confirmed |
|
||||
| dcy-037_20260714_operator-broker-truth-and-managed-slot-divergence-hotfix | 20260714 | HOTFIX — fetched credential 운영자 broker truth 정합 및 managed non-holding divergence 분리 (#39, #40, #41) | [Discovery](./dcy-037_20260714_operator-broker-truth-and-managed-slot-divergence-hotfix.md) | confirmed |
|
||||
| dcy-038_20260716_cost-basis-response-contract-and-safe-sync-seeding | 20260716 | P1 — portfolio-hub cost-basis response contract 정합 및 안전한 sync 평균가 seed | [Discovery](./dcy-038_20260716_cost-basis-response-contract-and-safe-sync-seeding.md) | confirmed |
|
||||
| dcy-039_20260727_intent-venue-end-to-end-propagation | 20260727 | 전략 intent venue를 슬롯·주문·취소까지 전달 | [Discovery](./dcy-039_20260727_intent-venue-end-to-end-propagation.md) | confirmed |
|
||||
|
||||
@@ -415,7 +415,7 @@ resume_kill_switch 요청·응답 (REQ-046):
|
||||
### Additive Notes (REQ-117)
|
||||
|
||||
- `venue` 필드는 BAT-056에서 additive 추가. 기존 orb/tsl 미지정 호출 → 기본값 `"KRX"` 자동 적용 → 하위 호환.
|
||||
- `OrderRequest`는 AMQP wire 위를 직접 흐르지 않으므로 wire 계약 영향 없음. intent 메시지에서 venue를 전달하는 것은 tvr 팬-인 batch(후속) 범위.
|
||||
- `OrderRequest`는 AMQP wire 위를 직접 흐르지 않는다. BAT-059부터 buy/sell intent의 선택적 `venue`(기본 KRX)를 슬롯에 보존해 이 DTO까지 전달한다. 허용값 외에는 `invalid_venue`로 admission 전 거부한다.
|
||||
- KIS 어댑터(REQ-118)는 `venue` → `EXCG_ID_DVSN_CD` 로 변환해 KIS HTTP 요청에 항상 명시 전송.
|
||||
|
||||
## `BrokerPort` Protocol 메서드 (BAT-056 / REQ-118)
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
- 슬롯 `limit_price` 생애주기(dcy-012/REQ-002): INACTIVE→PENDING_BUY(매수 지정가 set) → PENDING_BUY→HOLDING(clear) → HOLDING→PENDING_SELL(매도 지정가 set 또는 None=시장가) → PENDING_SELL→INACTIVE(clear). 롤백(→INACTIVE) 전이에서도 clear. 외부 취소 후 holding 복귀에서도 clear 되어 재매도는 시장가.
|
||||
- **실행 정책 생애주기(dcy-022/REQ-068)**: `timeout_sec`·`slippage_pct` 는 `limit_price` 와 동일한 생애주기(PENDING_* 진입 시 set, HOLDING/INACTIVE 탈출 시 clear). 슬롯의 `updated_at` 이 dispatch 시각 proxy 로 사용된다(mark_dispatched 가 갱신, stale-alert 와 동일 방법).
|
||||
- 어댑터: paper(결정적 async 더블) / KIS(모의투자 검증, 실계좌 게이팅).
|
||||
- **venue 전달 경로(BAT-056/REQ-117/118)**: `OrderRequest.venue`(기본 `"KRX"`) → KIS 어댑터가 `EXCG_ID_DVSN_CD`로 변환해 submit_order/cancel_order body에 항상 명시 전송. NXT 주문이면 `venue="NXT"` 지정 → `EXCG_ID_DVSN_CD=NXT`. 기존 orb/tsl은 venue 미지정(기본 KRX) → 이전 대비 EXCG 필드 추가만 다름(동작 동일). intent 메시지 → venue 전달은 tvr 팬-인 batch(후속) 범위 — 현재 wire 계약 불변.
|
||||
- **venue 전달 경로(BAT-056/REQ-117/118, BAT-059/REQ-124)**: buy/sell intent `venue`(기본 KRX) → pending `Slot.venue` → `OrderRequest.venue`와 취소 venue → KIS `EXCG_ID_DVSN_CD`. 허용값은 KRX/NXT/SOR이며 invalid는 슬롯 전이 전 거부한다. INACTIVE 복귀 시 KRX로 초기화한다.
|
||||
|
||||
### 운영자 cancel 백스톱 (dcy-022/REQ-071)
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# REQ-124: intent venue persistence and execution
|
||||
|
||||
- Type: Integration
|
||||
- Status: Implemented
|
||||
- Priority: High
|
||||
- Source: dcy-039
|
||||
|
||||
## Requirements
|
||||
|
||||
1. buy/sell intent는 `venue`를 선택적으로 받으며 미지정은 KRX다.
|
||||
2. KRX/NXT/SOR 외 값은 슬롯 전이 전에 `invalid_venue`로 거부한다.
|
||||
3. venue는 intent log, pending slot, `OrderRequest`, timeout/operator cancel에
|
||||
동일하게 전달한다.
|
||||
4. INACTIVE 복귀 시 venue는 KRX로 초기화한다.
|
||||
5. 기존 venue 미지정 전략과 KRX 테스트는 변경 없이 통과한다.
|
||||
6. PostgreSQL migration은 기존 행을 KRX로 안전하게 backfill한다.
|
||||
|
||||
## Acceptance Evidence
|
||||
|
||||
- `tests/application/test_venue_intent_flow.py`
|
||||
- `tests/domain/slot/test_contract.py`
|
||||
- `tests/adapters/persistence/test_postgresql_slot_repository_parity.py`
|
||||
- 전체 회귀 837 passed, 47 skipped
|
||||
@@ -204,9 +204,12 @@
|
||||
| REQ-121 | managed non-holding divergence 분류와 slot 존재 조회 계약 | Data | Implemented | High | trex-core-dev | [req-121](./Data/req-121_managed-non-holding-divergence-classification-query.md) |
|
||||
| REQ-122 | operator broker truth hotfix baseline 문서 정합 | Documentation | Implemented | High | trex-core-dev | [req-122](./Documentation/req-122_operator-broker-truth-hotfix-baseline-alignment.md) |
|
||||
| REQ-123 | cost-basis canonical parse 및 WAC unavailable 안전 sync seed | Integration | Approved | High | trex-core-dev | [req-123](./Integration/req-123_cost-basis-canonical-parse-and-safe-sync-seeding.md) |
|
||||
| REQ-124 | intent venue persistence and execution | Integration | Implemented | High | trex-core-dev | [req-124](./Integration/req-124_intent-venue-persistence-and-execution.md) |
|
||||
|
||||
## Recent Change Log Summary
|
||||
|
||||
- 2026-07-27 :: REQ-124 Implemented: strategy intent venue를 pending slot과 broker submit/cancel까지 additive 전달. 전체 회귀 837 passed, 47 skipped. 배포·실주문 제외.
|
||||
|
||||
- 2026-07-16 :: REQ-123 **confirm-req 승인(Approved, lead confirm)**: dcy-038 OQ-1~3 완료 기준으로 제약(scope 제외 항목: API/schema 변경/credential/config 변경/live sync/배포), OQ-2/OQ-3 결정 반영, fail-closed WAC unavail no-write 정책, canonical parsing 우선순위/legacy fallback 규칙 및 dry-run diagnostics가 일치함을 확인해 `REQ-123`을 Approved 전환.
|
||||
- 2026-07-16 :: REQ-123 **신규 생성(Proposed, draft-req, dcy-038)**: portfolio-hub canonical `cost_basis_per_unit` consumer parse, legacy alias precedence, WAC unavailable fail-closed sync/no-write, dry-run diagnostics, response-contract/regression tests, observability 및 rollback/release gate. portfolio-hub API/schema/auth·token/config·DB migration/direct edit·live sync·배포 제외.
|
||||
- 2026-07-15 :: REQ-119~122 **`Approved` → `Implemented`** (`confirm-req-implemented`, dcy-037/BAT-057): PR #42 merged to `main` (94e1489). Independent QC PASS-WITH-NOTES (acceptance blocker 0); 806 passed/47 skipped/1 deselected. `sync --live`, deployment, and production remediation remain separate release-approval work.
|
||||
|
||||
@@ -35,7 +35,7 @@ _SLOT_COLUMNS = (
|
||||
"order_id, strategy_id, broker_order_id, portfolio_id, "
|
||||
"sell_strategy_id, forced_close_at, stop_loss_price, take_profit_price, "
|
||||
"created_at, updated_at, instance_key, retired_at, "
|
||||
"timeout_sec, slippage_pct, occupancy_seq"
|
||||
"timeout_sec, slippage_pct, occupancy_seq, venue"
|
||||
)
|
||||
_MANAGED_SLOT_SQL = (
|
||||
f"SELECT {_SLOT_COLUMNS} FROM daily_slot "
|
||||
@@ -145,6 +145,7 @@ def _row(slot: Slot) -> tuple:
|
||||
slot.timeout_sec,
|
||||
Decimal(str(slot.slippage_pct)),
|
||||
slot.occupancy_seq,
|
||||
slot.venue,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Venue propagation from the strategy wire contract to broker execution."""
|
||||
from __future__ import annotations
|
||||
|
||||
from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore
|
||||
from trex_core.application.broker_port import BalanceItem, FillStatus, OrderAck, OrderRequest
|
||||
from trex_core.application.execution import ExecutionService
|
||||
from trex_core.application.intents import IntentService
|
||||
from trex_core.application.messaging import RpcRequest
|
||||
from trex_core.application.messaging_handlers import make_intent_rpc_handler
|
||||
from trex_core.domain.slot import SlotService, SlotState
|
||||
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
|
||||
|
||||
|
||||
class RecordingBroker:
|
||||
def __init__(self) -> None:
|
||||
self.orders: list[OrderRequest] = []
|
||||
self.cancel_venues: list[str] = []
|
||||
|
||||
def submit_order(self, req: OrderRequest) -> OrderAck:
|
||||
self.orders.append(req)
|
||||
return OrderAck(accepted=True, broker_order_id=f"order-{len(self.orders)}")
|
||||
|
||||
def poll_fill(self, broker_order_id: str, *, side: str, symbol: str) -> FillStatus:
|
||||
return FillStatus(state="accepted")
|
||||
|
||||
def cancel_order(
|
||||
self,
|
||||
broker_order_id: str,
|
||||
*,
|
||||
side: str,
|
||||
symbol: str,
|
||||
qty: int,
|
||||
venue: str = "KRX",
|
||||
) -> bool:
|
||||
self.cancel_venues.append(venue)
|
||||
return True
|
||||
|
||||
def get_balance(self) -> list[BalanceItem]:
|
||||
return []
|
||||
|
||||
|
||||
def _wire() -> tuple[
|
||||
InMemorySlotRepository,
|
||||
IntentService,
|
||||
ExecutionService,
|
||||
RecordingBroker,
|
||||
]:
|
||||
repo = InMemorySlotRepository()
|
||||
service = SlotService(repo)
|
||||
intents = IntentService(
|
||||
service,
|
||||
repo,
|
||||
InMemoryIntentStore(),
|
||||
resolve_symbol=lambda portfolio_id: "005930",
|
||||
)
|
||||
repo.reconcile_universe("default", {"PF-1": "005930"})
|
||||
broker = RecordingBroker()
|
||||
return repo, intents, ExecutionService(service, repo, broker), broker
|
||||
|
||||
|
||||
def test_nxt_buy_wire_value_reaches_broker_order() -> None:
|
||||
repo, intents, execution, broker = _wire()
|
||||
handler = make_intent_rpc_handler(intents)
|
||||
|
||||
response = handler(
|
||||
RpcRequest(
|
||||
payload={
|
||||
"intent_type": "buy",
|
||||
"intent_id": "nxt-buy",
|
||||
"portfolio_id": "PF-1",
|
||||
"qty": 1,
|
||||
"venue": "NXT",
|
||||
},
|
||||
correlation_id="nxt-buy",
|
||||
)
|
||||
)
|
||||
|
||||
assert response.payload["status"] == "admitted"
|
||||
pending = next(slot for slot in repo.list_slots("default") if slot.status is SlotState.PENDING_BUY)
|
||||
assert pending.venue == "NXT"
|
||||
execution.run_once()
|
||||
assert broker.orders[0].venue == "NXT"
|
||||
|
||||
|
||||
def test_invalid_venue_is_rejected_before_slot_transition() -> None:
|
||||
repo, intents, _, _ = _wire()
|
||||
handler = make_intent_rpc_handler(intents)
|
||||
|
||||
response = handler(
|
||||
RpcRequest(
|
||||
payload={
|
||||
"intent_type": "buy",
|
||||
"intent_id": "bad-venue",
|
||||
"portfolio_id": "PF-1",
|
||||
"qty": 1,
|
||||
"venue": "UNKNOWN",
|
||||
},
|
||||
correlation_id="bad-venue",
|
||||
)
|
||||
)
|
||||
|
||||
assert response.payload["status"] == "rejected"
|
||||
assert response.payload["reason"] == "invalid_venue"
|
||||
assert repo.list_slots("default")[0].status is SlotState.INACTIVE
|
||||
|
||||
|
||||
def test_inactive_transition_resets_venue_to_krx() -> None:
|
||||
repo, intents, _, _ = _wire()
|
||||
handler = make_intent_rpc_handler(intents)
|
||||
handler(
|
||||
RpcRequest(
|
||||
payload={
|
||||
"intent_type": "buy",
|
||||
"intent_id": "nxt-buy-reset",
|
||||
"portfolio_id": "PF-1",
|
||||
"qty": 1,
|
||||
"venue": "NXT",
|
||||
},
|
||||
correlation_id="nxt-buy-reset",
|
||||
)
|
||||
)
|
||||
slot = repo.list_slots("default")[0]
|
||||
service = SlotService(repo)
|
||||
reset = service.transit_slot(
|
||||
slot.slot_id,
|
||||
SlotState.PENDING_BUY,
|
||||
SlotState.INACTIVE,
|
||||
reason="rollback",
|
||||
actor="test",
|
||||
)
|
||||
assert reset.venue == "KRX"
|
||||
|
||||
|
||||
def test_operator_cancel_uses_original_nxt_venue() -> None:
|
||||
repo, intents, execution, broker = _wire()
|
||||
handler = make_intent_rpc_handler(intents)
|
||||
response = handler(
|
||||
RpcRequest(
|
||||
payload={
|
||||
"intent_type": "buy",
|
||||
"intent_id": "nxt-buy-cancel",
|
||||
"portfolio_id": "PF-1",
|
||||
"qty": 1,
|
||||
"venue": "NXT",
|
||||
},
|
||||
correlation_id="nxt-buy-cancel",
|
||||
)
|
||||
)
|
||||
execution.run_once()
|
||||
|
||||
assert execution.cancel_pending_slot(response.payload["slot_id"]) == "ok"
|
||||
assert broker.cancel_venues == ["NXT"]
|
||||
@@ -37,7 +37,7 @@ _SLOT_COLUMNS = (
|
||||
"order_id, strategy_id, broker_order_id, portfolio_id, "
|
||||
"sell_strategy_id, forced_close_at, stop_loss_price, take_profit_price, "
|
||||
"created_at, updated_at, instance_key, retired_at, "
|
||||
"timeout_sec, slippage_pct, occupancy_seq"
|
||||
"timeout_sec, slippage_pct, occupancy_seq, venue"
|
||||
)
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ class PostgreSQLSlotRepository:
|
||||
"broker_order_id=%s, portfolio_id=%s, sell_strategy_id=%s, "
|
||||
"forced_close_at=%s, stop_loss_price=%s, take_profit_price=%s, "
|
||||
"instance_key=%s, retired_at=%s, timeout_sec=%s, slippage_pct=%s, "
|
||||
"occupancy_seq=%s, updated_at=%s WHERE slot_id=%s",
|
||||
"occupancy_seq=%s, venue=%s, updated_at=%s WHERE slot_id=%s",
|
||||
(
|
||||
slot.status.value,
|
||||
slot.symbol,
|
||||
@@ -236,6 +236,7 @@ class PostgreSQLSlotRepository:
|
||||
slot.timeout_sec,
|
||||
slot.slippage_pct,
|
||||
slot.occupancy_seq,
|
||||
slot.venue,
|
||||
slot.updated_at or datetime.now(tz=timezone.utc),
|
||||
slot.slot_id,
|
||||
),
|
||||
@@ -361,7 +362,7 @@ class PostgreSQLSlotRepository:
|
||||
# forced_close_at(11) stop_loss_price(12) take_profit_price(13)
|
||||
# created_at(14) updated_at(15) instance_key(16) retired_at(17)
|
||||
# timeout_sec(18) slippage_pct(19) [dcy-022/REQ-068]
|
||||
# occupancy_seq(20) [dcy-028/REQ-081]
|
||||
# occupancy_seq(20) [dcy-028/REQ-081], venue(21)
|
||||
return Slot(
|
||||
slot_id=row[0] if isinstance(row[0], UUID) else UUID(str(row[0])),
|
||||
status=SlotState(row[1]),
|
||||
@@ -384,6 +385,7 @@ class PostgreSQLSlotRepository:
|
||||
timeout_sec=int(row[18]) if row[18] is not None else None,
|
||||
slippage_pct=f(row[19]),
|
||||
occupancy_seq=int(row[20]) if len(row) > 20 and row[20] is not None else 0,
|
||||
venue=str(row[21]) if len(row) > 21 and row[21] is not None else "KRX",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Preserve the admitted intent venue until dispatch/cancel. Existing rows are
|
||||
-- explicitly backward compatible with the historical KRX-only execution path.
|
||||
ALTER TABLE daily_slot
|
||||
ADD COLUMN IF NOT EXISTS venue TEXT NOT NULL DEFAULT 'KRX';
|
||||
|
||||
ALTER TABLE daily_slot
|
||||
DROP CONSTRAINT IF EXISTS daily_slot_venue_check;
|
||||
|
||||
ALTER TABLE daily_slot
|
||||
ADD CONSTRAINT daily_slot_venue_check
|
||||
CHECK (venue IN ('KRX', 'NXT', 'SOR'));
|
||||
@@ -197,6 +197,7 @@ class ExecutionService:
|
||||
qty=qty,
|
||||
limit_price=limit,
|
||||
order_type="00" if limit is not None else "01",
|
||||
venue=slot.venue,
|
||||
)
|
||||
)
|
||||
if ack.accepted and ack.broker_order_id:
|
||||
@@ -548,12 +549,14 @@ class ExecutionService:
|
||||
return
|
||||
qty = int(slot.qty or 0)
|
||||
try:
|
||||
accepted = self._broker.cancel_order(
|
||||
slot.broker_order_id,
|
||||
side=side,
|
||||
symbol=slot.symbol or "",
|
||||
qty=qty,
|
||||
)
|
||||
cancel_kwargs = {
|
||||
"side": side,
|
||||
"symbol": slot.symbol or "",
|
||||
"qty": qty,
|
||||
}
|
||||
if slot.venue != "KRX":
|
||||
cancel_kwargs["venue"] = slot.venue
|
||||
accepted = self._broker.cancel_order(slot.broker_order_id, **cancel_kwargs)
|
||||
r.timeout_cancelled += 1
|
||||
_log.warning(
|
||||
"timeout cancel sent: slot=%s status=%s broker_order_id=%s "
|
||||
@@ -665,12 +668,14 @@ class ExecutionService:
|
||||
side = "buy" if slot.status is SlotState.PENDING_BUY else "sell"
|
||||
qty = int(slot.qty or 0)
|
||||
try:
|
||||
self._broker.cancel_order(
|
||||
slot.broker_order_id,
|
||||
side=side,
|
||||
symbol=slot.symbol or "",
|
||||
qty=qty,
|
||||
)
|
||||
cancel_kwargs = {
|
||||
"side": side,
|
||||
"symbol": slot.symbol or "",
|
||||
"qty": qty,
|
||||
}
|
||||
if slot.venue != "KRX":
|
||||
cancel_kwargs["venue"] = slot.venue
|
||||
self._broker.cancel_order(slot.broker_order_id, **cancel_kwargs)
|
||||
except Exception: # noqa: BLE001 - best-effort
|
||||
_log.warning(
|
||||
"operator cancel failed: slot=%s broker_order_id=%s",
|
||||
|
||||
@@ -75,6 +75,7 @@ class BuyIntent:
|
||||
# slippage_pct: effective limit = entry_price*(1+pct/100) buy / *(1-pct/100) sell.
|
||||
timeout_sec: Optional[int] = None
|
||||
slippage_pct: Optional[float] = None
|
||||
venue: str = "KRX"
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -93,6 +94,7 @@ class SellIntent:
|
||||
# dcy-022 / REQ-068: execution policy (optional, backward-compat).
|
||||
timeout_sec: Optional[int] = None
|
||||
slippage_pct: Optional[float] = None
|
||||
venue: str = "KRX"
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -152,6 +154,7 @@ class IntentService:
|
||||
"entry_price": intent.entry_price,
|
||||
"strategy_id": intent.strategy_id,
|
||||
"sell_strategy_id": intent.sell_strategy_id,
|
||||
"venue": intent.venue,
|
||||
}
|
||||
prior = self._dedup(intent.intent_id, "buy", payload)
|
||||
if prior is not None:
|
||||
@@ -169,6 +172,7 @@ class IntentService:
|
||||
"reason": intent.reason,
|
||||
"sell_strategy_id": intent.sell_strategy_id,
|
||||
"qty": intent.qty,
|
||||
"venue": intent.venue,
|
||||
}
|
||||
prior = self._dedup(intent.intent_id, "sell", payload)
|
||||
if prior is not None:
|
||||
@@ -207,6 +211,9 @@ class IntentService:
|
||||
dcy-036 / REQ-114: entry gate — if entry_allowed_fn returns False,
|
||||
BuyIntent is rejected before any slot lookup."""
|
||||
|
||||
if intent.venue not in {"KRX", "NXT", "SOR"}:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, "invalid_venue")
|
||||
|
||||
# dcy-036 / REQ-114: day-start entry gate (fail-closed).
|
||||
# None or True → pass; False → reject new entry.
|
||||
# SellIntent is NOT checked (청산 경로는 차단하지 않음 AC-5).
|
||||
@@ -292,6 +299,7 @@ class IntentService:
|
||||
take_profit_price=intent.take_profit_price,
|
||||
timeout_sec=effective_timeout,
|
||||
slippage_pct=intent.slippage_pct,
|
||||
venue=intent.venue,
|
||||
extra={"intent_id": intent.intent_id, "symbol": symbol},
|
||||
)
|
||||
except SlotStateMismatchError:
|
||||
@@ -300,6 +308,9 @@ class IntentService:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, "already_active")
|
||||
|
||||
def _admit_sell(self, intent: SellIntent) -> IntentDecision:
|
||||
if intent.venue not in {"KRX", "NXT", "SOR"}:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, "invalid_venue")
|
||||
|
||||
slot = self._repo.fetch_slot(intent.slot_id)
|
||||
if slot is None:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, "slot_not_found")
|
||||
@@ -342,6 +353,7 @@ class IntentService:
|
||||
qty=intent.qty,
|
||||
timeout_sec=effective_timeout,
|
||||
slippage_pct=intent.slippage_pct,
|
||||
venue=intent.venue,
|
||||
extra={"intent_id": intent.intent_id},
|
||||
)
|
||||
except SlotStateMismatchError:
|
||||
|
||||
@@ -117,6 +117,7 @@ def make_intent_rpc_handler(
|
||||
# dcy-022 / REQ-068: execution policy (optional, backward-compat).
|
||||
timeout_sec=_opt_int(body.get("timeout_sec")),
|
||||
slippage_pct=_opt_float(body.get("slippage_pct")),
|
||||
venue=_opt_str(body.get("venue")) or "KRX",
|
||||
)
|
||||
d = intent_service.submit_buy(buy)
|
||||
elif intent_type == "sell":
|
||||
@@ -132,6 +133,7 @@ def make_intent_rpc_handler(
|
||||
# dcy-022 / REQ-068: execution policy (optional, backward-compat).
|
||||
timeout_sec=_opt_int(body.get("timeout_sec")),
|
||||
slippage_pct=_opt_float(body.get("slippage_pct")),
|
||||
venue=_opt_str(body.get("venue")) or "KRX",
|
||||
)
|
||||
d = intent_service.submit_sell(sell)
|
||||
else:
|
||||
@@ -626,4 +628,3 @@ def _parse_date(v) -> date_cls:
|
||||
def _today() -> date_cls:
|
||||
return datetime.now(tz=timezone.utc).date()
|
||||
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ class Slot:
|
||||
# holding episode. Used by the strategy-state CAS to reject writes computed
|
||||
# against a prior occupancy of a reused slot (point-ABA guard, dcy-028 R-1).
|
||||
occupancy_seq: int = 0
|
||||
# Order venue carried from the admitted intent through dispatch/cancel.
|
||||
# It is reset to KRX when the permanent asset slot returns to INACTIVE.
|
||||
venue: str = "KRX"
|
||||
|
||||
|
||||
# Canonical column contract for `Slot` (dcy-001 OQ-2 / FR-5).
|
||||
@@ -84,6 +87,7 @@ EXPECTED_SLOT_FIELDS: frozenset[str] = frozenset({
|
||||
"timeout_sec", # dcy-022 / REQ-068 (BAT-032)
|
||||
"slippage_pct", # dcy-022 / REQ-068 (BAT-032)
|
||||
"occupancy_seq", # dcy-028 / REQ-081 (BAT-042)
|
||||
"venue",
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ class SlotService:
|
||||
# dcy-022 / REQ-068: execution policy (set on PENDING entry, cleared on exit).
|
||||
timeout_sec: Optional[int] = None,
|
||||
slippage_pct: Optional[float] = None,
|
||||
venue: Optional[str] = None,
|
||||
extra: Optional[dict[str, Any]] = None,
|
||||
) -> Slot:
|
||||
"""Transition slot from one state to another atomically.
|
||||
@@ -133,6 +134,7 @@ class SlotService:
|
||||
take_profit_price=take_profit_price,
|
||||
timeout_sec=timeout_sec,
|
||||
slippage_pct=slippage_pct,
|
||||
venue=venue,
|
||||
)
|
||||
self._repo.update_slot(updated)
|
||||
|
||||
@@ -173,6 +175,7 @@ class SlotService:
|
||||
take_profit_price: Optional[Decimal] = None,
|
||||
timeout_sec: Optional[int] = None,
|
||||
slippage_pct: Optional[float] = None,
|
||||
venue: Optional[str] = None,
|
||||
) -> Slot:
|
||||
"""Compute the post-transition Slot row.
|
||||
|
||||
@@ -201,6 +204,7 @@ class SlotService:
|
||||
take_profit_price=None,
|
||||
timeout_sec=None,
|
||||
slippage_pct=None,
|
||||
venue="KRX",
|
||||
updated_at=datetime.now(tz=timezone.utc),
|
||||
)
|
||||
|
||||
@@ -267,6 +271,7 @@ class SlotService:
|
||||
timeout_sec=new_timeout_sec,
|
||||
slippage_pct=new_slippage_pct,
|
||||
occupancy_seq=new_occupancy_seq,
|
||||
venue=venue if venue is not None else slot.venue,
|
||||
updated_at=datetime.now(tz=timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user