programgarden 2.4.0__tar.gz → 2.5.0__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.4.0 → programgarden-2.5.0}/PKG-INFO +3 -3
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/executor.py +145 -5
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_semantics.py +30 -5
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/resolver.py +13 -2
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/validation_replay.py +1 -1
- {programgarden-2.4.0 → programgarden-2.5.0}/pyproject.toml +3 -3
- {programgarden-2.4.0 → programgarden-2.5.0}/README.md +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/__init__.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/binding_validator.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/client.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/code_worker.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/context.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/__init__.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/adjustment_delivery.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/broker_evidence.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/broker_order_totals.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/broker_snapshot.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/db_naming.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/execution_storage.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/fill_reconciler.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/order_cancellation.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/order_recovery.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/owned_order_cancellation.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/position_reconciliation.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/query_builder.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/deep_fixtures.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/futures_orderable.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/futures_pnl.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/futures_read_evidence.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/incremental_build.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/managed_order_control.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/node_runner.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/order_lifecycle.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/plugin/__init__.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/providers/__init__.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/reconnect_handler.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_contracts.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_events.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_external.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_order_adapter.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_orders.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_scenarios.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_sources.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_sqlite.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/replay_triggers.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/resource/__init__.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/resource/context.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/resource/limiter.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/resource/monitor.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/resource/throttle.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/semantic_rules.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/tools/__init__.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/tools/event_tools.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/tools/job_tools.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/validation_recommender.py +0 -0
- {programgarden-2.4.0 → programgarden-2.5.0}/programgarden/valuation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programgarden
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Author: 프로그램동산
|
|
@@ -15,8 +15,8 @@ Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
|
|
|
15
15
|
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
|
-
Requires-Dist: programgarden-community (>=2.
|
|
19
|
-
Requires-Dist: programgarden-core (>=2.
|
|
18
|
+
Requires-Dist: programgarden-community (>=2.3.0,<3.0.0)
|
|
19
|
+
Requires-Dist: programgarden-core (>=2.5.0,<3.0.0)
|
|
20
20
|
Requires-Dist: programgarden-finance (>=2.0.1,<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)
|
|
@@ -4125,6 +4125,41 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
4125
4125
|
# 시간 분포가 아니다 — LS 측 복구 시간은 우리가 관측한 적 없다.)
|
|
4126
4126
|
_FILL_SUBSCRIPTION_LOGIN_RETRY_DELAYS = (2.0, 5.0)
|
|
4127
4127
|
|
|
4128
|
+
@staticmethod
|
|
4129
|
+
def _on_workflow_loop(context: ExecutionContext, callback):
|
|
4130
|
+
"""Route broker thread callbacks to the loop that owns this workflow.
|
|
4131
|
+
|
|
4132
|
+
The SDK dispatches synchronous websocket listeners in a thread pool.
|
|
4133
|
+
Create notification coroutines only after reaching the owning loop, so
|
|
4134
|
+
shutdown cannot leave an unawaited coroutine or mutate task registries
|
|
4135
|
+
from a broker thread.
|
|
4136
|
+
"""
|
|
4137
|
+
loop = asyncio.get_running_loop()
|
|
4138
|
+
|
|
4139
|
+
def deliver(value):
|
|
4140
|
+
if not context.is_shutdown:
|
|
4141
|
+
callback(value)
|
|
4142
|
+
|
|
4143
|
+
def dispatch(value):
|
|
4144
|
+
if context.is_shutdown or loop.is_closed():
|
|
4145
|
+
return
|
|
4146
|
+
try:
|
|
4147
|
+
current = asyncio.get_running_loop()
|
|
4148
|
+
except RuntimeError:
|
|
4149
|
+
current = None
|
|
4150
|
+
if current is loop:
|
|
4151
|
+
deliver(value)
|
|
4152
|
+
else:
|
|
4153
|
+
try:
|
|
4154
|
+
loop.call_soon_threadsafe(deliver, value)
|
|
4155
|
+
except RuntimeError:
|
|
4156
|
+
# The owning loop may close between the check and enqueue.
|
|
4157
|
+
# No coroutine has been created on this path.
|
|
4158
|
+
if not loop.is_closed():
|
|
4159
|
+
raise
|
|
4160
|
+
|
|
4161
|
+
return dispatch
|
|
4162
|
+
|
|
4128
4163
|
def _start_background_task(
|
|
4129
4164
|
self, context: ExecutionContext, coroutine, *, notification: bool = False,
|
|
4130
4165
|
) -> Optional[asyncio.Task]:
|
|
@@ -5052,6 +5087,8 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
5052
5087
|
order_date=order_date,
|
|
5053
5088
|
symbol=symbol,
|
|
5054
5089
|
exchange='FUTURES',
|
|
5090
|
+
# TC3 supplies its unit; keep absent/blank evidence unknown.
|
|
5091
|
+
currency=getattr(body, 'crncy_cd', None),
|
|
5055
5092
|
side=side,
|
|
5056
5093
|
quantity=quantity,
|
|
5057
5094
|
price=price,
|
|
@@ -5577,6 +5614,7 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
5577
5614
|
notification=True,
|
|
5578
5615
|
)
|
|
5579
5616
|
|
|
5617
|
+
on_pnl_change = self._on_workflow_loop(context, on_pnl_change)
|
|
5580
5618
|
tracker.on_account_pnl_change(on_pnl_change)
|
|
5581
5619
|
await tracker.start()
|
|
5582
5620
|
|
|
@@ -5655,6 +5693,7 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
5655
5693
|
notification=True,
|
|
5656
5694
|
)
|
|
5657
5695
|
|
|
5696
|
+
on_pnl_change = self._on_workflow_loop(context, on_pnl_change)
|
|
5658
5697
|
tracker.on_account_pnl_change(on_pnl_change)
|
|
5659
5698
|
tracker.on_balance_change(lambda _: on_pnl_change(tracker.get_account_pnl()))
|
|
5660
5699
|
await tracker.start()
|
|
@@ -5703,6 +5742,7 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
5703
5742
|
notification=True,
|
|
5704
5743
|
)
|
|
5705
5744
|
|
|
5745
|
+
on_pnl_change = self._on_workflow_loop(context, on_pnl_change)
|
|
5706
5746
|
tracker.on_account_pnl_change(on_pnl_change)
|
|
5707
5747
|
await tracker.start()
|
|
5708
5748
|
|
|
@@ -21983,7 +22023,11 @@ class WorkflowJob:
|
|
|
21983
22023
|
"OverseasStockAccountNode", "OverseasFuturesAccountNode", # 계좌 노드
|
|
21984
22024
|
"OverseasStockRealAccountNode", "OverseasFuturesRealAccountNode",
|
|
21985
22025
|
"KoreaStockAccountNode", "KoreaStockRealAccountNode",
|
|
21986
|
-
|
|
22026
|
+
# HTTPRequestNode 는 여기서 뺐다(오너 결정 2026-09-28) — config 가
|
|
22027
|
+
# {{ item… }}/{{ index }}/{{ total }} 을 참조하면 상류 목록의 종목마다 1회씩
|
|
22028
|
+
# 반복하고, 참조가 없으면(조건→webhook POST 예제 등) 1회만 실행한다. 분기는
|
|
22029
|
+
# `_should_auto_iterate` 의 HTTPRequestNode 전용 가드가 담당한다.
|
|
22030
|
+
"SQLiteNode", "SessionGateNode", # 데이터 노드
|
|
21987
22031
|
"CodeNode", # custom code: receives the whole array in `data`, loops in-code (one subprocess call)
|
|
21988
22032
|
"BacktestEngineNode", "BenchmarkCompareNode", # 분석 노드
|
|
21989
22033
|
}
|
|
@@ -22306,6 +22350,15 @@ class WorkflowJob:
|
|
|
22306
22350
|
if config and self._consumes_whole_array(node_type, config) is True:
|
|
22307
22351
|
return False, "", []
|
|
22308
22352
|
|
|
22353
|
+
# HTTPRequestNode: 상류에 배열이 붙어도 config 가 {{ item… }}/{{ index }}/{{ total }}
|
|
22354
|
+
# 을 참조할 때만 종목별로 반복한다(오너 결정 2026-09-28). 참조가 없으면 오늘처럼
|
|
22355
|
+
# 1회만 실행 — 하위 호환(조건 → webhook POST 알림 예제는 N 번 발화하면 안 된다).
|
|
22356
|
+
# 이 시점의 config 는 최상위 {{ item… }} 키가 미평가로 남아 있어(iteration_item 이
|
|
22357
|
+
# 아직 없어 `_resolve_config_expressions` 가 deferred 처리) 판정에 쓸 수 있다.
|
|
22358
|
+
# config 가 None(레거시 호출)이면 `_references_iteration_item` 이 False → 반복 안 함.
|
|
22359
|
+
if node_type == "HTTPRequestNode" and not self._references_iteration_item(config):
|
|
22360
|
+
return False, "", []
|
|
22361
|
+
|
|
22309
22362
|
# 배열 입력이면 자동 반복 실행
|
|
22310
22363
|
return True, "item", input_data
|
|
22311
22364
|
|
|
@@ -22379,7 +22432,8 @@ class WorkflowJob:
|
|
|
22379
22432
|
# _rate_limit ClassVar가 있는 노드(주문, HTTP 등)에 한해 min_interval_sec
|
|
22380
22433
|
# 만큼 간격을 보장한다. skip이 아니라 sleep → 모든 N 아이템이 실행됨.
|
|
22381
22434
|
# rate-limit이 없는 순수 데이터/계산 노드는 영향 없음 (하위 호환).
|
|
22382
|
-
|
|
22435
|
+
# config 를 넘겨 사용자 rate_limit_interval(무료 API 분당 제한 등)을 반영.
|
|
22436
|
+
await self._auto_iterate_pacing_sleep(node_id, node.node_type, config)
|
|
22383
22437
|
|
|
22384
22438
|
try:
|
|
22385
22439
|
outputs = await self.executor.execute_node(
|
|
@@ -22417,6 +22471,12 @@ class WorkflowJob:
|
|
|
22417
22471
|
|
|
22418
22472
|
# 결과 병합: 배열 필드는 병합, 단일 필드는 마지막 값
|
|
22419
22473
|
merged = self._merge_iterate_results(all_results)
|
|
22474
|
+
# HTTPRequestNode 전용 후처리 — 일반 병합은 키를 `results[0]` 에서만 뽑아
|
|
22475
|
+
# 첫 항목이 오류(`{"error":…, "item":…}`)면 나머지 항목의 response/status_code/
|
|
22476
|
+
# success 가 통째로 유실된다. HTTP 는 항목 전체를 훑어 `results` 배열을 만들고
|
|
22477
|
+
# 최상위 error/success/키를 항목 기준으로 재계산한다.
|
|
22478
|
+
if node.node_type == "HTTPRequestNode":
|
|
22479
|
+
merged = self._merge_http_iterate_results(all_results, items)
|
|
22420
22480
|
_safe_print(f" ✅ Auto-iterate complete: {len(all_results)} results merged")
|
|
22421
22481
|
|
|
22422
22482
|
return merged
|
|
@@ -22552,6 +22612,70 @@ class WorkflowJob:
|
|
|
22552
22612
|
|
|
22553
22613
|
return merged
|
|
22554
22614
|
|
|
22615
|
+
def _merge_http_iterate_results(self, all_results: list, items: list) -> Dict[str, Any]:
|
|
22616
|
+
"""HTTPRequestNode auto-iterate 결과 전용 병합.
|
|
22617
|
+
|
|
22618
|
+
일반 병합(`_merge_iterate_results`)은 array_fields 가 아닌 포트를 마지막 값으로
|
|
22619
|
+
접고 병합 키를 `results[0].keys()` 에서만 뽑는다 — HTTP 는 첫 항목이 오류
|
|
22620
|
+
(`{"error":…, "item":…}`)면 나머지 항목의 response/status_code/success 포트가
|
|
22621
|
+
통째로 사라진다. 여기서는 항목 전체를 순회해:
|
|
22622
|
+
- `results`: 항목당 {item, response, status_code, success, error} (순서 보존).
|
|
22623
|
+
- `response`/`status_code`: **마지막 항목** 값(기존 {{ nodes.x.response }} 호환,
|
|
22624
|
+
첫 포트 유지).
|
|
22625
|
+
- `success`: 전 항목 AND.
|
|
22626
|
+
- `error`: **전 항목 실패**일 때만 대표 사유(재생은 bool(output["error"]) 로
|
|
22627
|
+
실패를 판정하므로, 한 건이라도 성공하면 "" 로 비워 실패로 안 읽히게 한다).
|
|
22628
|
+
- 최상위 키: 항목 전체의 합집합(첫 항목 오류로 나머지 포트 유실 방지).
|
|
22629
|
+
|
|
22630
|
+
per-item 결과 dict 모양(`programgarden_core.nodes.data.HTTPRequestNode.execute`):
|
|
22631
|
+
성공 2xx/3xx → {"response","status_code","success":True,"error":None}
|
|
22632
|
+
4xx → {"response","status_code","success":False,"error":"HTTP 4xx"}
|
|
22633
|
+
예외(5xx/네트워크/타임아웃, 재시도 소진) → 반복 루프가 {"error":str(e),"item":…}.
|
|
22634
|
+
"""
|
|
22635
|
+
entries: list = []
|
|
22636
|
+
for idx, r in enumerate(all_results):
|
|
22637
|
+
rec = r if isinstance(r, dict) else {}
|
|
22638
|
+
item = items[idx] if idx < len(items) else rec.get("item")
|
|
22639
|
+
err = rec.get("error")
|
|
22640
|
+
success = rec.get("success")
|
|
22641
|
+
if success is None:
|
|
22642
|
+
# 명시 success 가 없으면 error 유무로 판정(예외 항목은 error 만 있다).
|
|
22643
|
+
success = not err
|
|
22644
|
+
entries.append({
|
|
22645
|
+
"item": item,
|
|
22646
|
+
"response": rec.get("response"),
|
|
22647
|
+
"status_code": rec.get("status_code"),
|
|
22648
|
+
"success": bool(success),
|
|
22649
|
+
"error": err,
|
|
22650
|
+
})
|
|
22651
|
+
|
|
22652
|
+
out: Dict[str, Any] = {}
|
|
22653
|
+
# 최상위 키 = 항목 전체 합집합(마지막 non-None 우선). `item` 은 예외 항목이 끼워
|
|
22654
|
+
# 넣는 반복 문맥일 뿐 HTTP 출력 포트가 아니므로 최상위에서 제외한다.
|
|
22655
|
+
union_keys: Set[str] = set()
|
|
22656
|
+
for r in all_results:
|
|
22657
|
+
if isinstance(r, dict):
|
|
22658
|
+
union_keys.update(r.keys())
|
|
22659
|
+
union_keys.discard("item")
|
|
22660
|
+
for key in union_keys:
|
|
22661
|
+
for r in reversed(all_results):
|
|
22662
|
+
if isinstance(r, dict) and r.get(key) is not None:
|
|
22663
|
+
out[key] = r[key]
|
|
22664
|
+
break
|
|
22665
|
+
|
|
22666
|
+
out["results"] = entries
|
|
22667
|
+
out["response"] = entries[-1]["response"] if entries else None
|
|
22668
|
+
out["status_code"] = entries[-1]["status_code"] if entries else None
|
|
22669
|
+
out["success"] = all(e["success"] for e in entries) if entries else False
|
|
22670
|
+
all_failed = bool(entries) and all(not e["success"] for e in entries)
|
|
22671
|
+
if all_failed:
|
|
22672
|
+
out["error"] = next((e["error"] for e in entries if e["error"]),
|
|
22673
|
+
"All HTTP requests failed")
|
|
22674
|
+
else:
|
|
22675
|
+
# 한 건이라도 성공 → 최상위 error 는 비운다(재생/하류에서 실패로 안 읽히게).
|
|
22676
|
+
out["error"] = ""
|
|
22677
|
+
return out
|
|
22678
|
+
|
|
22555
22679
|
def _dependent_descendants(self, node_id: str) -> Set[str]:
|
|
22556
22680
|
"""A waiting data dependency blocks its chain, including dependent joins.
|
|
22557
22681
|
|
|
@@ -23454,11 +23578,16 @@ class WorkflowJob:
|
|
|
23454
23578
|
# A-3: auto-iterate per-item pacing (spacing, NOT skipping)
|
|
23455
23579
|
# ============================================================
|
|
23456
23580
|
|
|
23457
|
-
async def _auto_iterate_pacing_sleep(
|
|
23581
|
+
async def _auto_iterate_pacing_sleep(
|
|
23582
|
+
self, node_id: str, node_type: str, config: Optional[Dict[str, Any]] = None,
|
|
23583
|
+
) -> None:
|
|
23458
23584
|
"""auto-iterate 루프에서 per-item 간격을 보장하는 sleep.
|
|
23459
23585
|
|
|
23460
|
-
|
|
23461
|
-
|
|
23586
|
+
간격은 사용자 config 의 `rate_limit_interval`(없으면 `cooldown_sec`)을 우선하고,
|
|
23587
|
+
미지정이면 노드의 `_rate_limit.min_interval_sec`(HTTP=1초 기본) 을 쓴다 —
|
|
23588
|
+
`_apply_rate_limit_guard` 의 우선순위와 동일. 이로써 무료 API 의 분당 제한을
|
|
23589
|
+
사용자가 종목별 간격으로 조절할 수 있다(예: FMP 무료 티어).
|
|
23590
|
+
직전 아이템 실행으로부터 충분한 시간이 지나지 않았으면 잔여 시간만큼 sleep 후 실행.
|
|
23462
23591
|
모든 N 아이템이 반드시 실행된다 (skip 없음).
|
|
23463
23592
|
|
|
23464
23593
|
rate-limit이 없는 노드(ConditionNode 등)는 즉시 통과 — 하위 호환.
|
|
@@ -23497,7 +23626,18 @@ class WorkflowJob:
|
|
|
23497
23626
|
if not class_rate_limit:
|
|
23498
23627
|
return
|
|
23499
23628
|
|
|
23629
|
+
# 사용자 config 오버라이드 (rate_limit_interval 명시이면 우선, 없으면 cooldown_sec)
|
|
23630
|
+
# — `_apply_rate_limit_guard` 와 같은 규칙. 미지정이면 클래스 기본값 유지(HTTP=1초).
|
|
23500
23631
|
min_interval_sec = class_rate_limit.min_interval_sec
|
|
23632
|
+
if config:
|
|
23633
|
+
user_interval = config.get("rate_limit_interval")
|
|
23634
|
+
if user_interval is None:
|
|
23635
|
+
user_interval = config.get("cooldown_sec")
|
|
23636
|
+
if user_interval is not None:
|
|
23637
|
+
try:
|
|
23638
|
+
min_interval_sec = float(user_interval)
|
|
23639
|
+
except (TypeError, ValueError):
|
|
23640
|
+
pass
|
|
23501
23641
|
if min_interval_sec <= 0:
|
|
23502
23642
|
return
|
|
23503
23643
|
|
|
@@ -278,6 +278,24 @@ def _iterates(node_type: str, role: str, c, own_fields: List[str]) -> Optional[D
|
|
|
278
278
|
return None
|
|
279
279
|
if node_type in c["no_auto_iterate"] or role == "broker":
|
|
280
280
|
return None
|
|
281
|
+
if node_type == "HTTPRequestNode":
|
|
282
|
+
# HTTPRequestNode fans out per item ONLY when its config references an
|
|
283
|
+
# iteration binding ({{ item... }} / {{ index }} / {{ total }}); a config
|
|
284
|
+
# without such a reference runs ONCE (executor _should_auto_iterate,
|
|
285
|
+
# owner decision 2026-09-28 -- the condition->webhook POST example must
|
|
286
|
+
# not fire N times). It has no fixed `symbols` field, so `item` follows
|
|
287
|
+
# the upstream element shape and the per-item outcomes are exposed on the
|
|
288
|
+
# `results` output port; the recording key is EXCHANGE:SYMBOL for
|
|
289
|
+
# {symbol, exchange} items and a string/other item falls back to the
|
|
290
|
+
# index key (replay_external.py).
|
|
291
|
+
return {
|
|
292
|
+
"mode": "per_item",
|
|
293
|
+
"item": "upstream_element",
|
|
294
|
+
"when": "config_references_item_binding",
|
|
295
|
+
"record_key": "EXCHANGE:SYMBOL",
|
|
296
|
+
"single": "no_item_binding",
|
|
297
|
+
"results_port": "results",
|
|
298
|
+
}
|
|
281
299
|
if "symbols" not in own_fields:
|
|
282
300
|
return None
|
|
283
301
|
block: Dict[str, Any] = {
|
|
@@ -523,11 +541,18 @@ def execution_semantics_text(node_type: str) -> List[str]:
|
|
|
523
541
|
|
|
524
542
|
it = block.get("iteration")
|
|
525
543
|
if isinstance(it, dict):
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
544
|
+
if it.get("when") == "config_references_item_binding":
|
|
545
|
+
line = ("Auto-iterates per item over the upstream list ONLY when the config "
|
|
546
|
+
"references an item binding ({{ item }} / {{ index }} / {{ total }}); "
|
|
547
|
+
"otherwise it runs once. Each per-item recording is keyed EXCHANGE:SYMBOL "
|
|
548
|
+
"(a string or other item falls back to the index key), and per-item "
|
|
549
|
+
"outcomes are exposed on the 'results' output port.")
|
|
550
|
+
else:
|
|
551
|
+
line = ("Auto-iterates per item over an upstream symbol array; each per-item "
|
|
552
|
+
"recording is keyed EXCHANGE:SYMBOL and a single non-iterated "
|
|
553
|
+
"recording sets item=null.")
|
|
554
|
+
if it.get("default_exchange"):
|
|
555
|
+
line = line[:-1] + " (exchange defaults to KRX when omitted)."
|
|
531
556
|
lines.append(line)
|
|
532
557
|
|
|
533
558
|
emits = block.get("emits_events") or []
|
|
@@ -8,6 +8,14 @@ deterministic self-correction without parsing free-form strings.
|
|
|
8
8
|
|
|
9
9
|
from typing import Optional, List, Dict, Any, Set, Tuple
|
|
10
10
|
|
|
11
|
+
# product_scope 값 → 그 스코프의 브로커 노드 타입. MISSING_REQUIRED_BROKER 안내에 쓴다.
|
|
12
|
+
# (ProductScope.ALL 은 브로커가 필요 없으므로 없다.)
|
|
13
|
+
_BROKER_NODE_BY_SCOPE: Dict[str, str] = {
|
|
14
|
+
"overseas_stock": "OverseasStockBrokerNode",
|
|
15
|
+
"overseas_futures": "OverseasFuturesBrokerNode",
|
|
16
|
+
"korea_stock": "KoreaStockBrokerNode",
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
from programgarden_core import (
|
|
12
20
|
ErrorCode,
|
|
13
21
|
ErrorInfo,
|
|
@@ -1663,8 +1671,11 @@ class WorkflowResolver:
|
|
|
1663
1671
|
|
|
1664
1672
|
# product_scope match
|
|
1665
1673
|
if scope.value not in available_brokers:
|
|
1666
|
-
|
|
1667
|
-
|
|
1674
|
+
# 스코프별 브로커 노드 매핑. 2.4.0 까지는 STOCK 이 아닌 모든 스코프를 해외선물로
|
|
1675
|
+
# 표기해 국내주식 노드에 "OverseasFuturesBrokerNode 를 추가하라" 고 안내했다
|
|
1676
|
+
# (AI 저작 루프가 그 말을 믿고 헤맴 — 2026-09-27 dev smoke).
|
|
1677
|
+
product_label = scope.value
|
|
1678
|
+
broker_node = _BROKER_NODE_BY_SCOPE.get(scope, "OverseasFuturesBrokerNode")
|
|
1668
1679
|
result.add(
|
|
1669
1680
|
build_error(
|
|
1670
1681
|
ErrorCode.MISSING_REQUIRED_BROKER,
|
|
@@ -163,7 +163,7 @@ class ReplayJob(WorkflowJob):
|
|
|
163
163
|
# Validation never reads or writes a running strategy's execution DB.
|
|
164
164
|
return None
|
|
165
165
|
|
|
166
|
-
async def _auto_iterate_pacing_sleep(self, node_id, node_type):
|
|
166
|
+
async def _auto_iterate_pacing_sleep(self, node_id, node_type, config=None):
|
|
167
167
|
return None
|
|
168
168
|
|
|
169
169
|
def _rate_limit_now(self):
|
|
@@ -5,7 +5,7 @@ authors = [
|
|
|
5
5
|
homepage = "https://programgarden.com"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
name = "programgarden"
|
|
8
|
-
version = "2.
|
|
8
|
+
version = "2.5.0"
|
|
9
9
|
license = "AGPL-3.0-or-later"
|
|
10
10
|
description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
|
|
11
11
|
readme = "README.md"
|
|
@@ -36,9 +36,9 @@ 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.
|
|
39
|
+
programgarden-core = "^2.5.0"
|
|
40
40
|
programgarden-finance = "^2.0.1"
|
|
41
|
-
programgarden-community = "^2.
|
|
41
|
+
programgarden-community = "^2.3.0"
|
|
42
42
|
|
|
43
43
|
[tool.poetry.group.dev.dependencies]
|
|
44
44
|
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
|
|
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.4.0 → programgarden-2.5.0}/programgarden/database/owned_order_cancellation.py
RENAMED
|
File without changes
|
{programgarden-2.4.0 → programgarden-2.5.0}/programgarden/database/position_reconciliation.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden-2.4.0 → programgarden-2.5.0}/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
|