programgarden 1.40.0__tar.gz → 1.41.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.
Files changed (54) hide show
  1. {programgarden-1.40.0 → programgarden-1.41.0}/PKG-INFO +6 -2
  2. {programgarden-1.40.0 → programgarden-1.41.0}/README.md +4 -0
  3. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/code_worker.py +16 -3
  4. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/executor.py +49 -12
  5. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/semantic_rules.py +3 -1
  6. {programgarden-1.40.0 → programgarden-1.41.0}/pyproject.toml +2 -2
  7. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/__init__.py +0 -0
  8. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/binding_validator.py +0 -0
  9. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/client.py +0 -0
  10. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/context.py +0 -0
  11. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/__init__.py +0 -0
  12. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/adjustment_delivery.py +0 -0
  13. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/broker_evidence.py +0 -0
  14. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/broker_order_totals.py +0 -0
  15. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/broker_snapshot.py +0 -0
  16. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/checkpoint_manager.py +0 -0
  17. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/db_naming.py +0 -0
  18. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/execution_storage.py +0 -0
  19. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/fill_reconciler.py +0 -0
  20. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/order_cancellation.py +0 -0
  21. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/order_recovery.py +0 -0
  22. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/owned_order_cancellation.py +0 -0
  23. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/position_reconciliation.py +0 -0
  24. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/query_builder.py +0 -0
  25. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/workflow_position_tracker.py +0 -0
  26. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/database/workflow_risk_tracker.py +0 -0
  27. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/deep_fixtures.py +0 -0
  28. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/futures_orderable.py +0 -0
  29. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/futures_pnl.py +0 -0
  30. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/futures_read_evidence.py +0 -0
  31. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/managed_order_control.py +0 -0
  32. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/node_runner.py +0 -0
  33. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/order_lifecycle.py +0 -0
  34. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/plugin/__init__.py +0 -0
  35. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/plugin/sandbox.py +0 -0
  36. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/providers/__init__.py +0 -0
  37. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/providers/llm_errors.py +0 -0
  38. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/providers/llm_provider.py +0 -0
  39. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/reconnect_handler.py +0 -0
  40. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/resolver.py +0 -0
  41. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/resource/__init__.py +0 -0
  42. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/resource/context.py +0 -0
  43. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/resource/limiter.py +0 -0
  44. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/resource/monitor.py +0 -0
  45. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/resource/throttle.py +0 -0
  46. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/tools/__init__.py +0 -0
  47. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/tools/credential_tools.py +0 -0
  48. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/tools/definition_tools.py +0 -0
  49. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/tools/event_tools.py +0 -0
  50. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/tools/job_tools.py +0 -0
  51. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/tools/registry_tools.py +0 -0
  52. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/tools/sqlite_tools.py +0 -0
  53. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/validation_recommender.py +0 -0
  54. {programgarden-1.40.0 → programgarden-1.41.0}/programgarden/valuation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden
3
- Version: 1.40.0
3
+ Version: 1.41.0
4
4
  Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -16,7 +16,7 @@ Requires-Dist: croniter (>=6.0.0,<7.0.0)
16
16
  Requires-Dist: litellm (>=1.40.0)
17
17
  Requires-Dist: lxml (>=6.0.2,<7.0.0)
18
18
  Requires-Dist: programgarden-community (>=1.15.3,<2.0.0)
19
- Requires-Dist: programgarden-core (>=1.30.1,<2.0.0)
19
+ Requires-Dist: programgarden-core (>=1.31.0,<2.0.0)
20
20
  Requires-Dist: programgarden-finance (>=1.10.5,<2.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)
