programgarden 1.29.1__tar.gz → 1.29.2__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 (37) hide show
  1. {programgarden-1.29.1 → programgarden-1.29.2}/PKG-INFO +1 -1
  2. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/executor.py +75 -21
  3. {programgarden-1.29.1 → programgarden-1.29.2}/pyproject.toml +1 -1
  4. {programgarden-1.29.1 → programgarden-1.29.2}/README.md +0 -0
  5. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/__init__.py +0 -0
  6. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/binding_validator.py +0 -0
  7. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/client.py +0 -0
  8. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/code_worker.py +0 -0
  9. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/context.py +0 -0
  10. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/__init__.py +0 -0
  11. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/checkpoint_manager.py +0 -0
  12. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/query_builder.py +0 -0
  13. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/workflow_position_tracker.py +0 -0
  14. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/workflow_risk_tracker.py +0 -0
  15. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/deep_fixtures.py +0 -0
  16. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/node_runner.py +0 -0
  17. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/plugin/__init__.py +0 -0
  18. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/plugin/sandbox.py +0 -0
  19. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/providers/__init__.py +0 -0
  20. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/providers/llm_errors.py +0 -0
  21. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/providers/llm_provider.py +0 -0
  22. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/reconnect_handler.py +0 -0
  23. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resolver.py +0 -0
  24. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/__init__.py +0 -0
  25. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/context.py +0 -0
  26. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/limiter.py +0 -0
  27. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/monitor.py +0 -0
  28. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/throttle.py +0 -0
  29. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/semantic_rules.py +0 -0
  30. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/__init__.py +0 -0
  31. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/credential_tools.py +0 -0
  32. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/definition_tools.py +0 -0
  33. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/event_tools.py +0 -0
  34. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/job_tools.py +0 -0
  35. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/registry_tools.py +0 -0
  36. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/sqlite_tools.py +0 -0
  37. {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/validation_recommender.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden
3
- Version: 1.29.1
3
+ Version: 1.29.2
4
4
  Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -1040,6 +1040,10 @@ _SYMBOL_ITEM_KEYS = ("symbol", "shcode", "code")
1040
1040
  # 가격 대신 종목코드 목록이 표에 실렸다(비결정 + 잘못된 데이터).
1041
1041
  _BRANCH_PAYLOAD_PORTS = ("result", "value", "data", "ohlcv_data")
1042
1042
 
1043
+ # 실시간 노드가 어떤 분기 스코프(종목)까지 구독을 걸었는지 기록하는 node_state 키.
1044
+ # 종목마다 최초 1회만 실행하고, 틱마다의 분기 재구동에선 건너뛴다(재구독 폭주 방지).
1045
+ _SUBSCRIBED_SCOPES_KEY = "_subscribed_branch_scopes"
1046
+
1043
1047
 
1044
1048
  def _item_symbol(item: Any) -> Optional[str]:
1045
1049
  """Split 아이템에서 종목코드를 뽑는다 ('005930' 또는 {'symbol': '005930'})."""
@@ -1053,21 +1057,50 @@ def _item_symbol(item: Any) -> Optional[str]:
1053
1057
  return None
1054
1058
 
1055
1059
 
1060
+ def _is_symbol_keyed_payload(value: Any) -> bool:
1061
+ """`{"005930": [bar], "000660": [bar]}` 처럼 종목코드로 키가 잡힌 시세 payload 인가."""
1062
+ return (
1063
+ isinstance(value, dict)
1064
+ and bool(value)
1065
+ and all(isinstance(v, list) for v in value.values())
1066
+ )
1067
+
1068
+
1056
1069
  def _slice_realtime_outputs(outputs: Dict[str, Any], symbol: str) -> Dict[str, Any]:
1057
- """실시간 시세 노드의 합쳐진 출력을 한 종목 몫으로 자른다."""
1058
- sliced: Dict[str, Any] = {}
1070
+ """실시간 시세 노드의 합쳐진 출력을 한 종목 몫으로 자른다.
1071
+
1072
+ **이 종목의 틱이 아직 없으면 공개 포트를 하나도 내지 않는다**(내부 `_` 포트만 남긴다).
1073
+ 빈 값이나 `symbols` 목록만 내보내면 Throttle 이 "흘릴 데이터가 있다"고 착각해 통과시키고,
1074
+ 표에 **가격 없는 행**이 낀다. 실제로 HKEX 라이브에서 아직 체결이 없던 2번째 종목이
1075
+ 합쳐진 덩어리를 그대로 행으로 실어 이 결함이 드러났다(2026-07-14).
1076
+ """
1077
+ meta = {
1078
+ k: v for k, v in outputs.items()
1079
+ if isinstance(k, str) and k.startswith("_")
1080
+ }
1081
+ payload: Dict[str, Any] = {}
1082
+ has_payload = False
1083
+
1059
1084
  for port, value in outputs.items():
1085
+ if isinstance(port, str) and port.startswith("_"):
1086
+ continue
1087
+ if _is_symbol_keyed_payload(value):
1088
+ if symbol in value:
1089
+ payload[port] = value[symbol]
1090
+ has_payload = True
1091
+ # 이 종목 몫이 없으면 그 포트는 **아예 내지 않는다** (덩어리 통과 금지)
1092
+ continue
1060
1093
  if port == "symbol":
1061
- sliced[port] = symbol
1062
- elif isinstance(value, dict) and symbol in value:
1063
- # symbol-keyed dict (ohlcv_data / data) → 이 종목의 값만
1064
- sliced[port] = value[symbol]
1065
- elif port == "symbols" and isinstance(value, list):
1066
- kept = [s for s in value if _item_symbol(s) == symbol]
1067
- sliced[port] = kept or value
1068
- else:
1069
- sliced[port] = value
1070
- return sliced
1094
+ payload[port] = symbol
1095
+ continue
1096
+ if port == "symbols" and isinstance(value, list):
1097
+ payload[port] = [s for s in value if _item_symbol(s) == symbol]
1098
+ continue
1099
+ payload[port] = value
1100
+
1101
+ if not has_payload:
1102
+ return meta
1103
+ return {**meta, **payload}
1071
1104
 
1072
1105
 
1073
1106
  def _realtime_branch_row(public: Dict[str, Any], symbol: Optional[str]) -> Optional[Dict[str, Any]]:
@@ -18857,12 +18890,22 @@ class WorkflowJob:
18857
18890
  if not node:
18858
18891
  continue
18859
18892
 
18860
- # 이미 구독을 건 실시간 노드는 분기 재구동 때 다시 실행하지 않는다.
18861
- # 재실행하면 틱마다 재구독이 걸려 소켓이 폭주한다. 최신 시세는 틱 콜백이
18862
- # 이 노드의 context 출력에 계속 갱신해 두므로, 하류는 그걸 그대로 읽으면 된다.
18863
- # (최초 실행 때는 출력이 없으므로 정상적으로 실행돼 구독이 걸린다.)
18864
- if node.node_type in REALTIME_NODE_TYPES and self.context.get_all_outputs(node_id):
18865
- continue
18893
+ is_realtime = node.node_type in REALTIME_NODE_TYPES
18894
+ subscribed: List[str] = []
18895
+ if is_realtime:
18896
+ # 실시간 노드는 **분기 아이템(종목)마다 최초 1회는 반드시 실행**해야 한다
18897
+ # — 그래야 그 종목의 구독이 걸린다. 그 뒤(틱마다의 분기 재구동)엔 실행하지
18898
+ # 않는다: 재실행하면 틱마다 재구독이 걸려 소켓이 폭주한다. 최신 시세는 틱
18899
+ # 콜백이 이 노드의 context 출력에 계속 갱신해 두므로 하류는 그걸 읽으면 된다.
18900
+ #
18901
+ # ⚠️ 예전엔 "출력이 있으면 스킵"으로 판단했는데, 첫 종목이 실행되며 출력을
18902
+ # 남기는 순간 **2번째 이후 종목의 최초 구독까지 건너뛰어** 그 종목엔 시세가
18903
+ # 영영 오지 않았다(HKEX 라이브에서 2번째 종목이 통째로 死, 2026-07-14).
18904
+ subscribed = list(
18905
+ self.context.get_node_state(node_id, _SUBSCRIBED_SCOPES_KEY) or []
18906
+ )
18907
+ if branch_scope in subscribed:
18908
+ continue
18866
18909
 
18867
18910
  # Prepare config
18868
18911
  config = dict(node.config)
@@ -18901,15 +18944,26 @@ class WorkflowJob:
18901
18944
  for port_name, value in outputs.items():
18902
18945
  self.context.set_output(node_id, port_name, value)
18903
18946
 
18947
+ if is_realtime:
18948
+ # 이 종목 구독 완료 — 다음 재구동부터는 이 노드를 건너뛴다
18949
+ subscribed.append(branch_scope)
18950
+ self.context.set_node_state(node_id, _SUBSCRIBED_SCOPES_KEY, subscribed)
18951
+
18904
18952
  # Track last result — only from PUBLIC ports. Internal meta
18905
18953
  # (_throttle_stats 등)이 유일 출력일 때 그걸 결과로 집으면 Aggregate/Display
18906
18954
  # 가 내부 통계를 데이터로 렌더한다.
18907
18955
  public = _public_outputs(outputs) if outputs else {}
18908
18956
  last_had_public = bool(public)
18909
18957
  if public:
18910
- row = _realtime_branch_row(public, symbol) if realtime_branch else None
18911
- if row is not None:
18912
- result = row
18958
+ if realtime_branch:
18959
+ row = _realtime_branch_row(public, symbol)
18960
+ if row is None:
18961
+ # 실시간 분기인데 가격 payload 가 없다(예: symbols 목록만 남음)
18962
+ # → 이 아이템은 표에 기여할 실데이터가 없다 → skip.
18963
+ # 그러지 않으면 가격 없는 행이 '실시간 체결가' 표에 낀다.
18964
+ last_had_public = False
18965
+ else:
18966
+ result = row
18913
18967
  else:
18914
18968
  result = next(
18915
18969
  (public[p] for p in _BRANCH_PAYLOAD_PORTS if public.get(p) is not None),
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden"
8
- version = "1.29.1"
8
+ version = "1.29.2"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
11
11
  readme = "README.md"
File without changes