programgarden 1.22.2__tar.gz → 1.22.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-1.22.2 → programgarden-1.22.4}/PKG-INFO +6 -5
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/context.py +5 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/executor.py +175 -23
- {programgarden-1.22.2 → programgarden-1.22.4}/pyproject.toml +4 -9
- {programgarden-1.22.2 → programgarden-1.22.4}/README.md +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/__init__.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/binding_validator.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/client.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/database/__init__.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/node_runner.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/resolver.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/resource/context.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-1.22.2 → programgarden-1.22.4}/programgarden/validation_recommender.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: programgarden
|
|
3
|
-
Version: 1.22.
|
|
3
|
+
Version: 1.22.4
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
Author: 프로그램동산
|
|
6
6
|
Author-email: coding@programgarden.com
|
|
@@ -8,14 +8,15 @@ Requires-Python: >=3.12
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
12
|
Requires-Dist: aiohttp (>=3.9.0,<4.0.0)
|
|
12
13
|
Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
|
|
13
14
|
Requires-Dist: croniter (>=6.0.0,<7.0.0)
|
|
14
15
|
Requires-Dist: litellm (>=1.40.0)
|
|
15
16
|
Requires-Dist: lxml (>=6.0.2,<7.0.0)
|
|
16
|
-
Requires-Dist: programgarden-community (>=1.13.
|
|
17
|
-
Requires-Dist: programgarden-core (>=1.14.
|
|
18
|
-
Requires-Dist: programgarden-finance (>=1.6.
|
|
17
|
+
Requires-Dist: programgarden-community (>=1.13.8,<2.0.0)
|
|
18
|
+
Requires-Dist: programgarden-core (>=1.14.3,<2.0.0)
|
|
19
|
+
Requires-Dist: programgarden-finance (>=1.6.10,<2.0.0)
|
|
19
20
|
Requires-Dist: psutil (>=6.0.0,<7.0.0)
|
|
20
21
|
Requires-Dist: psycopg2-binary (>=2.9.11,<3.0.0)
|
|
21
22
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
@@ -150,12 +150,17 @@ class ExecutionContext:
|
|
|
150
150
|
workflow_edges: Optional[List[Any]] = None, # List[ResolvedEdge]
|
|
151
151
|
workflow_nodes: Optional[Dict[str, Any]] = None, # Dict[str, ResolvedNode]
|
|
152
152
|
storage_dir: Optional[str] = None,
|
|
153
|
+
ls_token_provider: Optional[Any] = None, # sync (appkey, product, paper_trading) -> (token, expires_at_epoch)
|
|
153
154
|
):
|
|
154
155
|
self.job_id = job_id
|
|
155
156
|
self.workflow_id = workflow_id
|
|
156
157
|
self._storage_dir = storage_dir
|
|
157
158
|
self.context_params = context_params or {}
|
|
158
159
|
|
|
160
|
+
# Opt-in LS token provider (Verified League §3.2.3). When set, broker
|
|
161
|
+
# logins consume a server-issued token instead of self-issuing.
|
|
162
|
+
self.ls_token_provider = ls_token_provider
|
|
163
|
+
|
|
159
164
|
# Secrets storage (never logged, separate from context_params)
|
|
160
165
|
self._secrets: Dict[str, Any] = secrets or {}
|
|
161
166
|
|
|
@@ -277,7 +277,23 @@ class LSClientManager:
|
|
|
277
277
|
# 새 인스턴스 생성 (싱글톤 우회)
|
|
278
278
|
ls = object.__new__(LS)
|
|
279
279
|
ls.__init__()
|
|
280
|
-
|
|
280
|
+
|
|
281
|
+
# Verified League §3.2.3: when a token provider is configured, route this
|
|
282
|
+
# LS instance through it (server = single issuer) so login consumes a
|
|
283
|
+
# server-issued token instead of self-issuing via GenerateToken. login()
|
|
284
|
+
# is synchronous, so we register a sync provider (it is also reused by the
|
|
285
|
+
# async refresh path as a fallback). Bound to this instance's
|
|
286
|
+
# appkey/product/paper_trading; returns (access_token, expires_at_epoch).
|
|
287
|
+
token_provider = getattr(context, "ls_token_provider", None)
|
|
288
|
+
if token_provider is not None:
|
|
289
|
+
def _sync_token_provider(
|
|
290
|
+
_appkey=appkey, _product=product, _paper=paper_trading,
|
|
291
|
+
):
|
|
292
|
+
return token_provider(_appkey, _product, _paper)
|
|
293
|
+
|
|
294
|
+
ls.set_token_provider(provider=_sync_token_provider)
|
|
295
|
+
context.log("info", f"LS token provider attached for {product}", node_id)
|
|
296
|
+
|
|
281
297
|
# 로그인
|
|
282
298
|
login_result = ls.login(
|
|
283
299
|
appkey=appkey,
|
|
@@ -8364,39 +8380,48 @@ class LogicNodeExecutor(NodeExecutorBase):
|
|
|
8364
8380
|
condition_results: List[Dict[str, Any]] = []
|
|
8365
8381
|
all_passed_symbols: List[List[str]] = []
|
|
8366
8382
|
weights: Dict[int, float] = {} # index -> weight
|
|
8367
|
-
|
|
8383
|
+
|
|
8368
8384
|
for idx, cond in enumerate(conditions):
|
|
8369
8385
|
# 조건 객체 검증
|
|
8370
8386
|
if not isinstance(cond, dict):
|
|
8371
8387
|
context.log("warning", f"Condition at index {idx} is not a dict, skipping", node_id)
|
|
8372
8388
|
continue
|
|
8373
|
-
|
|
8389
|
+
|
|
8374
8390
|
# is_condition_met 필수
|
|
8391
|
+
# 다종목 auto-iterate 시 is_condition_met 바인딩이 병합 리스트
|
|
8392
|
+
# ([False, False, ...]) 로 해석될 수 있다. 리스트는 "통과 여부"가
|
|
8393
|
+
# 아니라 "실행 여부"를 뜻하게 되므로, any() 로 스칼라화해 per-condition
|
|
8394
|
+
# 통과 여부로 환원한다. (None 은 미제공 → 기존처럼 False + 경고)
|
|
8375
8395
|
is_met = cond.get("is_condition_met")
|
|
8376
8396
|
if is_met is None:
|
|
8377
8397
|
context.log("warning", f"Condition at index {idx} missing 'is_condition_met', treating as False", node_id)
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8398
|
+
is_met_scalar = False
|
|
8399
|
+
elif isinstance(is_met, list):
|
|
8400
|
+
is_met_scalar = any(bool(x) for x in is_met)
|
|
8401
|
+
else:
|
|
8402
|
+
is_met_scalar = bool(is_met)
|
|
8403
|
+
|
|
8404
|
+
# passed_symbols: None(미제공) 과 [](명시 빈 리스트) 를 구분한다.
|
|
8405
|
+
# - 명시 리스트(빈 리스트 포함) → "symbol-bearing" 조건 (교집합 참여)
|
|
8406
|
+
# - 미제공/None/비리스트 → "boolean-gate" 조건 (bool 게이팅만)
|
|
8407
|
+
raw_passed = cond.get("passed_symbols")
|
|
8408
|
+
symbols_provided = isinstance(raw_passed, list)
|
|
8409
|
+
passed_symbols = raw_passed if symbols_provided else []
|
|
8410
|
+
|
|
8387
8411
|
# weight (optional, default 1.0)
|
|
8388
8412
|
weight = cond.get("weight", 1.0)
|
|
8389
8413
|
if not isinstance(weight, (int, float)):
|
|
8390
8414
|
weight = 1.0
|
|
8391
8415
|
weights[idx] = float(weight)
|
|
8392
|
-
|
|
8416
|
+
|
|
8393
8417
|
condition_results.append({
|
|
8394
8418
|
"index": idx,
|
|
8395
|
-
"result":
|
|
8419
|
+
"result": is_met_scalar,
|
|
8396
8420
|
"passed_symbols": passed_symbols,
|
|
8421
|
+
"symbols_provided": symbols_provided,
|
|
8397
8422
|
"weight": weight,
|
|
8398
8423
|
})
|
|
8399
|
-
all_passed_symbols.append(passed_symbols
|
|
8424
|
+
all_passed_symbols.append(passed_symbols)
|
|
8400
8425
|
|
|
8401
8426
|
if not condition_results:
|
|
8402
8427
|
context.log("warning", "No valid condition results to combine", node_id)
|
|
@@ -8486,8 +8511,16 @@ class LogicNodeExecutor(NodeExecutorBase):
|
|
|
8486
8511
|
elif isinstance(s, str):
|
|
8487
8512
|
codes.add(s)
|
|
8488
8513
|
return codes
|
|
8489
|
-
|
|
8490
|
-
|
|
8514
|
+
|
|
8515
|
+
# symbol-bearing 조건(passed_symbols 명시 제공)만 교집합에 참여한다.
|
|
8516
|
+
# 빈 리스트도 명시 제공이면 포함 → AND 교집합을 [] 로 영점화한다.
|
|
8517
|
+
# boolean-gate 조건(미제공)은 심볼 의미가 없으므로 교집합에서 제외하고
|
|
8518
|
+
# bool(result) 로만 게이팅된다.
|
|
8519
|
+
sets = [
|
|
8520
|
+
extract_codes(r["passed_symbols"])
|
|
8521
|
+
for r in results
|
|
8522
|
+
if r.get("symbols_provided")
|
|
8523
|
+
]
|
|
8491
8524
|
if not sets:
|
|
8492
8525
|
return []
|
|
8493
8526
|
common = sets[0]
|
|
@@ -9968,11 +10001,18 @@ class BacktestEngineNodeExecutor(NodeExecutorBase):
|
|
|
9968
10001
|
context.log("warning", f"External binding evaluation failed for {target_field}: {e}", node_id)
|
|
9969
10002
|
external_values[target_field] = source_expr
|
|
9970
10003
|
|
|
10004
|
+
# 기본 컨텍스트 dict (row 제외) — ExpressionContext 는 mapping 이 아니므로
|
|
10005
|
+
# 반드시 to_dict() 로 평탄화해야 한다. 이전엔 {**expr_context} 로 객체를
|
|
10006
|
+
# 직접 언팩하려다 비어있지 않은 from_data 의 첫 row 에서 TypeError 로
|
|
10007
|
+
# 크래시했다 (dry_run 은 mock 이 빈 time_series 를 줘 루프가 안 돌아 은폐됨).
|
|
10008
|
+
base_dict = expr_context.to_dict()
|
|
10009
|
+
|
|
9971
10010
|
# 각 row 처리
|
|
9972
10011
|
for row in from_data:
|
|
9973
10012
|
record = {}
|
|
9974
|
-
|
|
9975
|
-
|
|
10013
|
+
row_ctx = ExpressionContext()
|
|
10014
|
+
row_ctx.variables = {**base_dict, "row": row}
|
|
10015
|
+
row_evaluator = ExpressionEvaluator(row_ctx)
|
|
9976
10016
|
|
|
9977
10017
|
for target_field, source_expr in extract.items():
|
|
9978
10018
|
if target_field in external_values:
|
|
@@ -12891,6 +12931,7 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
|
|
|
12891
12931
|
)
|
|
12892
12932
|
return {
|
|
12893
12933
|
"order_id": order_id,
|
|
12934
|
+
"modified_order_id": order_id,
|
|
12894
12935
|
"status": "simulated",
|
|
12895
12936
|
"dry_run": True,
|
|
12896
12937
|
"requested": config,
|
|
@@ -13038,13 +13079,33 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
|
|
|
13038
13079
|
"original_order_id": original_order_id,
|
|
13039
13080
|
"product": "overseas_stock",
|
|
13040
13081
|
},
|
|
13082
|
+
"modified_order_id": "",
|
|
13041
13083
|
"modified_order": None,
|
|
13042
13084
|
}
|
|
13043
13085
|
|
|
13044
13086
|
new_order_no = ""
|
|
13045
13087
|
if response.block2:
|
|
13046
13088
|
new_order_no = str(response.block2.OrdNo) if response.block2.OrdNo else ""
|
|
13047
|
-
|
|
13089
|
+
|
|
13090
|
+
# 정정 빈-주문번호 가드 (거래시간 외/정정 불가 상태 silent no-op 차단)
|
|
13091
|
+
if not new_order_no:
|
|
13092
|
+
msg = response.rsp_msg or "정정 미반영"
|
|
13093
|
+
context.log(
|
|
13094
|
+
"warning",
|
|
13095
|
+
f"Modify order returned no OrderNo: {symbol} - {msg}",
|
|
13096
|
+
node_id,
|
|
13097
|
+
)
|
|
13098
|
+
return {
|
|
13099
|
+
"modify_result": {
|
|
13100
|
+
"success": False,
|
|
13101
|
+
"error": f"Empty modify order number: {msg} (거래시간 외/정정 불가 상태 가능)",
|
|
13102
|
+
"original_order_id": original_order_id,
|
|
13103
|
+
"product": "overseas_stock",
|
|
13104
|
+
},
|
|
13105
|
+
"modified_order_id": "",
|
|
13106
|
+
"modified_order": None,
|
|
13107
|
+
}
|
|
13108
|
+
|
|
13048
13109
|
context.log(
|
|
13049
13110
|
"info",
|
|
13050
13111
|
f"Order modified: {symbol} original={original_order_id} → new={new_order_no}",
|
|
@@ -13058,6 +13119,7 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
|
|
|
13058
13119
|
"new_order_id": new_order_no,
|
|
13059
13120
|
"product": "overseas_stock",
|
|
13060
13121
|
},
|
|
13122
|
+
"modified_order_id": new_order_no,
|
|
13061
13123
|
"modified_order": {
|
|
13062
13124
|
"symbol": symbol,
|
|
13063
13125
|
"exchange": exchange,
|
|
@@ -13137,14 +13199,34 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
|
|
|
13137
13199
|
"original_order_id": original_order_id,
|
|
13138
13200
|
"product": "overseas_futures",
|
|
13139
13201
|
},
|
|
13202
|
+
"modified_order_id": "",
|
|
13140
13203
|
"modified_order": None,
|
|
13141
13204
|
}
|
|
13142
|
-
|
|
13205
|
+
|
|
13143
13206
|
new_order_no = ""
|
|
13144
13207
|
if response.block2:
|
|
13145
13208
|
# 해외선물 정정 주문번호 필드: OvrsFutsOrdNo
|
|
13146
13209
|
new_order_no = str(response.block2.OvrsFutsOrdNo) if hasattr(response.block2, "OvrsFutsOrdNo") and response.block2.OvrsFutsOrdNo else ""
|
|
13147
13210
|
|
|
13211
|
+
# 정정 빈-주문번호 가드 (거래시간 외/정정 불가 상태 silent no-op 차단)
|
|
13212
|
+
if not new_order_no:
|
|
13213
|
+
msg = response.rsp_msg or "정정 미반영"
|
|
13214
|
+
context.log(
|
|
13215
|
+
"warning",
|
|
13216
|
+
f"Modify futures order returned no OrderNo: {symbol} - {msg}",
|
|
13217
|
+
node_id,
|
|
13218
|
+
)
|
|
13219
|
+
return {
|
|
13220
|
+
"modify_result": {
|
|
13221
|
+
"success": False,
|
|
13222
|
+
"error": f"Empty modify order number: {msg} (거래시간 외/정정 불가 상태 가능)",
|
|
13223
|
+
"original_order_id": original_order_id,
|
|
13224
|
+
"product": "overseas_futures",
|
|
13225
|
+
},
|
|
13226
|
+
"modified_order_id": "",
|
|
13227
|
+
"modified_order": None,
|
|
13228
|
+
}
|
|
13229
|
+
|
|
13148
13230
|
context.log(
|
|
13149
13231
|
"info",
|
|
13150
13232
|
f"Futures order modified: {symbol} original={original_order_id} → new={new_order_no}",
|
|
@@ -13158,6 +13240,7 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
|
|
|
13158
13240
|
"new_order_id": new_order_no,
|
|
13159
13241
|
"product": "overseas_futures",
|
|
13160
13242
|
},
|
|
13243
|
+
"modified_order_id": new_order_no,
|
|
13161
13244
|
"modified_order": {
|
|
13162
13245
|
"symbol": symbol,
|
|
13163
13246
|
"exchange": exchange_code,
|
|
@@ -13220,6 +13303,7 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
|
|
|
13220
13303
|
"original_order_id": original_order_id,
|
|
13221
13304
|
"product": "korea_stock",
|
|
13222
13305
|
},
|
|
13306
|
+
"modified_order_id": "",
|
|
13223
13307
|
"modified_order": None,
|
|
13224
13308
|
}
|
|
13225
13309
|
|
|
@@ -13227,6 +13311,25 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
|
|
|
13227
13311
|
if response.block2:
|
|
13228
13312
|
new_order_no = str(response.block2.OrdNo) if response.block2.OrdNo else ""
|
|
13229
13313
|
|
|
13314
|
+
# 정정 빈-주문번호 가드 (거래시간 외/정정 불가 상태 silent no-op 차단)
|
|
13315
|
+
if not new_order_no:
|
|
13316
|
+
msg = response.rsp_msg or "정정 미반영"
|
|
13317
|
+
context.log(
|
|
13318
|
+
"warning",
|
|
13319
|
+
f"Korea stock modify order returned no OrderNo: {symbol} - {msg}",
|
|
13320
|
+
node_id,
|
|
13321
|
+
)
|
|
13322
|
+
return {
|
|
13323
|
+
"modify_result": {
|
|
13324
|
+
"success": False,
|
|
13325
|
+
"error": f"Empty modify order number: {msg} (거래시간 외/정정 불가 상태 가능)",
|
|
13326
|
+
"original_order_id": original_order_id,
|
|
13327
|
+
"product": "korea_stock",
|
|
13328
|
+
},
|
|
13329
|
+
"modified_order_id": "",
|
|
13330
|
+
"modified_order": None,
|
|
13331
|
+
}
|
|
13332
|
+
|
|
13230
13333
|
context.log(
|
|
13231
13334
|
"info",
|
|
13232
13335
|
f"Korea stock order modified: {symbol} original={original_order_id} → new={new_order_no}",
|
|
@@ -13240,6 +13343,7 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
|
|
|
13240
13343
|
"new_order_id": new_order_no,
|
|
13241
13344
|
"product": "korea_stock",
|
|
13242
13345
|
},
|
|
13346
|
+
"modified_order_id": new_order_no,
|
|
13243
13347
|
"modified_order": {
|
|
13244
13348
|
"symbol": symbol,
|
|
13245
13349
|
"exchange": "KRX",
|
|
@@ -13270,6 +13374,7 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
|
|
|
13270
13374
|
"success": False,
|
|
13271
13375
|
"error": error_msg,
|
|
13272
13376
|
},
|
|
13377
|
+
"modified_order_id": "",
|
|
13273
13378
|
"modified_order": None,
|
|
13274
13379
|
}
|
|
13275
13380
|
|
|
@@ -13286,6 +13391,8 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13286
13391
|
- original_order_id: 취소할 주문번호 (필수)
|
|
13287
13392
|
- symbol: 종목코드
|
|
13288
13393
|
- exchange: 거래소
|
|
13394
|
+
|
|
13395
|
+
주의: LS 가 error_msg 없이 취소 미반영(거래시간 외 등)을 반환할 수 있다. 취소 성공은 사후 OpenOrders 재조회로 확인 권장.
|
|
13289
13396
|
"""
|
|
13290
13397
|
|
|
13291
13398
|
# 해외주식 시장 코드 매핑
|
|
@@ -13316,6 +13423,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13316
13423
|
)
|
|
13317
13424
|
return {
|
|
13318
13425
|
"order_id": order_id,
|
|
13426
|
+
"cancelled_order_id": order_id,
|
|
13319
13427
|
"status": "simulated",
|
|
13320
13428
|
"dry_run": True,
|
|
13321
13429
|
"requested": config,
|
|
@@ -13449,9 +13557,10 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13449
13557
|
"order_id": order_id,
|
|
13450
13558
|
"product": "overseas_stock",
|
|
13451
13559
|
},
|
|
13560
|
+
"cancelled_order_id": "",
|
|
13452
13561
|
"cancelled_order": None,
|
|
13453
13562
|
}
|
|
13454
|
-
|
|
13563
|
+
|
|
13455
13564
|
context.log(
|
|
13456
13565
|
"info",
|
|
13457
13566
|
f"Order cancelled: {symbol} order_id={order_id}",
|
|
@@ -13464,6 +13573,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13464
13573
|
"order_id": order_id,
|
|
13465
13574
|
"product": "overseas_stock",
|
|
13466
13575
|
},
|
|
13576
|
+
"cancelled_order_id": order_id,
|
|
13467
13577
|
"cancelled_order": {
|
|
13468
13578
|
"symbol": symbol,
|
|
13469
13579
|
"exchange": exchange,
|
|
@@ -13481,6 +13591,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13481
13591
|
"order_id": order_id,
|
|
13482
13592
|
"product": "overseas_stock",
|
|
13483
13593
|
},
|
|
13594
|
+
"cancelled_order_id": "",
|
|
13484
13595
|
"cancelled_order": None,
|
|
13485
13596
|
}
|
|
13486
13597
|
|
|
@@ -13527,6 +13638,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13527
13638
|
"order_id": order_id,
|
|
13528
13639
|
"product": "overseas_futures",
|
|
13529
13640
|
},
|
|
13641
|
+
"cancelled_order_id": "",
|
|
13530
13642
|
"cancelled_order": None,
|
|
13531
13643
|
}
|
|
13532
13644
|
|
|
@@ -13542,6 +13654,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13542
13654
|
"order_id": order_id,
|
|
13543
13655
|
"product": "overseas_futures",
|
|
13544
13656
|
},
|
|
13657
|
+
"cancelled_order_id": order_id,
|
|
13545
13658
|
"cancelled_order": {
|
|
13546
13659
|
"symbol": symbol,
|
|
13547
13660
|
"exchange": exchange_code,
|
|
@@ -13559,6 +13672,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13559
13672
|
"order_id": order_id,
|
|
13560
13673
|
"product": "overseas_futures",
|
|
13561
13674
|
},
|
|
13675
|
+
"cancelled_order_id": "",
|
|
13562
13676
|
"cancelled_order": None,
|
|
13563
13677
|
}
|
|
13564
13678
|
|
|
@@ -13600,6 +13714,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13600
13714
|
"order_id": order_id,
|
|
13601
13715
|
"product": "korea_stock",
|
|
13602
13716
|
},
|
|
13717
|
+
"cancelled_order_id": "",
|
|
13603
13718
|
"cancelled_order": None,
|
|
13604
13719
|
}
|
|
13605
13720
|
|
|
@@ -13615,6 +13730,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13615
13730
|
"order_id": order_id,
|
|
13616
13731
|
"product": "korea_stock",
|
|
13617
13732
|
},
|
|
13733
|
+
"cancelled_order_id": order_id,
|
|
13618
13734
|
"cancelled_order": {
|
|
13619
13735
|
"symbol": symbol,
|
|
13620
13736
|
"exchange": "KRX",
|
|
@@ -13632,6 +13748,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13632
13748
|
"order_id": order_id,
|
|
13633
13749
|
"product": "korea_stock",
|
|
13634
13750
|
},
|
|
13751
|
+
"cancelled_order_id": "",
|
|
13635
13752
|
"cancelled_order": None,
|
|
13636
13753
|
}
|
|
13637
13754
|
|
|
@@ -13642,6 +13759,7 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
|
|
|
13642
13759
|
"success": False,
|
|
13643
13760
|
"error": error_msg,
|
|
13644
13761
|
},
|
|
13762
|
+
"cancelled_order_id": "",
|
|
13645
13763
|
"cancelled_order": None,
|
|
13646
13764
|
}
|
|
13647
13765
|
|
|
@@ -14874,6 +14992,21 @@ class WorkflowExecutor:
|
|
|
14874
14992
|
self._executors: Dict[str, NodeExecutorBase] = self._init_executors()
|
|
14875
14993
|
# 동적 노드 레지스트리 (지연 임포트로 순환 참조 방지)
|
|
14876
14994
|
self._dynamic_registry = None
|
|
14995
|
+
# Opt-in LS token provider (Verified League §3.2.3). When set, broker
|
|
14996
|
+
# logins fetch the token from this callback (a remote server) instead of
|
|
14997
|
+
# self-issuing via GenerateToken, so the platform server is the single
|
|
14998
|
+
# token issuer and this executor is a pure consumer. login() is sync, so
|
|
14999
|
+
# the provider is a sync callable:
|
|
15000
|
+
# (appkey: str, product: str, paper_trading: bool) -> (access_token, expires_at_epoch)
|
|
15001
|
+
# Left None for standalone/public usage (unchanged self-issue path).
|
|
15002
|
+
self.ls_token_provider = None
|
|
15003
|
+
|
|
15004
|
+
def set_ls_token_provider(self, provider) -> None:
|
|
15005
|
+
"""Configure the opt-in LS token provider (Verified League §3.2.3).
|
|
15006
|
+
|
|
15007
|
+
Pass None to clear and revert to the default self-issue path.
|
|
15008
|
+
"""
|
|
15009
|
+
self.ls_token_provider = provider
|
|
14877
15010
|
|
|
14878
15011
|
def _get_dynamic_registry(self):
|
|
14879
15012
|
"""DynamicNodeRegistry 싱글톤 반환 (지연 로딩)"""
|
|
@@ -15129,8 +15262,9 @@ class WorkflowExecutor:
|
|
|
15129
15262
|
workflow_edges=resolved.edges,
|
|
15130
15263
|
workflow_nodes=resolved.nodes,
|
|
15131
15264
|
storage_dir=storage_dir,
|
|
15265
|
+
ls_token_provider=self.ls_token_provider,
|
|
15132
15266
|
)
|
|
15133
|
-
|
|
15267
|
+
|
|
15134
15268
|
# Set listeners (Option A: inject at creation)
|
|
15135
15269
|
if listeners:
|
|
15136
15270
|
context.set_listeners(listeners)
|
|
@@ -15253,6 +15387,7 @@ class WorkflowExecutor:
|
|
|
15253
15387
|
workflow_edges=resolved.edges,
|
|
15254
15388
|
workflow_nodes=resolved.nodes,
|
|
15255
15389
|
storage_dir=storage_dir,
|
|
15390
|
+
ls_token_provider=self.ls_token_provider,
|
|
15256
15391
|
)
|
|
15257
15392
|
|
|
15258
15393
|
if listeners:
|
|
@@ -16264,6 +16399,23 @@ class WorkflowJob:
|
|
|
16264
16399
|
input_data = None
|
|
16265
16400
|
for edge in self.workflow.edges:
|
|
16266
16401
|
if edge.to_node_id == node_id:
|
|
16402
|
+
# 명시적 from_port 우선 (예: ExclusionListNode.filtered).
|
|
16403
|
+
# 이를 지정하지 않으면 auto-iterate 는 소스 노드의 첫 출력
|
|
16404
|
+
# 포트를 집어버린다 — ExclusionListNode 는 첫 포트가
|
|
16405
|
+
# `excluded`(블랙리스트)라서, from_port 없이는 제외 종목을
|
|
16406
|
+
# 순회하게 되는 silent 버그가 생긴다. IfNode 분기 포트
|
|
16407
|
+
# (true/false/result)는 별도 라우팅 의미라 여기서 제외.
|
|
16408
|
+
explicit_port = getattr(edge, "from_port", None)
|
|
16409
|
+
if explicit_port and explicit_port not in (
|
|
16410
|
+
"output", "true", "false", "result",
|
|
16411
|
+
):
|
|
16412
|
+
port_data = self.context.get_output(
|
|
16413
|
+
edge.from_node_id, explicit_port
|
|
16414
|
+
)
|
|
16415
|
+
if port_data is not None:
|
|
16416
|
+
input_data = port_data
|
|
16417
|
+
break
|
|
16418
|
+
|
|
16267
16419
|
# symbols 포트 우선 확인 (WatchlistNode 출력)
|
|
16268
16420
|
input_data = self.context.get_output(edge.from_node_id, "symbols")
|
|
16269
16421
|
|
|
@@ -5,7 +5,7 @@ authors = [
|
|
|
5
5
|
homepage = "https://programgarden.com"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
name = "programgarden"
|
|
8
|
-
version = "1.22.
|
|
8
|
+
version = "1.22.4"
|
|
9
9
|
description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
|
|
@@ -28,14 +28,9 @@ lxml = "^6.0.2"
|
|
|
28
28
|
pytickersymbols = {version = ">=1.17.5", python = ">=3.12,<4.0"}
|
|
29
29
|
aiosqlite = "^0.20.0"
|
|
30
30
|
litellm = ">=1.40.0"
|
|
31
|
-
programgarden-core =
|
|
32
|
-
programgarden-finance =
|
|
33
|
-
programgarden-community =
|
|
34
|
-
|
|
35
|
-
[[tool.poetry.source]]
|
|
36
|
-
name = "testpypi"
|
|
37
|
-
url = "https://test.pypi.org/simple/"
|
|
38
|
-
priority = "supplemental"
|
|
31
|
+
programgarden-core = "^1.14.3"
|
|
32
|
+
programgarden-finance = "^1.6.10"
|
|
33
|
+
programgarden-community = "^1.13.8"
|
|
39
34
|
|
|
40
35
|
[tool.poetry.group.dev.dependencies]
|
|
41
36
|
pytest = "^8.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden-1.22.2 → programgarden-1.22.4}/programgarden/database/workflow_position_tracker.py
RENAMED
|
File without changes
|
{programgarden-1.22.2 → programgarden-1.22.4}/programgarden/database/workflow_risk_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
|