programgarden 2.6.2__tar.gz → 2.6.4__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.2 → programgarden-2.6.4}/PKG-INFO +10 -2
- {programgarden-2.6.2 → programgarden-2.6.4}/README.md +8 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/broker_evidence.py +46 -5
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/broker_snapshot.py +15 -3
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/executor.py +40 -15
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/futures_read_evidence.py +27 -8
- programgarden-2.6.4/programgarden/paper_futures_business_date.py +96 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/pyproject.toml +2 -2
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/__init__.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/binding_validator.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/client.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/code_worker.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/context.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/__init__.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/adjustment_delivery.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/broker_order_totals.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/db_naming.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/execution_storage.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/fill_reconciler.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/order_cancellation.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/order_recovery.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/owned_order_cancellation.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/position_reconciliation.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/query_builder.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/sqlite_scope.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/deep_fixtures.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/futures_contract_info.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/futures_history.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/futures_orderable.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/futures_pnl.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/incremental_build.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/managed_order_control.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/node_runner.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/order_lifecycle.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/plugin/__init__.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/providers/__init__.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/reconnect_handler.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_contracts.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_events.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_external.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_order_adapter.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_orders.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_scenarios.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_semantics.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_sources.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_sqlite.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/replay_triggers.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/resolver.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/resource/__init__.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/resource/context.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/resource/limiter.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/resource/monitor.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/resource/throttle.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/semantic_rules.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/tools/__init__.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/tools/event_tools.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/tools/job_tools.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/validation_recommender.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/programgarden/validation_replay.py +0 -0
- {programgarden-2.6.2 → programgarden-2.6.4}/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.4
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Author: 프로그램동산
|
|
@@ -17,7 +17,7 @@ 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
19
|
Requires-Dist: programgarden-core (>=2.6.2,<3.0.0)
|
|
20
|
-
Requires-Dist: programgarden-finance (>=2.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)
|
|
@@ -31,6 +31,14 @@ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures
|
|
|
31
31
|
|
|
32
32
|
# ProgramGarden
|
|
33
33
|
|
|
34
|
+
The 2.6.4 release candidate [paper futures account correction](../../docs/paper-futures-business-date.md#account-node-and-rejection-messages-264-candidate)
|
|
35
|
+
returns an empty futures account for the paper empty-account answer and names
|
|
36
|
+
the broker message in unaccepted response errors. Live behavior is unchanged.
|
|
37
|
+
|
|
38
|
+
The 2.6.3 release candidate [paper futures business-date correction](../../docs/paper-futures-business-date.md)
|
|
39
|
+
uses a fresh account-scoped broker date for startup pending orders, OpenOrdersNode
|
|
40
|
+
and legacy fill observation. Missing or mismatched evidence remains unavailable.
|
|
41
|
+
|
|
34
42
|
An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
|
|
35
43
|
rejects unsupported intraday periods instead of silently querying daily bars.
|
|
36
44
|
Supported g3204 periods retain their existing requests and output contract.
|
|
@@ -2,6 +2,14 @@ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures
|
|
|
2
2
|
|
|
3
3
|
# ProgramGarden
|
|
4
4
|
|
|
5
|
+
The 2.6.4 release candidate [paper futures account correction](../../docs/paper-futures-business-date.md#account-node-and-rejection-messages-264-candidate)
|
|
6
|
+
returns an empty futures account for the paper empty-account answer and names
|
|
7
|
+
the broker message in unaccepted response errors. Live behavior is unchanged.
|
|
8
|
+
|
|
9
|
+
The 2.6.3 release candidate [paper futures business-date correction](../../docs/paper-futures-business-date.md)
|
|
10
|
+
uses a fresh account-scoped broker date for startup pending orders, OpenOrdersNode
|
|
11
|
+
and legacy fill observation. Missing or mismatched evidence remains unavailable.
|
|
12
|
+
|
|
5
13
|
An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
|
|
6
14
|
rejects unsupported intraday periods instead of silently querying daily bars.
|
|
7
15
|
Supported g3204 periods retain their existing requests and output contract.
|
|
@@ -5,6 +5,7 @@ not turn a missing detail block, partial page or rejection into an empty account
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import inspect
|
|
8
|
+
import unicodedata
|
|
8
9
|
from decimal import Decimal, InvalidOperation
|
|
9
10
|
from datetime import datetime, timedelta, timezone
|
|
10
11
|
|
|
@@ -30,17 +31,50 @@ _OBSERVED_RESPONSE_CODES = {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
_BROKER_MESSAGE_LIMIT = 80
|
|
35
|
+
|
|
36
|
+
# Exact empty-result wording observed with rsp_cd 02679 on the stock reads below
|
|
37
|
+
# and on paper CIDBQ01500/CIDBQ02400 reads (2026-10-14). Not a generic success.
|
|
38
|
+
NO_RESULT_MESSAGE = "조회내역이 없습니다."
|
|
39
|
+
# The paper server's own empty-result wording, observed with rsp_cd 00707 on
|
|
40
|
+
# the two futures TRs below.
|
|
41
|
+
PAPER_NO_RESULT_MESSAGE = "모의투자 조회할 내역(자료)이 없습니다."
|
|
42
|
+
# The paper CIDBQ01500/CIDBQ02400 02679 branch accepts either exact wording:
|
|
43
|
+
# the same set the dsl-api startup gate uses for these two TRs
|
|
44
|
+
# (cloudrun/dsl-api/app/services/startup_broker.py, _NO_DATA_MESSAGES), so a
|
|
45
|
+
# start the server admits is not rejected by the engine. Stock branches keep
|
|
46
|
+
# the single NO_RESULT_MESSAGE, as the server gate does.
|
|
47
|
+
PAPER_FUTURES_NO_RESULT_MESSAGES = frozenset({NO_RESULT_MESSAGE, PAPER_NO_RESULT_MESSAGE})
|
|
48
|
+
|
|
49
|
+
|
|
33
50
|
class StartupEvidenceUnavailable(RuntimeError):
|
|
34
51
|
"""An incomplete response must never authorize execution."""
|
|
35
52
|
|
|
36
53
|
|
|
54
|
+
def broker_message(value):
|
|
55
|
+
"""Return the broker's diagnostic text as one bounded log-safe line.
|
|
56
|
+
|
|
57
|
+
Matches the server startup gate's ``str(rsp_msg or '')[:80]`` for ordinary
|
|
58
|
+
messages; control/format characters become spaces and outer whitespace is
|
|
59
|
+
stripped so a broker message cannot inject extra log lines.
|
|
60
|
+
"""
|
|
61
|
+
text = "".join(" " if unicodedata.category(char).startswith("C") else char
|
|
62
|
+
for char in str(value or ""))
|
|
63
|
+
return text.strip()[:_BROKER_MESSAGE_LIMIT]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def unverified_response_code(tr, code, message):
|
|
67
|
+
"""Keep the stable ``<TR>: unverified_response_code=<code>`` prefix."""
|
|
68
|
+
return f"{tr}: unverified_response_code={code} rsp_msg={broker_message(message)}"
|
|
69
|
+
|
|
70
|
+
|
|
37
71
|
class StartupQueryDateMismatch(StartupEvidenceUnavailable):
|
|
38
72
|
def __init__(self, business_day):
|
|
39
73
|
super().__init__("COSAQ00102: query_echo_mismatch:OrdDt")
|
|
40
74
|
self.business_day = business_day
|
|
41
75
|
|
|
42
76
|
|
|
43
|
-
async def checked_body(response, tr, request=None):
|
|
77
|
+
async def checked_body(response, tr, request=None, *, paper_trading=False):
|
|
44
78
|
"""Read original broker blocks so SDK defaults cannot manufacture success."""
|
|
45
79
|
if response.status_code != 200 or response.error_msg:
|
|
46
80
|
raise StartupEvidenceUnavailable(f"{tr}: transport_failure")
|
|
@@ -95,18 +129,25 @@ async def checked_body(response, tr, request=None):
|
|
|
95
129
|
if tr == "COSOQ00201" and code == "02679":
|
|
96
130
|
# Observed authenticated empty stock balance, 2026-09-21. Explicit
|
|
97
131
|
# blocks, terminal pagination and matching query scope remain required.
|
|
98
|
-
accepted = not rows and response.rsp_msg ==
|
|
132
|
+
accepted = not rows and response.rsp_msg == NO_RESULT_MESSAGE
|
|
99
133
|
if tr == "COSAQ00102" and code == "02679":
|
|
100
134
|
echo = body["COSAQ00102OutBlock1"]
|
|
101
135
|
accepted = (not rows and isinstance(echo, dict)
|
|
102
|
-
and response.rsp_msg ==
|
|
136
|
+
and response.rsp_msg == NO_RESULT_MESSAGE
|
|
103
137
|
and echo.get("OrdMktCode") == "%"
|
|
104
138
|
and echo.get("ThdayBnsAppYn") == "1"
|
|
105
139
|
and echo.get("BnsTpCode") == "0" and echo.get("CrcyCode") == "000")
|
|
106
140
|
elif tr in {"CIDBQ01500", "CIDBQ02400"} and code == "00707":
|
|
107
|
-
accepted = (not rows and response.rsp_msg ==
|
|
141
|
+
accepted = (not rows and response.rsp_msg == PAPER_NO_RESULT_MESSAGE)
|
|
142
|
+
elif paper_trading is True and tr in {"CIDBQ01500", "CIDBQ02400"} and code == "02679":
|
|
143
|
+
# Observed paper response with complete raw blocks, explicit empty rows
|
|
144
|
+
# and terminal continuation. The caller still binds date/account scope.
|
|
145
|
+
# The wording set matches the server startup gate for these two TRs.
|
|
146
|
+
accepted = not rows and response.rsp_msg in PAPER_FUTURES_NO_RESULT_MESSAGES
|
|
108
147
|
if not accepted:
|
|
109
|
-
|
|
148
|
+
# The broker message distinguishes, for example, an unrecognized empty
|
|
149
|
+
# wording from a rejected query; the stable reason prefix is unchanged.
|
|
150
|
+
raise StartupEvidenceUnavailable(unverified_response_code(tr, code, response.rsp_msg))
|
|
110
151
|
if tr in {"t0424", "t0425"}:
|
|
111
152
|
cursor = "cts_expcode" if tr == "t0424" else "cts_ordno"
|
|
112
153
|
summary = body[_BLOCKS[tr][0]]
|
|
@@ -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:
|
|
@@ -6142,6 +6142,7 @@ class AccountNodeExecutor(NodeExecutorBase):
|
|
|
6142
6142
|
from datetime import datetime
|
|
6143
6143
|
from zoneinfo import ZoneInfo
|
|
6144
6144
|
from programgarden.futures_read_evidence import require_complete_rows, require_position_identity
|
|
6145
|
+
from programgarden.paper_futures_business_date import is_paper_client
|
|
6145
6146
|
|
|
6146
6147
|
try:
|
|
6147
6148
|
from programgarden_finance.ls.overseas_futureoption.accno.CIDBQ01500.blocks import CIDBQ01500InBlock1
|
|
@@ -6157,7 +6158,11 @@ class AccountNodeExecutor(NodeExecutorBase):
|
|
|
6157
6158
|
response = await ls.overseas_futureoption().accno().CIDBQ01500(
|
|
6158
6159
|
body=position_request,
|
|
6159
6160
|
).req_async()
|
|
6160
|
-
|
|
6161
|
+
# A paper server answers an empty account with 02679; only that
|
|
6162
|
+
# exact empty envelope is accepted, and only for a paper client.
|
|
6163
|
+
position_rows = require_complete_rows(
|
|
6164
|
+
response, position_request, "CIDBQ01500", paper_trading=is_paper_client(ls),
|
|
6165
|
+
)
|
|
6161
6166
|
|
|
6162
6167
|
# block2 = 종목별 잔고 (list 형태로 변환, NewOrderNode 호환)
|
|
6163
6168
|
positions = []
|
|
@@ -6659,21 +6664,33 @@ class OpenOrdersNodeExecutor(NodeExecutorBase):
|
|
|
6659
6664
|
from datetime import datetime
|
|
6660
6665
|
from zoneinfo import ZoneInfo
|
|
6661
6666
|
from programgarden.futures_read_evidence import require_complete_rows, require_pending_identity
|
|
6667
|
+
from programgarden.paper_futures_business_date import is_paper_client, read_current_business_date
|
|
6668
|
+
from programgarden.database.broker_evidence import StartupEvidenceUnavailable
|
|
6662
6669
|
from programgarden_finance.ls.overseas_futureoption.accno.CIDBQ02400.blocks import CIDBQ02400InBlock1
|
|
6663
6670
|
|
|
6664
6671
|
|
|
6665
|
-
|
|
6672
|
+
paper_day = None
|
|
6673
|
+
try:
|
|
6674
|
+
if is_paper_client(ls):
|
|
6675
|
+
paper_day = await read_current_business_date(ls)
|
|
6676
|
+
except StartupEvidenceUnavailable as exc:
|
|
6677
|
+
return self._empty_result(str(exc))
|
|
6678
|
+
today = paper_day.day if paper_day else datetime.now(ZoneInfo("Asia/Seoul")).strftime("%Y%m%d")
|
|
6666
6679
|
request = CIDBQ02400InBlock1(
|
|
6667
6680
|
RecCnt=1, IsuCodeVal="", QrySrtDt=today, QryEndDt=today,
|
|
6668
6681
|
ThdayTpCode="1", OrdStatCode="2", BnsTpCode="0", QryTpCode="2",
|
|
6669
6682
|
OrdPtnCode="00", OvrsDrvtFnoTpCode="A",
|
|
6670
6683
|
)
|
|
6671
|
-
response = await ls.overseas_futureoption().accno().CIDBQ02400(body=request).req_async()
|
|
6672
6684
|
try:
|
|
6673
|
-
|
|
6685
|
+
if paper_day:
|
|
6686
|
+
paper_day.scope.check(ls)
|
|
6687
|
+
query = ls.overseas_futureoption().accno().CIDBQ02400(body=request)
|
|
6688
|
+
response = await query.req_async()
|
|
6689
|
+
rows = (await paper_day.check_orders(ls, response, query) if paper_day else
|
|
6690
|
+
require_complete_rows(response, request, "CIDBQ02400"))
|
|
6674
6691
|
for row in rows:
|
|
6675
6692
|
require_pending_identity(row)
|
|
6676
|
-
except ValueError as exc:
|
|
6693
|
+
except (ValueError, StartupEvidenceUnavailable) as exc:
|
|
6677
6694
|
return self._empty_result(str(exc))
|
|
6678
6695
|
|
|
6679
6696
|
open_orders = []
|
|
@@ -16849,17 +16866,21 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
16849
16866
|
from programgarden_finance.ls.overseas_futureoption.accno.CIDBQ02400.blocks import (
|
|
16850
16867
|
CIDBQ02400InBlock1,
|
|
16851
16868
|
)
|
|
16852
|
-
|
|
16853
|
-
|
|
16854
|
-
#
|
|
16855
|
-
#
|
|
16856
|
-
#
|
|
16857
|
-
|
|
16858
|
-
|
|
16869
|
+
from programgarden.paper_futures_business_date import is_paper_client, read_current_business_date
|
|
16870
|
+
|
|
16871
|
+
# A paper server can reject the documented blank same-day dates.
|
|
16872
|
+
# Use its fresh account-scoped date, including overnight sessions;
|
|
16873
|
+
# never replace missing evidence with the machine's calendar date.
|
|
16874
|
+
paper_day = await read_current_business_date(ls) if is_paper_client(ls) else None
|
|
16875
|
+
query_day = paper_day.day if paper_day else ""
|
|
16876
|
+
if paper_day:
|
|
16877
|
+
paper_day.scope.check(ls)
|
|
16878
|
+
query = ls.overseas_futureoption().accno().CIDBQ02400(
|
|
16859
16879
|
body=CIDBQ02400InBlock1(
|
|
16880
|
+
RecCnt=1,
|
|
16860
16881
|
IsuCodeVal="", # 전체 종목
|
|
16861
|
-
QrySrtDt=
|
|
16862
|
-
QryEndDt=
|
|
16882
|
+
QrySrtDt=query_day,
|
|
16883
|
+
QryEndDt=query_day,
|
|
16863
16884
|
ThdayTpCode="1", # 1: 당일조회
|
|
16864
16885
|
OrdStatCode="0", # 0: 전체(체결+미체결) — 상태 판정에 필요
|
|
16865
16886
|
BnsTpCode="0", # 0: 전체
|
|
@@ -16867,7 +16888,11 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
16867
16888
|
OrdPtnCode="00", # 00: 전체
|
|
16868
16889
|
OvrsDrvtFnoTpCode="A", # A: 전체(선물+옵션)
|
|
16869
16890
|
)
|
|
16870
|
-
)
|
|
16891
|
+
)
|
|
16892
|
+
response = await query.req_async()
|
|
16893
|
+
|
|
16894
|
+
if paper_day:
|
|
16895
|
+
await paper_day.check_orders(ls, response, query)
|
|
16871
16896
|
|
|
16872
16897
|
if getattr(response, "error_msg", None):
|
|
16873
16898
|
context.log("debug", f"CIDBQ02400 fill query error: {response.error_msg}", node_id)
|
|
@@ -2,19 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
import math
|
|
4
4
|
|
|
5
|
+
from programgarden.database.broker_evidence import (
|
|
6
|
+
PAPER_FUTURES_NO_RESULT_MESSAGES,
|
|
7
|
+
unverified_response_code,
|
|
8
|
+
)
|
|
5
9
|
|
|
6
|
-
|
|
10
|
+
_NO_DATA_TRS = {"CIDBQ01500", "CIDBQ02400"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def require_complete_rows(response, request, tr, *, paper_trading=False):
|
|
7
14
|
"""Validate the observed terminal envelope and return its detail rows.
|
|
8
15
|
|
|
9
16
|
00707 is accepted only for an explicitly empty, matching terminal response
|
|
10
17
|
for the two position/pending TRs observed on the paper server. It is not a
|
|
11
18
|
generic success code. Continuation pages remain unavailable in this reader.
|
|
19
|
+
|
|
20
|
+
For a paper client only, 02679 with one of the exact paper empty-result
|
|
21
|
+
messages (the startup gate's set for these two TRs) and explicitly empty
|
|
22
|
+
rows is the same complete empty result on those two TRs. Live clients and
|
|
23
|
+
other TRs still reject it.
|
|
12
24
|
"""
|
|
25
|
+
if response.status_code != 200 or response.error_msg:
|
|
26
|
+
raise ValueError(f"{tr} did not return a complete terminal response")
|
|
27
|
+
code = response.rsp_cd
|
|
28
|
+
accepted = code in {"00000", "00136", "00707"} or (
|
|
29
|
+
paper_trading is True
|
|
30
|
+
and tr in _NO_DATA_TRS
|
|
31
|
+
and code == "02679"
|
|
32
|
+
and not response.block2
|
|
33
|
+
and response.rsp_msg in PAPER_FUTURES_NO_RESULT_MESSAGES
|
|
34
|
+
)
|
|
35
|
+
if not accepted:
|
|
36
|
+
raise ValueError(unverified_response_code(tr, code, response.rsp_msg))
|
|
13
37
|
if (
|
|
14
|
-
response.
|
|
15
|
-
or response.error_msg
|
|
16
|
-
or response.rsp_cd not in {"00000", "00136", "00707"}
|
|
17
|
-
or response.header is None
|
|
38
|
+
response.header is None
|
|
18
39
|
or response.header.tr_cd not in {tr, "CIDBQ"}
|
|
19
40
|
or response.header.tr_cont != "N"
|
|
20
41
|
or response.header.tr_cont_key
|
|
@@ -29,9 +50,7 @@ def require_complete_rows(response, request, tr):
|
|
|
29
50
|
or any(getattr(echo, field) != value for field, value in expected.items())
|
|
30
51
|
):
|
|
31
52
|
raise ValueError(f"{tr} query echo is missing or mismatched")
|
|
32
|
-
if
|
|
33
|
-
tr not in {"CIDBQ01500", "CIDBQ02400"} or response.block2
|
|
34
|
-
):
|
|
53
|
+
if code == "00707" and (tr not in _NO_DATA_TRS or response.block2):
|
|
35
54
|
raise ValueError(f"{tr} no-data response is inconsistent")
|
|
36
55
|
return response.block2
|
|
37
56
|
|
|
@@ -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)
|
|
@@ -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.4"
|
|
9
9
|
license = "AGPL-3.0-or-later"
|
|
10
10
|
description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
|
|
11
11
|
readme = "README.md"
|
|
@@ -37,7 +37,7 @@ litellm = ">=1.40.0"
|
|
|
37
37
|
# (1.26.0 이후로도 context.py 는 WorkflowPnLEvent(**event_data) 에 personal_metrics
|
|
38
38
|
# 를 무조건 실으므로 그 하한 근거도 이 상한에 포함된다.)
|
|
39
39
|
programgarden-core = "^2.6.2"
|
|
40
|
-
programgarden-finance = "^2.0.
|
|
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.2 → programgarden-2.6.4}/programgarden/database/owned_order_cancellation.py
RENAMED
|
File without changes
|
{programgarden-2.6.2 → programgarden-2.6.4}/programgarden/database/position_reconciliation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden-2.6.2 → programgarden-2.6.4}/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
|
|
File without changes
|