programgarden 2.6.1__tar.gz → 2.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {programgarden-2.6.1 → programgarden-2.6.3}/PKG-INFO +9 -3
- {programgarden-2.6.1 → programgarden-2.6.3}/README.md +6 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/broker_evidence.py +5 -1
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/broker_snapshot.py +15 -3
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/executor.py +59 -21
- programgarden-2.6.3/programgarden/futures_history.py +23 -0
- programgarden-2.6.3/programgarden/paper_futures_business_date.py +96 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/resolver.py +27 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/pyproject.toml +3 -3
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/__init__.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/binding_validator.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/client.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/code_worker.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/context.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/__init__.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/adjustment_delivery.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/broker_order_totals.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/db_naming.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/execution_storage.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/fill_reconciler.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/order_cancellation.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/order_recovery.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/owned_order_cancellation.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/position_reconciliation.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/query_builder.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/sqlite_scope.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/deep_fixtures.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/futures_contract_info.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/futures_orderable.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/futures_pnl.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/futures_read_evidence.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/incremental_build.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/managed_order_control.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/node_runner.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/order_lifecycle.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/plugin/__init__.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/providers/__init__.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/reconnect_handler.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_contracts.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_events.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_external.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_order_adapter.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_orders.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_scenarios.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_semantics.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_sources.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_sqlite.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/replay_triggers.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/resource/__init__.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/resource/context.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/resource/limiter.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/resource/monitor.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/resource/throttle.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/semantic_rules.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/tools/__init__.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/tools/event_tools.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/tools/job_tools.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/validation_recommender.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/validation_replay.py +0 -0
- {programgarden-2.6.1 → programgarden-2.6.3}/programgarden/valuation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programgarden
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.3
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Author: 프로그램동산
|
|
@@ -16,8 +16,8 @@ Requires-Dist: croniter (>=6.0.0,<7.0.0)
|
|
|
16
16
|
Requires-Dist: litellm (>=1.40.0)
|
|
17
17
|
Requires-Dist: lxml (>=6.0.2,<7.0.0)
|
|
18
18
|
Requires-Dist: programgarden-community (>=2.3.1,<3.0.0)
|
|
19
|
-
Requires-Dist: programgarden-core (>=2.6.
|
|
20
|
-
Requires-Dist: programgarden-finance (>=2.0.
|
|
19
|
+
Requires-Dist: programgarden-core (>=2.6.2,<3.0.0)
|
|
20
|
+
Requires-Dist: programgarden-finance (>=2.0.2,<3.0.0)
|
|
21
21
|
Requires-Dist: psutil (>=6.0.0,<7.0.0)
|
|
22
22
|
Requires-Dist: psycopg2-binary (>=2.9.11,<3.0.0)
|
|
23
23
|
Requires-Dist: pydantic (>=2.0.1,<3.0.0)
|
|
@@ -27,8 +27,14 @@ Requires-Dist: tzdata (>=2025.2,<2026.0)
|
|
|
27
27
|
Requires-Dist: yfinance (>=0.2.0,<0.3.0)
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
|
+
The 2.6.2 release candidate [futures minute alias correction](../../docs/futures-minute-alias-contract.md) aligns canonical minute periods with the existing one-page o3103 path and rejects unsupported hourly/adjustment claims. Complete-range coverage and consumer adoption are not implied.
|
|
31
|
+
|
|
30
32
|
# ProgramGarden
|
|
31
33
|
|
|
34
|
+
The 2.6.3 release candidate [paper futures business-date correction](../../docs/paper-futures-business-date.md)
|
|
35
|
+
uses a fresh account-scoped broker date for startup pending orders, OpenOrdersNode
|
|
36
|
+
and legacy fill observation. Missing or mismatched evidence remains unavailable.
|
|
37
|
+
|
|
32
38
|
An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
|
|
33
39
|
rejects unsupported intraday periods instead of silently querying daily bars.
|
|
34
40
|
Supported g3204 periods retain their existing requests and output contract.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
The 2.6.2 release candidate [futures minute alias correction](../../docs/futures-minute-alias-contract.md) aligns canonical minute periods with the existing one-page o3103 path and rejects unsupported hourly/adjustment claims. Complete-range coverage and consumer adoption are not implied.
|
|
2
|
+
|
|
1
3
|
# ProgramGarden
|
|
2
4
|
|
|
5
|
+
The 2.6.3 release candidate [paper futures business-date correction](../../docs/paper-futures-business-date.md)
|
|
6
|
+
uses a fresh account-scoped broker date for startup pending orders, OpenOrdersNode
|
|
7
|
+
and legacy fill observation. Missing or mismatched evidence remains unavailable.
|
|
8
|
+
|
|
3
9
|
An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
|
|
4
10
|
rejects unsupported intraday periods instead of silently querying daily bars.
|
|
5
11
|
Supported g3204 periods retain their existing requests and output contract.
|
|
@@ -40,7 +40,7 @@ class StartupQueryDateMismatch(StartupEvidenceUnavailable):
|
|
|
40
40
|
self.business_day = business_day
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
async def checked_body(response, tr, request=None):
|
|
43
|
+
async def checked_body(response, tr, request=None, *, paper_trading=False):
|
|
44
44
|
"""Read original broker blocks so SDK defaults cannot manufacture success."""
|
|
45
45
|
if response.status_code != 200 or response.error_msg:
|
|
46
46
|
raise StartupEvidenceUnavailable(f"{tr}: transport_failure")
|
|
@@ -105,6 +105,10 @@ async def checked_body(response, tr, request=None):
|
|
|
105
105
|
and echo.get("BnsTpCode") == "0" and echo.get("CrcyCode") == "000")
|
|
106
106
|
elif tr in {"CIDBQ01500", "CIDBQ02400"} and code == "00707":
|
|
107
107
|
accepted = (not rows and response.rsp_msg == "모의투자 조회할 내역(자료)이 없습니다.")
|
|
108
|
+
elif paper_trading is True and tr in {"CIDBQ01500", "CIDBQ02400"} and code == "02679":
|
|
109
|
+
# Observed paper response with complete raw blocks, explicit empty rows
|
|
110
|
+
# and terminal continuation. The caller still binds date/account scope.
|
|
111
|
+
accepted = not rows and response.rsp_msg == "조회내역이 없습니다."
|
|
108
112
|
if not accepted:
|
|
109
113
|
raise StartupEvidenceUnavailable(f"{tr}: unverified_response_code={code}")
|
|
110
114
|
if tr in {"t0424", "t0425"}:
|
|
@@ -110,12 +110,18 @@ async def read_broker_snapshot(ls, *, execution_key, product, provider, trading_
|
|
|
110
110
|
source = "COSOQ00201+COSAQ00102"
|
|
111
111
|
elif product == "overseas_futures":
|
|
112
112
|
from programgarden_finance import CIDBQ01500, CIDBQ02400
|
|
113
|
+
from programgarden.paper_futures_business_date import PaperFuturesScope, from_current_positions
|
|
113
114
|
|
|
114
115
|
accno = ls.overseas_futureoption().accno()
|
|
116
|
+
paper_scope = PaperFuturesScope.capture(ls) if trading_mode == "paper" else None
|
|
115
117
|
query = accno.CIDBQ01500(body=CIDBQ01500.CIDBQ01500InBlock1(
|
|
116
118
|
RecCnt=1, AcntTpCode="1", FcmAcntNo="", QryDt="", BalTpCode="2"))
|
|
117
119
|
response = await query.req_async()
|
|
118
|
-
|
|
120
|
+
paper_day = None
|
|
121
|
+
if paper_scope is not None:
|
|
122
|
+
paper_day = await from_current_positions(ls, response, query, paper_scope)
|
|
123
|
+
else:
|
|
124
|
+
await checked_body(response, "CIDBQ01500", query)
|
|
119
125
|
for row in response.block2:
|
|
120
126
|
symbol = normalized_symbol(str(required(row, "IsuCodeVal")), product)
|
|
121
127
|
side = str(required(row, "BnsTpCode"))
|
|
@@ -127,11 +133,17 @@ async def read_broker_snapshot(ls, *, execution_key, product, provider, trading_
|
|
|
127
133
|
if side == "2":
|
|
128
134
|
positions[symbol] = positions.get(symbol, Decimal(0)) + amount
|
|
129
135
|
await asyncio.sleep(2)
|
|
136
|
+
if paper_day is not None:
|
|
137
|
+
paper_day.scope.check(ls)
|
|
138
|
+
query_day = paper_day.day if paper_day is not None else ""
|
|
130
139
|
query = accno.CIDBQ02400(body=CIDBQ02400.CIDBQ02400InBlock1(
|
|
131
|
-
RecCnt=1, IsuCodeVal="", QrySrtDt=
|
|
140
|
+
RecCnt=1, IsuCodeVal="", QrySrtDt=query_day, QryEndDt=query_day, ThdayTpCode="1",
|
|
132
141
|
OrdStatCode="2", BnsTpCode="0", QryTpCode="2", OrdPtnCode="00", OvrsDrvtFnoTpCode="A"))
|
|
133
142
|
response = await query.req_async()
|
|
134
|
-
|
|
143
|
+
if paper_day is not None:
|
|
144
|
+
await paper_day.check_orders(ls, response, query)
|
|
145
|
+
else:
|
|
146
|
+
await checked_body(response, "CIDBQ02400", query)
|
|
135
147
|
pending = [value for row in response.block2 if (value := pending_row(row, product)) is not None]
|
|
136
148
|
source = "CIDBQ01500+CIDBQ02400"
|
|
137
149
|
else:
|
|
@@ -35,6 +35,12 @@ from programgarden_core import (
|
|
|
35
35
|
OVERSEAS_STOCK_ORDER_MARKET_CODES,
|
|
36
36
|
)
|
|
37
37
|
from programgarden.context import ExecutionContext, WorkflowEvent
|
|
38
|
+
from programgarden.futures_history import (
|
|
39
|
+
DAILY_INTERVALS as FUTURES_DAILY_INTERVALS,
|
|
40
|
+
MINUTE_INTERVALS as FUTURES_MINUTE_INTERVALS,
|
|
41
|
+
normalize_interval as normalize_futures_history_interval,
|
|
42
|
+
validate_adjust as validate_futures_history_adjust,
|
|
43
|
+
)
|
|
38
44
|
from programgarden.order_lifecycle import (
|
|
39
45
|
FUTURES_PRODUCTS, OrderLifecycleMetadata, exact_futures_credential,
|
|
40
46
|
inject_futures_connection, operation_key,
|
|
@@ -6653,21 +6659,33 @@ class OpenOrdersNodeExecutor(NodeExecutorBase):
|
|
|
6653
6659
|
from datetime import datetime
|
|
6654
6660
|
from zoneinfo import ZoneInfo
|
|
6655
6661
|
from programgarden.futures_read_evidence import require_complete_rows, require_pending_identity
|
|
6662
|
+
from programgarden.paper_futures_business_date import is_paper_client, read_current_business_date
|
|
6663
|
+
from programgarden.database.broker_evidence import StartupEvidenceUnavailable
|
|
6656
6664
|
from programgarden_finance.ls.overseas_futureoption.accno.CIDBQ02400.blocks import CIDBQ02400InBlock1
|
|
6657
6665
|
|
|
6658
6666
|
|
|
6659
|
-
|
|
6667
|
+
paper_day = None
|
|
6668
|
+
try:
|
|
6669
|
+
if is_paper_client(ls):
|
|
6670
|
+
paper_day = await read_current_business_date(ls)
|
|
6671
|
+
except StartupEvidenceUnavailable as exc:
|
|
6672
|
+
return self._empty_result(str(exc))
|
|
6673
|
+
today = paper_day.day if paper_day else datetime.now(ZoneInfo("Asia/Seoul")).strftime("%Y%m%d")
|
|
6660
6674
|
request = CIDBQ02400InBlock1(
|
|
6661
6675
|
RecCnt=1, IsuCodeVal="", QrySrtDt=today, QryEndDt=today,
|
|
6662
6676
|
ThdayTpCode="1", OrdStatCode="2", BnsTpCode="0", QryTpCode="2",
|
|
6663
6677
|
OrdPtnCode="00", OvrsDrvtFnoTpCode="A",
|
|
6664
6678
|
)
|
|
6665
|
-
response = await ls.overseas_futureoption().accno().CIDBQ02400(body=request).req_async()
|
|
6666
6679
|
try:
|
|
6667
|
-
|
|
6680
|
+
if paper_day:
|
|
6681
|
+
paper_day.scope.check(ls)
|
|
6682
|
+
query = ls.overseas_futureoption().accno().CIDBQ02400(body=request)
|
|
6683
|
+
response = await query.req_async()
|
|
6684
|
+
rows = (await paper_day.check_orders(ls, response, query) if paper_day else
|
|
6685
|
+
require_complete_rows(response, request, "CIDBQ02400"))
|
|
6668
6686
|
for row in rows:
|
|
6669
6687
|
require_pending_identity(row)
|
|
6670
|
-
except ValueError as exc:
|
|
6688
|
+
except (ValueError, StartupEvidenceUnavailable) as exc:
|
|
6671
6689
|
return self._empty_result(str(exc))
|
|
6672
6690
|
|
|
6673
6691
|
open_orders = []
|
|
@@ -12216,6 +12234,11 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
|
|
|
12216
12234
|
node_type == "HistoricalDataNode" and product in (None, "overseas_stock")
|
|
12217
12235
|
):
|
|
12218
12236
|
self._stock_history_gubun(config.get("interval", "1d"))
|
|
12237
|
+
if node_type == "OverseasFuturesHistoricalDataNode" or (
|
|
12238
|
+
node_type == "HistoricalDataNode" and product in FUTURES_PRODUCTS
|
|
12239
|
+
):
|
|
12240
|
+
normalize_futures_history_interval(config.get("interval", "1d"))
|
|
12241
|
+
validate_futures_history_adjust(config.get("adjust", False))
|
|
12219
12242
|
|
|
12220
12243
|
# deep_validate: HistoricalDataNode hits the real LS API. Inject a fixture
|
|
12221
12244
|
# time series so the flow completes without a network call.
|
|
@@ -12638,6 +12661,7 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
|
|
|
12638
12661
|
|
|
12639
12662
|
반환: [{symbol, exchange, time_series: [{date, open, high, low, close, volume}, ...]}, ...]
|
|
12640
12663
|
"""
|
|
12664
|
+
interval = normalize_futures_history_interval(interval)
|
|
12641
12665
|
credential = context.get_credential()
|
|
12642
12666
|
if not credential:
|
|
12643
12667
|
context.log("error", "Credential not set for HistoricalDataNode(futures)", node_id)
|
|
@@ -12669,7 +12693,7 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
|
|
|
12669
12693
|
symbol_to_exchange[sym] = exch
|
|
12670
12694
|
|
|
12671
12695
|
# interval별 분기
|
|
12672
|
-
is_minute_data = interval in
|
|
12696
|
+
is_minute_data = interval in FUTURES_MINUTE_INTERVALS
|
|
12673
12697
|
|
|
12674
12698
|
for symbol in symbols:
|
|
12675
12699
|
try:
|
|
@@ -12720,8 +12744,10 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
|
|
|
12720
12744
|
from programgarden_finance.ls.overseas_futureoption.chart.o3108.blocks import O3108InBlock
|
|
12721
12745
|
|
|
12722
12746
|
# interval → gubun 변환 (0=일, 1=주, 2=월)
|
|
12723
|
-
|
|
12724
|
-
|
|
12747
|
+
interval = normalize_futures_history_interval(interval)
|
|
12748
|
+
if interval not in FUTURES_DAILY_INTERVALS:
|
|
12749
|
+
raise ValueError("FUTURES_HISTORY_DAILY_INTERVAL_REQUIRED")
|
|
12750
|
+
gubun = FUTURES_DAILY_INTERVALS[interval]
|
|
12725
12751
|
|
|
12726
12752
|
body = O3108InBlock(
|
|
12727
12753
|
shcode=symbol,
|
|
@@ -12763,17 +12789,21 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
|
|
|
12763
12789
|
context: ExecutionContext,
|
|
12764
12790
|
node_id: str,
|
|
12765
12791
|
) -> List[Dict[str, Any]]:
|
|
12766
|
-
"""
|
|
12792
|
+
"""Read the initial o3103 page; dates and complete-range coverage are not supplied."""
|
|
12767
12793
|
from programgarden_finance.ls.overseas_futureoption.chart.o3103.blocks import O3103InBlock
|
|
12768
12794
|
|
|
12769
12795
|
# interval → ncnt 변환 (0=30초, 1=1분, 5=5분, 30=30분)
|
|
12770
|
-
|
|
12771
|
-
|
|
12796
|
+
interval = normalize_futures_history_interval(interval)
|
|
12797
|
+
if interval not in FUTURES_MINUTE_INTERVALS:
|
|
12798
|
+
raise ValueError("FUTURES_HISTORY_MINUTE_INTERVAL_REQUIRED")
|
|
12799
|
+
ncnt = FUTURES_MINUTE_INTERVALS[interval]
|
|
12772
12800
|
|
|
12773
12801
|
body = O3103InBlock(
|
|
12774
12802
|
shcode=symbol,
|
|
12775
12803
|
ncnt=ncnt,
|
|
12776
|
-
readcnt=200, #
|
|
12804
|
+
readcnt=200, # Existing application request size, not a documented LS maximum.
|
|
12805
|
+
cts_date="",
|
|
12806
|
+
cts_time="",
|
|
12777
12807
|
)
|
|
12778
12808
|
|
|
12779
12809
|
result = await api.chart().o3103(body=body).req_async()
|
|
@@ -16831,17 +16861,21 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
16831
16861
|
from programgarden_finance.ls.overseas_futureoption.accno.CIDBQ02400.blocks import (
|
|
16832
16862
|
CIDBQ02400InBlock1,
|
|
16833
16863
|
)
|
|
16834
|
-
|
|
16835
|
-
|
|
16836
|
-
#
|
|
16837
|
-
#
|
|
16838
|
-
#
|
|
16839
|
-
|
|
16840
|
-
|
|
16864
|
+
from programgarden.paper_futures_business_date import is_paper_client, read_current_business_date
|
|
16865
|
+
|
|
16866
|
+
# A paper server can reject the documented blank same-day dates.
|
|
16867
|
+
# Use its fresh account-scoped date, including overnight sessions;
|
|
16868
|
+
# never replace missing evidence with the machine's calendar date.
|
|
16869
|
+
paper_day = await read_current_business_date(ls) if is_paper_client(ls) else None
|
|
16870
|
+
query_day = paper_day.day if paper_day else ""
|
|
16871
|
+
if paper_day:
|
|
16872
|
+
paper_day.scope.check(ls)
|
|
16873
|
+
query = ls.overseas_futureoption().accno().CIDBQ02400(
|
|
16841
16874
|
body=CIDBQ02400InBlock1(
|
|
16875
|
+
RecCnt=1,
|
|
16842
16876
|
IsuCodeVal="", # 전체 종목
|
|
16843
|
-
QrySrtDt=
|
|
16844
|
-
QryEndDt=
|
|
16877
|
+
QrySrtDt=query_day,
|
|
16878
|
+
QryEndDt=query_day,
|
|
16845
16879
|
ThdayTpCode="1", # 1: 당일조회
|
|
16846
16880
|
OrdStatCode="0", # 0: 전체(체결+미체결) — 상태 판정에 필요
|
|
16847
16881
|
BnsTpCode="0", # 0: 전체
|
|
@@ -16849,7 +16883,11 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
16849
16883
|
OrdPtnCode="00", # 00: 전체
|
|
16850
16884
|
OvrsDrvtFnoTpCode="A", # A: 전체(선물+옵션)
|
|
16851
16885
|
)
|
|
16852
|
-
)
|
|
16886
|
+
)
|
|
16887
|
+
response = await query.req_async()
|
|
16888
|
+
|
|
16889
|
+
if paper_day:
|
|
16890
|
+
await paper_day.check_orders(ls, response, query)
|
|
16853
16891
|
|
|
16854
16892
|
if getattr(response, "error_msg", None):
|
|
16855
16893
|
context.log("debug", f"CIDBQ02400 fill query error: {response.error_msg}", node_id)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Explicit mappings for the existing single-page LS futures chart requests."""
|
|
2
|
+
|
|
3
|
+
MINUTE_INTERVALS = {"30s": 0, "1m": 1, "5m": 5, "15m": 15, "30m": 30}
|
|
4
|
+
DAILY_INTERVALS = {"1d": "0", "1w": "1", "1M": "2"}
|
|
5
|
+
INTERVAL_ALIASES = {"1min": "1m", "5min": "5m", "15min": "15m", "30min": "30m"}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize_interval(interval):
|
|
9
|
+
"""Preserve known legacy minute names without falling back to daily data."""
|
|
10
|
+
if isinstance(interval, str):
|
|
11
|
+
interval = INTERVAL_ALIASES.get(interval, interval)
|
|
12
|
+
if interval in MINUTE_INTERVALS or interval in DAILY_INTERVALS:
|
|
13
|
+
return interval
|
|
14
|
+
raise ValueError(
|
|
15
|
+
"UNSUPPORTED_FUTURES_HISTORY_INTERVAL: choose 30s, 1m, 5m, 15m, 30m, "
|
|
16
|
+
"1d, 1w or 1M. Hourly and unknown periods are not implemented; no daily fallback."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_adjust(adjust):
|
|
21
|
+
"""Never describe exact-contract prices as rollover-adjusted history."""
|
|
22
|
+
if adjust is not False:
|
|
23
|
+
raise ValueError("FUTURES_HISTORY_ADJUST_UNSUPPORTED: adjust must be false; rollover adjustment is not implemented")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Use fresh account-scoped broker dates for paper futures order inquiries.
|
|
2
|
+
|
|
3
|
+
The published same-day blank-date contract conflicts with an observed paper
|
|
4
|
+
response. This adapter uses CIDBQ01500's explicit current-date echo, never a
|
|
5
|
+
machine date, and does not cache it across calls or relax unavailable evidence.
|
|
6
|
+
"""
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
import hashlib
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from programgarden.database.broker_evidence import StartupEvidenceUnavailable, checked_body
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_paper_client(ls):
|
|
17
|
+
return getattr(getattr(ls, "token_manager", None), "paper_trading", None) is True
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class PaperFuturesScope:
|
|
22
|
+
manager: object = field(repr=False)
|
|
23
|
+
key_digest: str = field(repr=False)
|
|
24
|
+
started: float
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def capture(cls, ls):
|
|
28
|
+
manager = getattr(ls, "token_manager", None)
|
|
29
|
+
key = getattr(manager, "appkey", None)
|
|
30
|
+
if not is_paper_client(ls) or not isinstance(key, str) or not key:
|
|
31
|
+
raise StartupEvidenceUnavailable("paper_futures_business_date: unavailable_client_scope")
|
|
32
|
+
return cls(manager, hashlib.sha256(key.encode()).hexdigest(), time.monotonic())
|
|
33
|
+
|
|
34
|
+
def check(self, ls):
|
|
35
|
+
current = self.capture(ls)
|
|
36
|
+
if current.manager is not self.manager or current.key_digest != self.key_digest:
|
|
37
|
+
raise StartupEvidenceUnavailable("paper_futures_business_date: client_scope_changed")
|
|
38
|
+
age = time.monotonic() - self.started
|
|
39
|
+
if age < 0 or age > 60:
|
|
40
|
+
raise StartupEvidenceUnavailable("paper_futures_business_date: expired_observation")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class PaperFuturesBusinessDate:
|
|
45
|
+
day: str
|
|
46
|
+
account: str = field(repr=False)
|
|
47
|
+
scope: PaperFuturesScope = field(repr=False)
|
|
48
|
+
|
|
49
|
+
async def check_orders(self, ls, response, query):
|
|
50
|
+
self.scope.check(ls)
|
|
51
|
+
body = await checked_body(response, "CIDBQ02400", query, paper_trading=True)
|
|
52
|
+
echo = body["CIDBQ02400OutBlock1"]
|
|
53
|
+
request = next(iter(query.request_data.body.values()))
|
|
54
|
+
expected = request.model_dump(exclude={"RecCnt"})
|
|
55
|
+
if (request.ThdayTpCode != "1" or request.QrySrtDt != self.day
|
|
56
|
+
or request.QryEndDt != self.day or not isinstance(echo, dict)
|
|
57
|
+
or echo.get("AcntNo") != self.account
|
|
58
|
+
or any(name not in echo or echo[name] != value for name, value in expected.items())):
|
|
59
|
+
raise StartupEvidenceUnavailable("CIDBQ02400: business_date_scope_mismatch")
|
|
60
|
+
self.scope.check(ls)
|
|
61
|
+
return response.block2
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def from_current_positions(ls, response, query, scope):
|
|
65
|
+
"""Validate a just-fetched positions response without making another call."""
|
|
66
|
+
scope.check(ls)
|
|
67
|
+
body = await checked_body(response, "CIDBQ01500", query, paper_trading=True)
|
|
68
|
+
echo = body["CIDBQ01500OutBlock1"]
|
|
69
|
+
request = next(iter(query.request_data.body.values()))
|
|
70
|
+
expected = {"AcntTpCode": "1", "QryDt": "", "BalTpCode": "2", "FcmAcntNo": ""}
|
|
71
|
+
if any(getattr(request, name) != value for name, value in expected.items()):
|
|
72
|
+
raise StartupEvidenceUnavailable("CIDBQ01500: current_date_query_required")
|
|
73
|
+
if not isinstance(echo, dict) or any(echo.get(name) != value for name, value in expected.items() if name != "QryDt"):
|
|
74
|
+
raise StartupEvidenceUnavailable("CIDBQ01500: business_date_scope_mismatch")
|
|
75
|
+
day, account = echo.get("QryDt"), echo.get("AcntNo")
|
|
76
|
+
if not isinstance(day, str) or not re.fullmatch(r"[0-9]{8}", day):
|
|
77
|
+
raise StartupEvidenceUnavailable("CIDBQ01500: unavailable_business_date")
|
|
78
|
+
try:
|
|
79
|
+
datetime.strptime(day, "%Y%m%d")
|
|
80
|
+
except ValueError:
|
|
81
|
+
raise StartupEvidenceUnavailable("CIDBQ01500: invalid_business_date") from None
|
|
82
|
+
if not isinstance(account, str) or not account.strip():
|
|
83
|
+
raise StartupEvidenceUnavailable("CIDBQ01500: unavailable_account_echo")
|
|
84
|
+
scope.check(ls)
|
|
85
|
+
return PaperFuturesBusinessDate(day, account, scope)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def read_current_business_date(ls):
|
|
89
|
+
"""One bounded read; the SDK retains its existing shared-key rate limiter."""
|
|
90
|
+
from programgarden_finance import CIDBQ01500
|
|
91
|
+
|
|
92
|
+
scope = PaperFuturesScope.capture(ls)
|
|
93
|
+
query = ls.overseas_futureoption().accno().CIDBQ01500(body=CIDBQ01500.CIDBQ01500InBlock1(
|
|
94
|
+
RecCnt=1, AcntTpCode="1", FcmAcntNo="", QryDt="", BalTpCode="2"))
|
|
95
|
+
response = await query.req_async()
|
|
96
|
+
return await from_current_positions(ls, response, query, scope)
|
|
@@ -346,6 +346,7 @@ class WorkflowResolver:
|
|
|
346
346
|
# Order routing choices are literals, never prices or item bindings.
|
|
347
347
|
self._validate_order_enums(workflow, registry, result)
|
|
348
348
|
self._validate_stock_history_interval(workflow, registry, result)
|
|
349
|
+
self._validate_futures_history_contract(workflow, registry, result)
|
|
349
350
|
|
|
350
351
|
# 11. Static recommendations (topology analysis)
|
|
351
352
|
for rec in run_static_recommendation_rules(
|
|
@@ -386,6 +387,32 @@ class WorkflowResolver:
|
|
|
386
387
|
suggestion="Choose 1d, 1w, 1M or 1Y explicitly; do not substitute daily bars for an intraday strategy.",
|
|
387
388
|
))
|
|
388
389
|
|
|
390
|
+
def _validate_futures_history_contract(self, workflow, registry, result) -> None:
|
|
391
|
+
"""Keep native validation aligned with period and adjustment routing."""
|
|
392
|
+
from programgarden.futures_history import normalize_interval, validate_adjust
|
|
393
|
+
|
|
394
|
+
node_type = "OverseasFuturesHistoricalDataNode"
|
|
395
|
+
node_class = registry.get(node_type)
|
|
396
|
+
if node_class is None:
|
|
397
|
+
return
|
|
398
|
+
field = node_class.get_field_schema()["interval"]
|
|
399
|
+
for node in workflow.nodes:
|
|
400
|
+
if node.get("type") != node_type:
|
|
401
|
+
continue
|
|
402
|
+
for name, check, default, available in (
|
|
403
|
+
("interval", normalize_interval, field.default, field.enum_values),
|
|
404
|
+
("adjust", validate_adjust, False, ["false"]),
|
|
405
|
+
):
|
|
406
|
+
try:
|
|
407
|
+
check(node.get(name, default))
|
|
408
|
+
except ValueError as exc:
|
|
409
|
+
result.add(build_error(
|
|
410
|
+
ErrorCode.INVALID_FIELD_ENUM, str(exc),
|
|
411
|
+
location=ErrorLocation(node_id=node.get("id"), node_type=node_type, field_path=name),
|
|
412
|
+
available_values=available,
|
|
413
|
+
suggestion="Choose a supported period and keep adjust=false; only exact-contract prices are implemented.",
|
|
414
|
+
))
|
|
415
|
+
|
|
389
416
|
def _validate_order_enums(self, workflow, registry, result) -> None:
|
|
390
417
|
"""Enforce fixed price/order types without changing dynamic close sides."""
|
|
391
418
|
from programgarden_core.models.field_binding import ExpressionMode
|
|
@@ -5,7 +5,7 @@ authors = [
|
|
|
5
5
|
homepage = "https://programgarden.com"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
name = "programgarden"
|
|
8
|
-
version = "2.6.
|
|
8
|
+
version = "2.6.3"
|
|
9
9
|
license = "AGPL-3.0-or-later"
|
|
10
10
|
description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
|
|
11
11
|
readme = "README.md"
|
|
@@ -36,8 +36,8 @@ litellm = ">=1.40.0"
|
|
|
36
36
|
# 로 통째 실패한다(1.35.1 의 조용한 TypeError 보다 강한 모듈 로드 실패).
|
|
37
37
|
# (1.26.0 이후로도 context.py 는 WorkflowPnLEvent(**event_data) 에 personal_metrics
|
|
38
38
|
# 를 무조건 실으므로 그 하한 근거도 이 상한에 포함된다.)
|
|
39
|
-
programgarden-core = "^2.6.
|
|
40
|
-
programgarden-finance = "^2.0.
|
|
39
|
+
programgarden-core = "^2.6.2"
|
|
40
|
+
programgarden-finance = "^2.0.2"
|
|
41
41
|
programgarden-community = "^2.3.1"
|
|
42
42
|
|
|
43
43
|
[tool.poetry.group.dev.dependencies]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/owned_order_cancellation.py
RENAMED
|
File without changes
|
{programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/position_reconciliation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden-2.6.1 → programgarden-2.6.3}/programgarden/database/workflow_position_tracker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|