@@ -308,3 +308,7 @@ job = await pg.run_async(
308
308
 
309
309
  자세한 변경 사항은 `CHANGELOG.md`를 참고하세요.
310
310
 
311
+ See [Session gates and guarded Split](../../docs/session-gates-and-guarded-split.md)
312
+ for immediate timezone decisions, per-item conditional routing, effective sequential
313
+ execution of guarded/order branches, and finite CodeNode input bindings.
314
+
@@ -278,3 +278,7 @@ job = await pg.run_async(
278
278
  ## 변경 로그
279
279
 
280
280
  자세한 변경 사항은 `CHANGELOG.md`를 참고하세요.
281
+
282
+ See [Session gates and guarded Split](../../docs/session-gates-and-guarded-split.md)
283
+ for immediate timezone decisions, per-item conditional routing, effective sequential
284
+ execution of guarded/order branches, and finite CodeNode input bindings.
@@ -472,12 +472,25 @@ def run_code_node_sandboxed(
472
472
  {"ok": True, "value": <raw return>}
473
473
  {"ok": False, "error_code": ..., "message": ..., "suggestion": ..., "line": ..., "traceback": ...}
474
474
  """
475
+ # Expressions can accidentally return a bound helper (e.g. `.count`
476
+ # instead of `.count()`). Never pickle such objects into the child: their
477
+ # proxy state can recurse during unpickling and crash the worker process.
478
+ # Normalize the input boundary to the same finite JSON domain as outputs.
479
+ try:
480
+ inputs = json.loads(json.dumps(
481
+ {"data": data, "params": params or {}, "ctx_snapshot": ctx_snapshot or {}},
482
+ allow_nan=False,
483
+ ))
484
+ except (TypeError, ValueError, RecursionError):
485
+ return _error_envelope(
486
+ "CODE_NODE_EXEC_ERROR",
487
+ "CodeNode input must contain only finite JSON-safe values.",
488
+ suggestion="Check data/params bindings; call array helpers such as count() instead of passing a method.",
489
+ )
475
490
  task: Dict[str, Any] = {
476
491
  "code": code,
477
492
  "node_id": node_id,
478
- "data": data,
479
- "params": params or {},
480
- "ctx_snapshot": ctx_snapshot or {},
493
+ **inputs,
481
494
  "allowed_imports": list(allowed_imports) if allowed_imports else None,
482
495
  }
483
496
  pool = get_code_worker_pool()
@@ -21837,6 +21837,16 @@ class WorkflowJob:
21837
21837
  )
21838
21838
  continue
21839
21839
 
21840
+ # === IfNode: 비활성 브랜치 스킵 ===
21841
+ if node_id in if_skipped_nodes:
21842
+ _safe_print(f" ⏭ Skipping node: {node_id} (IfNode branch not taken)")
21843
+ await self.context.notify_node_state(
21844
+ node_id=node_id,
21845
+ node_type=node.node_type,
21846
+ state=NodeState.SKIPPED,
21847
+ )
21848
+ continue
21849
+
21840
21850
  # === Item-based execution: Skip branch nodes (handled by SplitNode) ===
21841
21851
  if node_id in branch_nodes and node.node_type not in ("SplitNode", "AggregateNode"):
21842
21852
  _safe_print(f" ⏭ Skipping branch node: {node_id} (handled by SplitNode)")
@@ -21853,16 +21863,6 @@ class WorkflowJob:
21853
21863
  _safe_print(f" ⏭ Skipping AggregateNode: {node_id} (already executed by SplitNode)")
21854
21864
  continue
21855
21865
 
21856
- # === IfNode: 비활성 브랜치 스킵 ===
21857
- if node_id in if_skipped_nodes:
21858
- _safe_print(f" ⏭ Skipping node: {node_id} (IfNode branch not taken)")
21859
- await self.context.notify_node_state(
21860
- node_id=node_id,
21861
- node_type=node.node_type,
21862
- state=NodeState.SKIPPED,
21863
- )
21864
- continue
21865
-
21866
21866
  _safe_print(f" ▶ Executing node: {node_id} ({node.node_type})")
21867
21867
 
21868
21868
  # 🆕 노드 실행 시작 알림
@@ -22265,7 +22265,7 @@ class WorkflowJob:
22265
22265
  "OverseasStockAccountNode", "OverseasFuturesAccountNode", # 계좌 노드
22266
22266
  "OverseasStockRealAccountNode", "OverseasFuturesRealAccountNode",
22267
22267
  "KoreaStockAccountNode", "KoreaStockRealAccountNode",
22268
- "SQLiteNode", "HTTPRequestNode", # 데이터 노드
22268
+ "SQLiteNode", "HTTPRequestNode", "SessionGateNode", # 데이터 노드
22269
22269
  "CodeNode", # custom code: receives the whole array in `data`, loops in-code (one subprocess call)
22270
22270
  "BacktestEngineNode", "BenchmarkCompareNode", # 분석 노드
22271
22271
  }
@@ -22979,6 +22979,24 @@ class WorkflowJob:
22979
22979
  parallel = config.get("parallel", False)
22980
22980
  delay_ms = config.get("delay_ms", 0)
22981
22981
  continue_on_error = config.get("continue_on_error", True)
22982
+ # Branch expressions share ExecutionContext outputs. Until contexts
22983
+ # are isolated per parallel item, an await can replace item A's gate
22984
+ # evidence with item B's data. Serialize guarded/order branches even
22985
+ # when parallel was requested; do not race an authorization decision.
22986
+ guarded_branch = any(
22987
+ self.workflow.nodes[n].node_type == "IfNode"
22988
+ or self.workflow.nodes[n].node_type.endswith(
22989
+ ("NewOrderNode", "ModifyOrderNode", "CancelOrderNode")
22990
+ )
22991
+ for n in branch_order if n in self.workflow.nodes
22992
+ )
22993
+ if parallel and guarded_branch:
22994
+ parallel = False
22995
+ self.context.log(
22996
+ "warning",
22997
+ "SplitNode guarded/order branches execute sequentially to preserve per-item decisions.",
22998
+ split_id,
22999
+ )
22982
23000
 
22983
23001
  # Resolve the array to split — the single source of truth for this
22984
23002
  # branch: per-item item/index/total AND the items/_array output ports
@@ -23181,11 +23199,26 @@ class WorkflowJob:
23181
23199
  # 종목들이 한 노드를 공유하면 대기시간까지 공유돼 한 번에 한 종목만 통과한다.
23182
23200
  branch_scope = f"{split_id}#{index}"
23183
23201
 
23202
+ # Routing decisions belong to this item, never the preceding item.
23203
+ if_skipped_nodes: Set[str] = set()
23204
+ terminal_nodes = {
23205
+ edge.from_node_id for edge in self.workflow.edges
23206
+ if edge.from_node_id in branch_order
23207
+ and self.workflow.nodes.get(edge.to_node_id)
23208
+ and self.workflow.nodes[edge.to_node_id].node_type == "AggregateNode"
23209
+ }
23210
+
23184
23211
  # Execute each branch node
23185
23212
  for node_id in branch_order:
23186
23213
  node = self.workflow.nodes.get(node_id)
23187
23214
  if not node:
23188
23215
  continue
23216
+ if node_id in if_skipped_nodes:
23217
+ # Do not expose a previous item's gated output through a later
23218
+ # expression or collect it as this item's result.
23219
+ self.context._outputs.pop(node_id, None)
23220
+ self.context._outputs.pop(f"_input_{node_id}", None)
23221
+ continue
23189
23222
 
23190
23223
  is_realtime = node.node_type in REALTIME_NODE_TYPES
23191
23224
  subscribed: List[str] = []
@@ -23239,6 +23272,10 @@ class WorkflowJob:
23239
23272
  order_iteration_index=index,
23240
23273
  )
23241
23274
 
23275
+ if node.node_type == "IfNode" and outputs:
23276
+ taken = outputs.pop("_if_branch", "true")
23277
+ if_skipped_nodes.update(self._compute_if_skip_nodes(node_id, taken))
23278
+
23242
23279
  # Store outputs
23243
23280
  for port_name, value in outputs.items():
23244
23281
  self.context.set_output(node_id, port_name, value)
@@ -23310,7 +23347,7 @@ class WorkflowJob:
23310
23347
  # 을 throttling 중이라 내부 메타만 반환) 이 아이템은 기여할 실데이터가 없다 →
23311
23348
  # skip 하여 "실시간 체결가" 표에 가격 없는/이질적인 행이 끼지 않게 한다. branch
23312
23349
  # 노드가 아예 없으면(bare Split→Aggregate) item 을 그대로 수집한다(정상 패턴).
23313
- if branch_order and last_had_public is False:
23350
+ if branch_order and (last_had_public is False or (terminal_nodes and terminal_nodes <= if_skipped_nodes)):
23314
23351
  return _SKIP_BRANCH_ITEM
23315
23352
 
23316
23353
  return result
@@ -198,8 +198,10 @@ def _is_aiagent_node(node_type: Any) -> bool:
198
198
  # workflow at the strict save gate.
199
199
  _RESERVED_NODE_KEYS: frozenset = frozenset(
200
200
  {"id", "type", "category", "position", "config", "description", "plugin",
201
- "fields", "credential_id"}
201
+ "fields", "credential_id", "customLabel", "size"}
202
202
  )
203
+ # The editor persists these two presentation fields on nodes. They are not
204
+ # execution settings; keep them intact while rejecting unknown configuration.
203
205
 
204
206
 
205
207
  def _node_allowed_keys(node_type: Any) -> Optional[frozenset]:
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden"
8
- version = "1.40.0"
8
+ version = "1.41.0"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
11
11
  readme = "README.md"
@@ -36,7 +36,7 @@ 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 = "^1.30.1"
39
+ programgarden-core = "^1.31.0"
40
40
  programgarden-finance = "^1.10.5"
41
41
  programgarden-community = "^1.15.3"
42
42