programgarden 2.6.3__tar.gz → 2.6.5__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.3 → programgarden-2.6.5}/PKG-INFO +9 -1
- {programgarden-2.6.3 → programgarden-2.6.5}/README.md +8 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/context.py +3 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/broker_evidence.py +42 -5
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/executor.py +37 -5
- programgarden-2.6.5/programgarden/futures_exchange.py +102 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/futures_read_evidence.py +27 -8
- {programgarden-2.6.3 → programgarden-2.6.5}/pyproject.toml +1 -1
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/__init__.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/binding_validator.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/client.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/code_worker.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/__init__.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/adjustment_delivery.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/broker_order_totals.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/broker_snapshot.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/db_naming.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/execution_storage.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/fill_reconciler.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/order_cancellation.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/order_recovery.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/owned_order_cancellation.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/position_reconciliation.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/query_builder.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/sqlite_scope.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/deep_fixtures.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/futures_contract_info.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/futures_history.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/futures_orderable.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/futures_pnl.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/incremental_build.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/managed_order_control.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/node_runner.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/order_lifecycle.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/paper_futures_business_date.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/plugin/__init__.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/providers/__init__.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/reconnect_handler.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_contracts.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_events.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_external.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_order_adapter.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_orders.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_scenarios.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_semantics.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_sources.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_sqlite.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/replay_triggers.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/resolver.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/resource/__init__.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/resource/context.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/resource/limiter.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/resource/monitor.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/resource/throttle.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/semantic_rules.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/tools/__init__.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/tools/event_tools.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/tools/job_tools.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/validation_recommender.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/programgarden/validation_replay.py +0 -0
- {programgarden-2.6.3 → programgarden-2.6.5}/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.5
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Author: 프로그램동산
|
|
@@ -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.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
|
|
35
|
+
reads each futures position's and pending order's exchange from o3105 instead
|
|
36
|
+
of a fixed `HKEX`; an unconfirmed exchange stays empty.
|
|
37
|
+
|
|
38
|
+
The 2.6.4 release candidate [paper futures account correction](../../docs/paper-futures-business-date.md#account-node-and-rejection-messages-264-candidate)
|
|
39
|
+
returns an empty futures account for the paper empty-account answer and names
|
|
40
|
+
the broker message in unaccepted response errors. Live behavior is unchanged.
|
|
41
|
+
|
|
34
42
|
The 2.6.3 release candidate [paper futures business-date correction](../../docs/paper-futures-business-date.md)
|
|
35
43
|
uses a fresh account-scoped broker date for startup pending orders, OpenOrdersNode
|
|
36
44
|
and legacy fill observation. Missing or mismatched evidence remains unavailable.
|
|
@@ -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.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
|
|
6
|
+
reads each futures position's and pending order's exchange from o3105 instead
|
|
7
|
+
of a fixed `HKEX`; an unconfirmed exchange stays empty.
|
|
8
|
+
|
|
9
|
+
The 2.6.4 release candidate [paper futures account correction](../../docs/paper-futures-business-date.md#account-node-and-rejection-messages-264-candidate)
|
|
10
|
+
returns an empty futures account for the paper empty-account answer and names
|
|
11
|
+
the broker message in unaccepted response errors. Live behavior is unchanged.
|
|
12
|
+
|
|
5
13
|
The 2.6.3 release candidate [paper futures business-date correction](../../docs/paper-futures-business-date.md)
|
|
6
14
|
uses a fresh account-scoped broker date for startup pending orders, OpenOrdersNode
|
|
7
15
|
and legacy fill observation. Missing or mismatched evidence remains unavailable.
|
|
@@ -285,6 +285,9 @@ class ExecutionContext:
|
|
|
285
285
|
# Tracks workflow positions separately using FIFO for competition ranking
|
|
286
286
|
self._workflow_position_tracker: Optional[Any] = None # WorkflowPositionTracker (lazy init)
|
|
287
287
|
self._personal_metrics_cache: Optional[Tuple[tuple, float, dict]] = None
|
|
288
|
+
# Per-run o3105 exchange answers for futures account/pending rows
|
|
289
|
+
# (programgarden.futures_exchange creates it on first use).
|
|
290
|
+
self._futures_exchange_cache: Optional[Any] = None
|
|
288
291
|
self._workflow_risk_tracker: Optional[Any] = None # WorkflowRiskTracker (lazy init)
|
|
289
292
|
self._workflow_broker_node_id: Optional[str] = None # BrokerNode ID for PnL refresh
|
|
290
293
|
self._workflow_product: str = "overseas_stock" # Product type for PnL refresh
|
|
@@ -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,10 +31,43 @@ _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")
|
|
@@ -95,22 +129,25 @@ async def checked_body(response, tr, request=None, *, paper_trading=False):
|
|
|
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)
|
|
108
142
|
elif paper_trading is True and tr in {"CIDBQ01500", "CIDBQ02400"} and code == "02679":
|
|
109
143
|
# Observed paper response with complete raw blocks, explicit empty rows
|
|
110
144
|
# and terminal continuation. The caller still binds date/account scope.
|
|
111
|
-
|
|
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
|
|
112
147
|
if not accepted:
|
|
113
|
-
|
|
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))
|
|
114
151
|
if tr in {"t0424", "t0425"}:
|
|
115
152
|
cursor = "cts_expcode" if tr == "t0424" else "cts_ordno"
|
|
116
153
|
summary = body[_BLOCKS[tr][0]]
|
|
@@ -6141,7 +6141,9 @@ class AccountNodeExecutor(NodeExecutorBase):
|
|
|
6141
6141
|
"""
|
|
6142
6142
|
from datetime import datetime
|
|
6143
6143
|
from zoneinfo import ZoneInfo
|
|
6144
|
+
from programgarden.futures_exchange import resolve_contract_exchanges
|
|
6144
6145
|
from programgarden.futures_read_evidence import require_complete_rows, require_position_identity
|
|
6146
|
+
from programgarden.paper_futures_business_date import is_paper_client
|
|
6145
6147
|
|
|
6146
6148
|
try:
|
|
6147
6149
|
from programgarden_finance.ls.overseas_futureoption.accno.CIDBQ01500.blocks import CIDBQ01500InBlock1
|
|
@@ -6157,11 +6159,14 @@ class AccountNodeExecutor(NodeExecutorBase):
|
|
|
6157
6159
|
response = await ls.overseas_futureoption().accno().CIDBQ01500(
|
|
6158
6160
|
body=position_request,
|
|
6159
6161
|
).req_async()
|
|
6160
|
-
|
|
6162
|
+
# A paper server answers an empty account with 02679; only that
|
|
6163
|
+
# exact empty envelope is accepted, and only for a paper client.
|
|
6164
|
+
position_rows = require_complete_rows(
|
|
6165
|
+
response, position_request, "CIDBQ01500", paper_trading=is_paper_client(ls),
|
|
6166
|
+
)
|
|
6161
6167
|
|
|
6162
6168
|
# block2 = 종목별 잔고 (list 형태로 변환, NewOrderNode 호환)
|
|
6163
6169
|
positions = []
|
|
6164
|
-
held_symbols = []
|
|
6165
6170
|
for item in position_rows:
|
|
6166
6171
|
require_position_identity(item)
|
|
6167
6172
|
symbol = item.IsuCodeVal.strip() if item.IsuCodeVal else ""
|
|
@@ -6182,7 +6187,9 @@ class AccountNodeExecutor(NodeExecutorBase):
|
|
|
6182
6187
|
|
|
6183
6188
|
positions.append({
|
|
6184
6189
|
"symbol": symbol,
|
|
6185
|
-
|
|
6190
|
+
# CIDBQ01500 carries no exchange. Filled below from o3105
|
|
6191
|
+
# ExchCd; None when that evidence is unavailable.
|
|
6192
|
+
"exchange": None,
|
|
6186
6193
|
"name": item.IsuNm.strip() if hasattr(item, 'IsuNm') and item.IsuNm else symbol,
|
|
6187
6194
|
"direction": position_side, # long/short
|
|
6188
6195
|
"close_side": close_side, # 청산 시 주문 방향: sell/buy
|
|
@@ -6208,7 +6215,6 @@ class AccountNodeExecutor(NodeExecutorBase):
|
|
|
6208
6215
|
"pnl_rate": pnl_rate, # 명목가 대비 수익률(%) — StopLoss/ProfitTarget 소비
|
|
6209
6216
|
"currency": item.CrcyCodeVal.strip() if item.CrcyCodeVal else "",
|
|
6210
6217
|
})
|
|
6211
|
-
held_symbols.append({"exchange": "HKEX", "symbol": symbol})
|
|
6212
6218
|
|
|
6213
6219
|
# 2. CIDBQ05300: 예탁자산 조회 (CIDBQ03000 상위호환).
|
|
6214
6220
|
# Wrapped in its own try so a balance failure no longer drops
|
|
@@ -6283,6 +6289,15 @@ class AccountNodeExecutor(NodeExecutorBase):
|
|
|
6283
6289
|
balance_info["_failure_codes"] = ["CIDBQ05300"]
|
|
6284
6290
|
balance_info["_failure_reason"] = cidbq05300_failure_reason or "CIDBQ05300 fetch failed"
|
|
6285
6291
|
|
|
6292
|
+
# 3. o3105: the exchange of each held contract (at most one read per
|
|
6293
|
+
# symbol per run; unresolved stays None, never a default exchange).
|
|
6294
|
+
exchanges = await resolve_contract_exchanges(
|
|
6295
|
+
ls, [row["symbol"] for row in positions], context, node_id,
|
|
6296
|
+
)
|
|
6297
|
+
for row in positions:
|
|
6298
|
+
row["exchange"] = exchanges.get(row["symbol"])
|
|
6299
|
+
held_symbols = [{"exchange": row["exchange"], "symbol": row["symbol"]} for row in positions]
|
|
6300
|
+
|
|
6286
6301
|
context.log("info", f"AccountNode (futures): {len(positions)} positions, {len(balance_by_currency)} currencies", node_id)
|
|
6287
6302
|
return {
|
|
6288
6303
|
"held_symbols": held_symbols,
|
|
@@ -6658,6 +6673,7 @@ class OpenOrdersNodeExecutor(NodeExecutorBase):
|
|
|
6658
6673
|
"""해외선물 미체결 조회 (CIDBQ02400)"""
|
|
6659
6674
|
from datetime import datetime
|
|
6660
6675
|
from zoneinfo import ZoneInfo
|
|
6676
|
+
from programgarden.futures_exchange import resolve_contract_exchanges
|
|
6661
6677
|
from programgarden.futures_read_evidence import require_complete_rows, require_pending_identity
|
|
6662
6678
|
from programgarden.paper_futures_business_date import is_paper_client, read_current_business_date
|
|
6663
6679
|
from programgarden.database.broker_evidence import StartupEvidenceUnavailable
|
|
@@ -6704,7 +6720,9 @@ class OpenOrdersNodeExecutor(NodeExecutorBase):
|
|
|
6704
6720
|
|
|
6705
6721
|
open_orders.append({
|
|
6706
6722
|
"order_id": order_id,
|
|
6707
|
-
|
|
6723
|
+
# CIDBQ02400 carries no exchange. Filled below from o3105
|
|
6724
|
+
# ExchCd; None when that evidence is unavailable.
|
|
6725
|
+
"exchange": None,
|
|
6708
6726
|
"symbol": item.IsuCodeVal.strip() if hasattr(item, 'IsuCodeVal') and item.IsuCodeVal else "",
|
|
6709
6727
|
"name": item.IsuNm.strip() if hasattr(item, 'IsuNm') and item.IsuNm else "",
|
|
6710
6728
|
"side": side,
|
|
@@ -6716,6 +6734,12 @@ class OpenOrdersNodeExecutor(NodeExecutorBase):
|
|
|
6716
6734
|
"order_time": item.OrdSndDttm if hasattr(item, 'OrdSndDttm') else "",
|
|
6717
6735
|
})
|
|
6718
6736
|
|
|
6737
|
+
exchanges = await resolve_contract_exchanges(
|
|
6738
|
+
ls, [row["symbol"] for row in open_orders], context, node_id,
|
|
6739
|
+
)
|
|
6740
|
+
for row in open_orders:
|
|
6741
|
+
row["exchange"] = exchanges.get(row["symbol"])
|
|
6742
|
+
|
|
6719
6743
|
context.log("info", f"OpenOrdersNode (futures): {len(open_orders)} open orders", node_id)
|
|
6720
6744
|
return {
|
|
6721
6745
|
"open_orders": open_orders,
|
|
@@ -16922,6 +16946,14 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
16922
16946
|
from datetime import date
|
|
16923
16947
|
from decimal import Decimal
|
|
16924
16948
|
|
|
16949
|
+
if not isinstance(order.get("exchange"), str):
|
|
16950
|
+
# A futures position or pending row whose exchange o3105 could not
|
|
16951
|
+
# confirm carries exchange None. Stop before any lifecycle entry is
|
|
16952
|
+
# recorded, so nothing is sent and a later retry is not blocked.
|
|
16953
|
+
raise ValueError(
|
|
16954
|
+
"Futures order exchange is unavailable (exchange=None, e.g. a position "
|
|
16955
|
+
"whose exchange o3105 could not confirm); no order was sent"
|
|
16956
|
+
)
|
|
16925
16957
|
cycle = getattr(context._workflow_job, "_order_cycle", 0)
|
|
16926
16958
|
iteration = iteration_index
|
|
16927
16959
|
if iteration is None:
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Listing exchange of the overseas-futures contracts an account reports.
|
|
2
|
+
|
|
3
|
+
CIDBQ01500 (open positions) and CIDBQ02400 (pending orders) name a contract
|
|
4
|
+
but carry no exchange or market field. The exchange code is read from o3105
|
|
5
|
+
``ExchCd``, the contract-detail TR that FuturesContractInfoNode already uses,
|
|
6
|
+
under the same identity rules as ``futures_contract_info.read_contract_info``:
|
|
7
|
+
HTTP 200, no error, ``rsp_cd`` 00000, an echoed ``Symbol`` equal to the
|
|
8
|
+
requested contract and a non-empty ``ExchCd``. Anything else leaves the
|
|
9
|
+
exchange unknown (``None``). No default exchange is ever substituted.
|
|
10
|
+
|
|
11
|
+
Each execution context (one workflow run) asks o3105 at most once per symbol.
|
|
12
|
+
The answer is cached on the context, including an unknown answer. Reads are
|
|
13
|
+
serialized and spaced like the startup broker reads so the shared app key is
|
|
14
|
+
not burst. Request fields follow
|
|
15
|
+
``finance/example/overseas_futureoption/run_o3105.py`` (symbol only).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from typing import Any, Dict, Iterable, Optional
|
|
21
|
+
|
|
22
|
+
# Same gap the startup broker reads keep between consecutive TRs on one app key
|
|
23
|
+
# (database/broker_snapshot.py). o3105 is limited to 2 requests per second.
|
|
24
|
+
READ_SPACING_SECONDS = 2.0
|
|
25
|
+
READ_TIMEOUT_SECONDS = 20.0
|
|
26
|
+
SOURCE_TR = "o3105"
|
|
27
|
+
|
|
28
|
+
_sleep = asyncio.sleep
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _RunExchangeCache:
|
|
32
|
+
"""Per-run symbol -> exchange answers; ``None`` records an unresolved read."""
|
|
33
|
+
|
|
34
|
+
__slots__ = ("exchanges", "lock")
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self.exchanges: Dict[str, Optional[str]] = {}
|
|
38
|
+
self.lock = asyncio.Lock()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _run_cache(context: Any) -> _RunExchangeCache:
|
|
42
|
+
cache = getattr(context, "_futures_exchange_cache", None)
|
|
43
|
+
if not isinstance(cache, _RunExchangeCache):
|
|
44
|
+
cache = _RunExchangeCache()
|
|
45
|
+
setattr(context, "_futures_exchange_cache", cache)
|
|
46
|
+
return cache
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def exchange_from_contract_detail(response: Any, symbol: str) -> Optional[str]:
|
|
50
|
+
"""Return o3105 ``ExchCd`` for ``symbol``, or ``None`` without exact evidence."""
|
|
51
|
+
if (
|
|
52
|
+
getattr(response, "status_code", None) != 200
|
|
53
|
+
or getattr(response, "error_msg", None)
|
|
54
|
+
or getattr(response, "rsp_cd", None) != "00000"
|
|
55
|
+
):
|
|
56
|
+
return None
|
|
57
|
+
block = getattr(response, "block", None)
|
|
58
|
+
fields = getattr(block, "model_fields_set", None)
|
|
59
|
+
if block is None or not isinstance(fields, (set, frozenset)) or not {"Symbol", "ExchCd"} <= fields:
|
|
60
|
+
return None
|
|
61
|
+
echoed, exchange = getattr(block, "Symbol", None), getattr(block, "ExchCd", None)
|
|
62
|
+
if not isinstance(echoed, str) or echoed.strip() != symbol or not isinstance(exchange, str):
|
|
63
|
+
return None
|
|
64
|
+
return exchange.strip() or None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _read_exchange(ls: Any, symbol: str) -> Optional[str]:
|
|
68
|
+
from programgarden_finance.ls.overseas_futureoption.market.o3105.blocks import O3105InBlock
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
response = await asyncio.wait_for(
|
|
72
|
+
ls.overseas_futureoption().market().o3105(body=O3105InBlock(symbol=symbol)).req_async(),
|
|
73
|
+
timeout=READ_TIMEOUT_SECONDS,
|
|
74
|
+
)
|
|
75
|
+
except Exception: # noqa: BLE001 - an unavailable read leaves the exchange unknown
|
|
76
|
+
return None
|
|
77
|
+
return exchange_from_contract_detail(response, symbol)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def resolve_contract_exchanges(
|
|
81
|
+
ls: Any, symbols: Iterable[str], context: Any, node_id: str,
|
|
82
|
+
) -> Dict[str, Optional[str]]:
|
|
83
|
+
"""Map each distinct contract symbol to its o3105 exchange code or ``None``."""
|
|
84
|
+
wanted = [symbol for symbol in dict.fromkeys(symbols) if isinstance(symbol, str) and symbol]
|
|
85
|
+
if not wanted:
|
|
86
|
+
return {}
|
|
87
|
+
cache = _run_cache(context)
|
|
88
|
+
async with cache.lock:
|
|
89
|
+
for symbol in wanted:
|
|
90
|
+
if symbol in cache.exchanges:
|
|
91
|
+
continue
|
|
92
|
+
await _sleep(READ_SPACING_SECONDS)
|
|
93
|
+
exchange = await _read_exchange(ls, symbol)
|
|
94
|
+
cache.exchanges[symbol] = exchange
|
|
95
|
+
if exchange is None:
|
|
96
|
+
context.log(
|
|
97
|
+
"warning",
|
|
98
|
+
f"Futures exchange for {symbol} is unavailable ({SOURCE_TR} gave no matching "
|
|
99
|
+
"exchange code); the row's exchange is left empty for this run",
|
|
100
|
+
node_id,
|
|
101
|
+
)
|
|
102
|
+
return {symbol: cache.exchanges[symbol] for symbol in wanted}
|
|
@@ -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
|
|
|
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.3 → programgarden-2.6.5}/programgarden/database/owned_order_cancellation.py
RENAMED
|
File without changes
|
{programgarden-2.6.3 → programgarden-2.6.5}/programgarden/database/position_reconciliation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden-2.6.3 → programgarden-2.6.5}/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
|
|
File without changes
|