programgarden 1.33.2__tar.gz → 1.33.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.33.2 → programgarden-1.33.4}/PKG-INFO +2 -2
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/executor.py +363 -17
- {programgarden-1.33.2 → programgarden-1.33.4}/pyproject.toml +2 -2
- {programgarden-1.33.2 → programgarden-1.33.4}/README.md +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/__init__.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/binding_validator.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/client.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/code_worker.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/context.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/database/__init__.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/deep_fixtures.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/node_runner.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/resolver.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/resource/context.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/semantic_rules.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-1.33.2 → programgarden-1.33.4}/programgarden/validation_recommender.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programgarden
|
|
3
|
-
Version: 1.33.
|
|
3
|
+
Version: 1.33.4
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Author: 프로그램동산
|
|
@@ -17,7 +17,7 @@ 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.0,<2.0.0)
|
|
19
19
|
Requires-Dist: programgarden-core (>=1.25.0,<2.0.0)
|
|
20
|
-
Requires-Dist: programgarden-finance (>=1.9.
|
|
20
|
+
Requires-Dist: programgarden-finance (>=1.9.3,<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)
|
|
23
23
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
@@ -778,6 +778,260 @@ def evaluate_all_bindings(
|
|
|
778
778
|
return {k: evaluate_value(v) for k, v in config.items()}
|
|
779
779
|
|
|
780
780
|
|
|
781
|
+
# ---------------------------------------------------------------------------
|
|
782
|
+
# D2 (AI 모델 벤치마크 2026-09-06): "No symbols provided" 3분류
|
|
783
|
+
#
|
|
784
|
+
# 사이징/과거시세/현재가 노드가 빈 종목 목록을 받는 원인은 세 가지인데, 종전에는 전부
|
|
785
|
+
# 같은 warning("No symbols provided …")으로 찍혔다. 챗봇 저장 게이트(pg-ai
|
|
786
|
+
# `_detect_input_warnings`)는 그 문구를 "설계 결함" 으로 읽어 저장을 막으므로, 조건을
|
|
787
|
+
# 올바르게 배선한 워크플로우가 **모의 실행 표본에서 오늘 조건을 통과한 종목이 0** 이라는
|
|
788
|
+
# 이유만으로 저장되지 못했다(벤치 본선: 모델 무관 최대 실패 원인, 40여 건 중 대부분이
|
|
789
|
+
# `symbols: {{ nodes.rsi.passed_symbols }}` / `{{ nodes.filter.symbols }}` 정상 배선).
|
|
790
|
+
#
|
|
791
|
+
# no_signal — 원본 설정에 바인딩(또는 입력 포트)이 있고 상류가 이번 실행에서 빈 목록을
|
|
792
|
+
# 냈다. 정상 런타임(오늘 신호 없음). info 로만 남기고 경고를 찍지 않는다.
|
|
793
|
+
# unresolved — 바인딩 표현식이 리터럴로 남았다(없는 노드/포트, 반복 밖의 `{{ item }}`).
|
|
794
|
+
# unbound — symbols/symbol 자체가 없다(종목 소스 미연결).
|
|
795
|
+
# 뒤의 둘은 설계 결함이라 종전처럼 warning("No symbols provided …") 을 유지한다 — 게이트가
|
|
796
|
+
# 계속 잡아야 하는 케이스다.
|
|
797
|
+
# ---------------------------------------------------------------------------
|
|
798
|
+
EMPTY_SYMBOLS_NO_SIGNAL = "no_signal"
|
|
799
|
+
EMPTY_SYMBOLS_UNRESOLVED = "unresolved"
|
|
800
|
+
EMPTY_SYMBOLS_UNBOUND = "unbound"
|
|
801
|
+
EMPTY_SYMBOLS_UPSTREAM_FAILED = "upstream_failed" # 참조한 노드가 error/fetch_failed 를 냈다
|
|
802
|
+
|
|
803
|
+
_EMPTY_VALUES = (None, "", [], {}, ())
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _raw_node_config(node_id: str, workflow: Any, fallback: Dict[str, Any]) -> Dict[str, Any]:
|
|
807
|
+
"""`ResolvedWorkflow.nodes[node_id].config` (평가 전 원본). 없으면 fallback."""
|
|
808
|
+
try:
|
|
809
|
+
nodes = getattr(workflow, "nodes", None)
|
|
810
|
+
node = nodes.get(node_id) if isinstance(nodes, dict) else None
|
|
811
|
+
except Exception:
|
|
812
|
+
node = None
|
|
813
|
+
cfg = getattr(node, "config", None)
|
|
814
|
+
return cfg if isinstance(cfg, dict) else fallback
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _input_namespace(context: "ExecutionContext", node_id: str) -> Dict[str, Any]:
|
|
818
|
+
"""`_input_<node_id>` 의사 노드(엣지로 들어온 상류 출력 전체). 없으면 {}."""
|
|
819
|
+
try:
|
|
820
|
+
outputs = context.get_all_outputs(f"_input_{node_id}")
|
|
821
|
+
except Exception:
|
|
822
|
+
return {}
|
|
823
|
+
return outputs if isinstance(outputs, dict) else {}
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _contains_expression(value: Any) -> bool:
|
|
827
|
+
if isinstance(value, str):
|
|
828
|
+
return "{{" in value
|
|
829
|
+
if isinstance(value, dict):
|
|
830
|
+
return any(_contains_expression(v) for v in value.values())
|
|
831
|
+
if isinstance(value, list):
|
|
832
|
+
return any(_contains_expression(v) for v in value)
|
|
833
|
+
return False
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _first_expression(value: Any) -> Optional[str]:
|
|
837
|
+
if isinstance(value, str):
|
|
838
|
+
return value if "{{" in value else None
|
|
839
|
+
if isinstance(value, dict):
|
|
840
|
+
for v in value.values():
|
|
841
|
+
found = _first_expression(v)
|
|
842
|
+
if found:
|
|
843
|
+
return found
|
|
844
|
+
if isinstance(value, list):
|
|
845
|
+
for v in value:
|
|
846
|
+
found = _first_expression(v)
|
|
847
|
+
if found:
|
|
848
|
+
return found
|
|
849
|
+
return None
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _referenced_node_failure(expr: Optional[str], context: "ExecutionContext") -> Optional[str]:
|
|
853
|
+
"""`{{ nodes.<id>.<port> }}` 가 가리키는 노드의 FULL 출력에 실패 신호(error /
|
|
854
|
+
reason=fetch_failed / _partial_failure)가 있으면 그 사유, 없으면 None.
|
|
855
|
+
|
|
856
|
+
MarketData/HistoricalData 는 조회 실패를 raise 하지 않고 ``{"values": [], "error": …}``
|
|
857
|
+
로 돌려주므로, 그 ``values`` 를 symbols 로 묶은 하류는 빈 목록만 보고 "오늘 신호 없음"
|
|
858
|
+
으로 오판할 수 있다 — 여기서 참조 노드 자체를 본다.
|
|
859
|
+
"""
|
|
860
|
+
if not isinstance(expr, str) or context is None:
|
|
861
|
+
return None
|
|
862
|
+
import re as _re
|
|
863
|
+
|
|
864
|
+
m = _re.search(r"nodes\.([A-Za-z0-9_\-]+)", expr)
|
|
865
|
+
if not m:
|
|
866
|
+
return None
|
|
867
|
+
try:
|
|
868
|
+
outputs = context.get_all_outputs(m.group(1))
|
|
869
|
+
except Exception:
|
|
870
|
+
return None
|
|
871
|
+
if not isinstance(outputs, dict) or not outputs:
|
|
872
|
+
return None
|
|
873
|
+
return NewOrderNodeExecutor._extract_upstream_error(outputs)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
# 심볼을 나르는 포트 — `{{ item }}` 이 반복 밖에서 리터럴로 남았을 때 "상류 배열이 비어
|
|
877
|
+
# 반복이 안 일어났다" 를 판정할 축. 메인 루프의 auto-iterate 소스 우선순위(명시 from_port
|
|
878
|
+
# > symbols > 첫 출력)와 맞춘다: ConditionNode 는 0건 통과일 때도 ``symbols``(평가 대상 전체)·
|
|
879
|
+
# ``failed_symbols``·``symbol_results`` 가 **비어 있지 않으므로**, 모든 리스트 포트가 비어야
|
|
880
|
+
# 한다는 규칙은 D2 가 없애려던 바로 그 케이스(조건 미통과 → `symbol: {{ item }}` 경고)를
|
|
881
|
+
# 남긴다. passed_symbols 가 있으면 그것만, 없으면 symbols, 둘 다 없으면 전체 리스트 포트.
|
|
882
|
+
_ITERATION_SYMBOL_PORTS = ("passed_symbols", "symbols", "items", "values")
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _upstream_array_is_empty(inputs: Dict[str, Any]) -> Optional[bool]:
|
|
886
|
+
"""True = 반복 소스 배열이 비어 있다(no_signal), False = 배열이 있는데 반복이 안 걸렸다
|
|
887
|
+
(설계 결함), None = 리스트 포트가 하나도 없다."""
|
|
888
|
+
for port in _ITERATION_SYMBOL_PORTS:
|
|
889
|
+
if port in inputs and isinstance(inputs[port], list):
|
|
890
|
+
return len(inputs[port]) == 0
|
|
891
|
+
list_ports = [v for k, v in inputs.items() if isinstance(v, list) and not str(k).startswith("_")]
|
|
892
|
+
if not list_ports:
|
|
893
|
+
return None
|
|
894
|
+
return all(len(v) == 0 for v in list_ports)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def classify_empty_symbol_source(
|
|
898
|
+
node_id: str,
|
|
899
|
+
evaluated_config: Dict[str, Any],
|
|
900
|
+
context: "ExecutionContext",
|
|
901
|
+
workflow: Any = None,
|
|
902
|
+
keys: Tuple[str, ...] = ("symbols", "symbol"),
|
|
903
|
+
) -> Tuple[str, str]:
|
|
904
|
+
"""빈 종목 입력의 원인을 (kind, detail) 로 돌려준다.
|
|
905
|
+
|
|
906
|
+
kind ∈ {EMPTY_SYMBOLS_NO_SIGNAL, EMPTY_SYMBOLS_UNRESOLVED, EMPTY_SYMBOLS_UNBOUND}.
|
|
907
|
+
detail 은 사람이 읽는 한 문장(영문 — 챗봇 fix_hint 로 그대로 실린다).
|
|
908
|
+
|
|
909
|
+
판정 순서:
|
|
910
|
+
1. 평가된 값에 `{{ … }}` 가 리터럴로 남아 있다 → 미해석.
|
|
911
|
+
단 `{{ item }}`/`{{ index }}` 인데 반복 컨텍스트가 없으면, 상류 배열이 비어
|
|
912
|
+
반복이 안 일어난 것(no_signal)과 배열 소스 없이 item 바인딩을 쓴 것(unresolved)을
|
|
913
|
+
`_input_<id>` 의 리스트 포트로 가른다.
|
|
914
|
+
2. 원본 설정에 바인딩/리터럴이 있었거나 입력 포트로 값이 들어왔다 → 상류가 빈 목록을
|
|
915
|
+
낸 것(no_signal). 원본이 표현식 없는 리터럴 목록인데 정규화 결과가 비었다면 항목
|
|
916
|
+
모양이 틀린 것(unbound 로 분류하되 detail 로 알린다).
|
|
917
|
+
3. 그 외 → 종목 소스 미연결(unbound).
|
|
918
|
+
"""
|
|
919
|
+
import re as _re
|
|
920
|
+
|
|
921
|
+
item_re = _re.compile(r"\{\{\s*(item|index)\b")
|
|
922
|
+
raw = _raw_node_config(node_id, workflow, evaluated_config)
|
|
923
|
+
inputs = _input_namespace(context, node_id)
|
|
924
|
+
iterating = getattr(context, "_iteration_item", None) is not None
|
|
925
|
+
|
|
926
|
+
# 1) 미해석 표현식
|
|
927
|
+
for key in keys:
|
|
928
|
+
value = evaluated_config.get(key)
|
|
929
|
+
expr = _first_expression(value)
|
|
930
|
+
if not expr:
|
|
931
|
+
continue
|
|
932
|
+
if item_re.search(expr) and not iterating:
|
|
933
|
+
empty = _upstream_array_is_empty(inputs)
|
|
934
|
+
if empty is True:
|
|
935
|
+
return (
|
|
936
|
+
EMPTY_SYMBOLS_NO_SIGNAL,
|
|
937
|
+
f"`{key}: {expr}` — the upstream symbol array is empty in this run "
|
|
938
|
+
"(no symbol passed / nothing to iterate), so this node was not iterated (no signal)",
|
|
939
|
+
)
|
|
940
|
+
return (
|
|
941
|
+
EMPTY_SYMBOLS_UNRESOLVED,
|
|
942
|
+
f"`{key}: {expr}` is an item binding but this node is not being iterated — "
|
|
943
|
+
"`{{ item }}` only resolves when this node is auto-iterated over an upstream "
|
|
944
|
+
"array (WatchlistNode.symbols / ConditionNode.passed_symbols / SymbolFilterNode.symbols); "
|
|
945
|
+
"inside a SplitNode branch use `{{ nodes.<split>.item }}` instead",
|
|
946
|
+
)
|
|
947
|
+
return (
|
|
948
|
+
EMPTY_SYMBOLS_UNRESOLVED,
|
|
949
|
+
f"`{key}: {expr}` did not resolve — check the node id and output port "
|
|
950
|
+
"(e.g. `{{ nodes.<condition>.passed_symbols }}` / `{{ nodes.<filter>.symbols }}`)",
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
# 2) 바인딩/입력 포트는 있는데 결과가 빈 목록
|
|
954
|
+
bound_key = None
|
|
955
|
+
for key in keys:
|
|
956
|
+
if raw.get(key) not in _EMPTY_VALUES:
|
|
957
|
+
bound_key = key
|
|
958
|
+
break
|
|
959
|
+
port_key = next((k for k in keys if k in inputs), None)
|
|
960
|
+
|
|
961
|
+
if bound_key is not None:
|
|
962
|
+
raw_value = raw[bound_key]
|
|
963
|
+
if _contains_expression(raw_value):
|
|
964
|
+
expr = _first_expression(raw_value)
|
|
965
|
+
resolved = evaluated_config.get(bound_key)
|
|
966
|
+
# 참조한 노드 자체가 실패했다(MarketData/HistoricalData 의 `{"values": [], "error": …}`,
|
|
967
|
+
# reason=fetch_failed, _partial_failure) → 빈 목록은 신호 없음이 아니라 파이프라인 고장.
|
|
968
|
+
failure = _referenced_node_failure(expr, context)
|
|
969
|
+
if failure:
|
|
970
|
+
return (
|
|
971
|
+
EMPTY_SYMBOLS_UPSTREAM_FAILED,
|
|
972
|
+
f"upstream node referenced by `{bound_key}: {expr}` failed: {failure}",
|
|
973
|
+
)
|
|
974
|
+
# 표현식이 **빈 목록이 아니라 None** 으로 풀렸다 = 참조한 노드/포트가 없다
|
|
975
|
+
# (평가기는 없는 포트를 None 으로 관대하게 푼다). 실제 종목 소스(ConditionNode.
|
|
976
|
+
# passed_symbols / SymbolFilterNode.symbols …)는 비어도 항상 [] 를 낸다.
|
|
977
|
+
if resolved is None and bound_key not in inputs:
|
|
978
|
+
return (
|
|
979
|
+
EMPTY_SYMBOLS_UNRESOLVED,
|
|
980
|
+
f"`{bound_key}: {expr}` resolved to nothing — the referenced node id or "
|
|
981
|
+
"output port does not exist (check the spelling; e.g. "
|
|
982
|
+
"`{{ nodes.<condition>.passed_symbols }}` / `{{ nodes.<filter>.symbols }}`)",
|
|
983
|
+
)
|
|
984
|
+
# 스칼라(bool/int/str)로 풀렸다 = 종목 목록이 아닌 포트를 묶었다(`result` /
|
|
985
|
+
# `is_condition_met` / `count`). 매 실행 빈 목록이 되는 영구 결함이라 warning.
|
|
986
|
+
if resolved is not None and not isinstance(resolved, (list, dict)):
|
|
987
|
+
return (
|
|
988
|
+
EMPTY_SYMBOLS_UNRESOLVED,
|
|
989
|
+
f"`{bound_key}: {expr}` resolved to a {type(resolved).__name__} "
|
|
990
|
+
f"({str(resolved)[:40]!r}), not a symbol list — bind a list port "
|
|
991
|
+
"(`passed_symbols` / `symbols`), not `result` / `is_condition_met` / `count`",
|
|
992
|
+
)
|
|
993
|
+
return (
|
|
994
|
+
EMPTY_SYMBOLS_NO_SIGNAL,
|
|
995
|
+
f"upstream symbol source `{bound_key}: {expr}` "
|
|
996
|
+
"produced no symbols in this run (no signal)",
|
|
997
|
+
)
|
|
998
|
+
if isinstance(raw_value, (list, dict)):
|
|
999
|
+
return (
|
|
1000
|
+
EMPTY_SYMBOLS_UNBOUND,
|
|
1001
|
+
f"`{bound_key}` is a literal list but none of its entries is a usable "
|
|
1002
|
+
"{symbol, exchange} object or symbol string",
|
|
1003
|
+
)
|
|
1004
|
+
return (
|
|
1005
|
+
EMPTY_SYMBOLS_NO_SIGNAL,
|
|
1006
|
+
f"symbol source `{bound_key}` resolved to nothing in this run (no signal)",
|
|
1007
|
+
)
|
|
1008
|
+
if port_key is not None:
|
|
1009
|
+
port_value = inputs.get(port_key)
|
|
1010
|
+
# 엣지로 들어온 포트가 **비어 있지 않은데** 이 노드가 빈 목록을 봤다 = 이 노드는
|
|
1011
|
+
# 입력 포트를 읽지 않는다(사이징은 evaluated config 만 본다). 바인딩을 명시해야
|
|
1012
|
+
# 하는 설계 결함이지 "오늘 신호 없음" 이 아니다.
|
|
1013
|
+
if port_value not in _EMPTY_VALUES:
|
|
1014
|
+
return (
|
|
1015
|
+
EMPTY_SYMBOLS_UNBOUND,
|
|
1016
|
+
f"the upstream node emits a non-empty `{port_key}` list but this node has no "
|
|
1017
|
+
f"`{port_key}` binding to consume it — bind `{port_key}: "
|
|
1018
|
+
"{{ nodes.<upstream>." + port_key + " }}` explicitly",
|
|
1019
|
+
)
|
|
1020
|
+
return (
|
|
1021
|
+
EMPTY_SYMBOLS_NO_SIGNAL,
|
|
1022
|
+
f"the upstream node wired into this node emitted an empty `{port_key}` list "
|
|
1023
|
+
"in this run (no signal)",
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
# 3) 소스 없음
|
|
1027
|
+
return (
|
|
1028
|
+
EMPTY_SYMBOLS_UNBOUND,
|
|
1029
|
+
"no symbol source is configured — bind `symbols` to an upstream list "
|
|
1030
|
+
"(`{{ nodes.<condition>.passed_symbols }}` / `{{ nodes.<filter>.symbols }}` / "
|
|
1031
|
+
"`{{ nodes.<watchlist>.symbols }}`) or `symbol` to `{{ item }}` inside a per-symbol loop",
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
|
|
781
1035
|
class GenericNodeExecutor(NodeExecutorBase):
|
|
782
1036
|
"""
|
|
783
1037
|
범용 노드 실행기 (커뮤니티 노드 및 execute() 메서드가 있는 노드용)
|
|
@@ -10275,7 +10529,18 @@ class MarketDataNodeExecutor(NodeExecutorBase):
|
|
|
10275
10529
|
return _df.apply_override(fixture, override)
|
|
10276
10530
|
|
|
10277
10531
|
if not symbols:
|
|
10278
|
-
|
|
10532
|
+
# D2: 상류(조건/필터)가 이번 실행에서 빈 목록을 낸 정상 케이스는 error 없이 빈
|
|
10533
|
+
# values 로 흘린다(오늘 조회할 종목 없음). 소스 미연결/미해석만 종전 error.
|
|
10534
|
+
kind, detail = classify_empty_symbol_source(
|
|
10535
|
+
node_id, config, context, kwargs.get("workflow"), keys=("symbol", "symbols")
|
|
10536
|
+
)
|
|
10537
|
+
if kind == EMPTY_SYMBOLS_NO_SIGNAL:
|
|
10538
|
+
context.log("info", f"No symbols to quote in this run — {detail}", node_id)
|
|
10539
|
+
return {"values": []}
|
|
10540
|
+
error_msg = (
|
|
10541
|
+
"symbols 필드가 필수입니다. 종목을 직접 입력하거나 WatchlistNode를 연결하세요. "
|
|
10542
|
+
f"({detail})"
|
|
10543
|
+
)
|
|
10279
10544
|
context.log("error", error_msg, node_id)
|
|
10280
10545
|
return {"error": error_msg, "values": []}
|
|
10281
10546
|
|
|
@@ -10340,6 +10605,7 @@ class MarketDataNodeExecutor(NodeExecutorBase):
|
|
|
10340
10605
|
values = []
|
|
10341
10606
|
|
|
10342
10607
|
for symbol_entry in symbols:
|
|
10608
|
+
exchange, symbol = "", "" # handler-safe defaults: the except below must never touch an unassigned name (UnboundLocalError masked the real cause)
|
|
10343
10609
|
try:
|
|
10344
10610
|
# 거래소와 심볼 추출
|
|
10345
10611
|
exchange = symbol_entry.get("exchange", "NASDAQ")
|
|
@@ -10414,7 +10680,7 @@ class MarketDataNodeExecutor(NodeExecutorBase):
|
|
|
10414
10680
|
context.log("warning", f"No data for {exchange}:{symbol}", node_id)
|
|
10415
10681
|
|
|
10416
10682
|
except Exception as e:
|
|
10417
|
-
context.log("warning", f"Failed to fetch {exchange}:{symbol}: {e}", node_id)
|
|
10683
|
+
context.log("warning", f"Failed to fetch {exchange}:{symbol or symbol_entry!r}: {e}", node_id)
|
|
10418
10684
|
continue
|
|
10419
10685
|
|
|
10420
10686
|
return {"values": values}
|
|
@@ -10459,6 +10725,7 @@ class MarketDataNodeExecutor(NodeExecutorBase):
|
|
|
10459
10725
|
values = []
|
|
10460
10726
|
|
|
10461
10727
|
for symbol_entry in symbols:
|
|
10728
|
+
exchange, symbol = "", "" # handler-safe defaults: the except below must never touch an unassigned name (UnboundLocalError masked the real cause)
|
|
10462
10729
|
try:
|
|
10463
10730
|
# 해외선물은 exchange 대신 symbol만 사용 (예: "GCGF25", "CLH25")
|
|
10464
10731
|
exchange = symbol_entry.get("exchange", "CME")
|
|
@@ -10499,7 +10766,7 @@ class MarketDataNodeExecutor(NodeExecutorBase):
|
|
|
10499
10766
|
context.log("warning", f"No data for {exchange}:{symbol}", node_id)
|
|
10500
10767
|
|
|
10501
10768
|
except Exception as e:
|
|
10502
|
-
context.log("warning", f"Failed to fetch {exchange}:{symbol}: {e}", node_id)
|
|
10769
|
+
context.log("warning", f"Failed to fetch {exchange}:{symbol or symbol_entry!r}: {e}", node_id)
|
|
10503
10770
|
continue
|
|
10504
10771
|
|
|
10505
10772
|
return {"values": values}
|
|
@@ -10544,6 +10811,7 @@ class MarketDataNodeExecutor(NodeExecutorBase):
|
|
|
10544
10811
|
values = []
|
|
10545
10812
|
|
|
10546
10813
|
for symbol_entry in symbols:
|
|
10814
|
+
symbol = "" # handler-safe default: the except below must never touch an unassigned name (UnboundLocalError masked the real cause)
|
|
10547
10815
|
try:
|
|
10548
10816
|
symbol = symbol_entry.get("symbol", "")
|
|
10549
10817
|
if not symbol:
|
|
@@ -10583,7 +10851,7 @@ class MarketDataNodeExecutor(NodeExecutorBase):
|
|
|
10583
10851
|
context.log("warning", f"No data for KRX:{symbol}", node_id)
|
|
10584
10852
|
|
|
10585
10853
|
except Exception as e:
|
|
10586
|
-
context.log("warning", f"Failed to fetch KRX:{symbol}: {e}", node_id)
|
|
10854
|
+
context.log("warning", f"Failed to fetch KRX:{symbol or symbol_entry!r}: {e}", node_id)
|
|
10587
10855
|
continue
|
|
10588
10856
|
|
|
10589
10857
|
return {"values": values}
|
|
@@ -10644,7 +10912,17 @@ class FundamentalNodeExecutor(NodeExecutorBase):
|
|
|
10644
10912
|
symbols = [config_symbol]
|
|
10645
10913
|
|
|
10646
10914
|
if not symbols:
|
|
10647
|
-
|
|
10915
|
+
# D2: MarketDataNodeExecutor 와 같은 3분류 — 정상 no-signal 은 error 없이 빈 values.
|
|
10916
|
+
kind, detail = classify_empty_symbol_source(
|
|
10917
|
+
node_id, config, context, kwargs.get("workflow"), keys=("symbol", "symbols")
|
|
10918
|
+
)
|
|
10919
|
+
if kind == EMPTY_SYMBOLS_NO_SIGNAL:
|
|
10920
|
+
context.log("info", f"No symbols to look up in this run — {detail}", node_id)
|
|
10921
|
+
return {"values": []}
|
|
10922
|
+
error_msg = (
|
|
10923
|
+
"symbols 필드가 필수입니다. 종목을 직접 입력하거나 WatchlistNode를 연결하세요. "
|
|
10924
|
+
f"({detail})"
|
|
10925
|
+
)
|
|
10648
10926
|
context.log("error", error_msg, node_id)
|
|
10649
10927
|
return {"error": error_msg, "values": []}
|
|
10650
10928
|
|
|
@@ -10690,6 +10968,7 @@ class FundamentalNodeExecutor(NodeExecutorBase):
|
|
|
10690
10968
|
values = []
|
|
10691
10969
|
|
|
10692
10970
|
for symbol_entry in symbols:
|
|
10971
|
+
exchange, symbol = "", "" # handler-safe defaults: the except below must never touch an unassigned name (UnboundLocalError masked the real cause)
|
|
10693
10972
|
try:
|
|
10694
10973
|
exchange = symbol_entry.get("exchange", "NASDAQ")
|
|
10695
10974
|
symbol = symbol_entry.get("symbol", "")
|
|
@@ -10739,7 +11018,7 @@ class FundamentalNodeExecutor(NodeExecutorBase):
|
|
|
10739
11018
|
context.log("warning", f"No data for {exchange}:{symbol}", node_id)
|
|
10740
11019
|
|
|
10741
11020
|
except Exception as e:
|
|
10742
|
-
context.log("warning", f"Failed to fetch {exchange}:{symbol}: {e}", node_id)
|
|
11021
|
+
context.log("warning", f"Failed to fetch {exchange}:{symbol or symbol_entry!r}: {e}", node_id)
|
|
10743
11022
|
continue
|
|
10744
11023
|
|
|
10745
11024
|
return {"values": values}
|
|
@@ -10780,6 +11059,7 @@ class FundamentalNodeExecutor(NodeExecutorBase):
|
|
|
10780
11059
|
values = []
|
|
10781
11060
|
|
|
10782
11061
|
for symbol_entry in symbols:
|
|
11062
|
+
symbol = "" # handler-safe default: the except below must never touch an unassigned name (UnboundLocalError masked the real cause)
|
|
10783
11063
|
try:
|
|
10784
11064
|
symbol = symbol_entry.get("symbol", "")
|
|
10785
11065
|
if not symbol:
|
|
@@ -10812,7 +11092,7 @@ class FundamentalNodeExecutor(NodeExecutorBase):
|
|
|
10812
11092
|
context.log("warning", f"No data for KRX:{symbol}", node_id)
|
|
10813
11093
|
|
|
10814
11094
|
except Exception as e:
|
|
10815
|
-
context.log("warning", f"Failed to fetch KRX:{symbol}: {e}", node_id)
|
|
11095
|
+
context.log("warning", f"Failed to fetch KRX:{symbol or symbol_entry!r}: {e}", node_id)
|
|
10816
11096
|
continue
|
|
10817
11097
|
|
|
10818
11098
|
return {"values": values}
|
|
@@ -10998,7 +11278,16 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
|
|
|
10998
11278
|
# / ConditionNode 의 `item.time_series`)이 조용히 None 이 되고 방어적
|
|
10999
11279
|
# `data or []` 가 그걸 삼켜 에러 없이 빈 결과(count:0)를 낸다.
|
|
11000
11280
|
# (deep_fixtures.historical_data_fixture 도 이 스키마를 못박는다.)
|
|
11001
|
-
|
|
11281
|
+
# D2: 상류 배열이 비어 반복이 없었던 것(정상 no-signal)은 info, 소스 미연결/
|
|
11282
|
+
# 미해석 바인딩(설계 결함)만 warning("No symbols provided …") — 챗봇 저장
|
|
11283
|
+
# 게이트가 이 문구를 결함으로 읽으므로 정상 케이스에는 찍지 않는다.
|
|
11284
|
+
kind, detail = classify_empty_symbol_source(
|
|
11285
|
+
node_id, config, context, kwargs.get("workflow"), keys=("symbol", "symbols")
|
|
11286
|
+
)
|
|
11287
|
+
if kind == EMPTY_SYMBOLS_NO_SIGNAL:
|
|
11288
|
+
context.log("info", f"No symbols to fetch in this run — {detail}", node_id)
|
|
11289
|
+
else:
|
|
11290
|
+
context.log("warning", f"No symbols provided — {detail}", node_id)
|
|
11002
11291
|
return {
|
|
11003
11292
|
"value": None,
|
|
11004
11293
|
"values": [],
|
|
@@ -12757,18 +13046,45 @@ class PositionSizingNodeExecutor(NodeExecutorBase):
|
|
|
12757
13046
|
kelly_fraction = float(evaluated.get("kelly_fraction", 0.25))
|
|
12758
13047
|
atr_risk_percent = float(evaluated.get("atr_risk_percent", 1.0))
|
|
12759
13048
|
|
|
13049
|
+
# 상류가 error 신호를 동봉한 dict(예: `{"error": …, "values": []}`)를 symbols 로
|
|
13050
|
+
# 넘겼으면 fetch_failed. 정규화 **전에** 본다 — `_normalize_symbols` 의 dict 분기는
|
|
13051
|
+
# `{종목: {...}}` 맵 전용이라, error 페이로드를 넣으면 "error"/"values" 라는 가짜
|
|
13052
|
+
# 종목 2건이 만들어져 사이징이 그대로 진행됐다(빈-목록 분기에 영영 안 닿음).
|
|
13053
|
+
if isinstance(symbols_input, dict) and symbols_input.get("error"):
|
|
13054
|
+
err = str(symbols_input.get("error"))
|
|
13055
|
+
context.log(
|
|
13056
|
+
"warning",
|
|
13057
|
+
f"No symbols provided for position sizing — upstream fetch failed: {err}",
|
|
13058
|
+
node_id,
|
|
13059
|
+
)
|
|
13060
|
+
return self._empty_result(EmptyOrderReason.FETCH_FAILED, err)
|
|
13061
|
+
|
|
12760
13062
|
# 종목 리스트 정규화
|
|
12761
13063
|
symbols = self._normalize_symbols(symbols_input)
|
|
12762
13064
|
|
|
12763
13065
|
if not symbols:
|
|
12764
|
-
|
|
12765
|
-
#
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
13066
|
+
# D2: "오늘 신호 없음(정상)" 과 "종목 소스 미연결/미해석(설계 결함)" 을 가른다.
|
|
13067
|
+
# 전자는 warning 을 찍지 않는다 — 챗봇 저장 게이트가 "No symbols provided" 를
|
|
13068
|
+
# 설계 결함으로 읽어, 올바로 배선된 워크플로우가 모의 실행 표본에서 조건 통과
|
|
13069
|
+
# 종목이 0 이라는 이유만으로 저장되지 못했다(벤치 본선 최대 실패 원인).
|
|
13070
|
+
kind, detail = classify_empty_symbol_source(
|
|
13071
|
+
node_id, evaluated, context, kwargs.get("workflow"), keys=("symbols", "symbol")
|
|
13072
|
+
)
|
|
13073
|
+
if kind == EMPTY_SYMBOLS_NO_SIGNAL:
|
|
13074
|
+
context.log(
|
|
13075
|
+
"info",
|
|
13076
|
+
f"No symbols to size in this run — {detail}; sizing skipped",
|
|
13077
|
+
node_id,
|
|
13078
|
+
)
|
|
13079
|
+
return self._empty_result(EmptyOrderReason.NO_SIGNAL, detail)
|
|
13080
|
+
context.log(
|
|
13081
|
+
"warning",
|
|
13082
|
+
f"No symbols provided for position sizing — {detail}",
|
|
13083
|
+
node_id,
|
|
13084
|
+
)
|
|
13085
|
+
if kind == EMPTY_SYMBOLS_UPSTREAM_FAILED:
|
|
13086
|
+
return self._empty_result(EmptyOrderReason.FETCH_FAILED, detail)
|
|
13087
|
+
return self._empty_result(EmptyOrderReason.NO_SYMBOL, detail)
|
|
12772
13088
|
|
|
12773
13089
|
# Refuse to silently zero-out when the upstream AccountNode flagged
|
|
12774
13090
|
# a partial fetch. fixed_quantity ignores balance entirely, so it
|
|
@@ -12812,6 +13128,18 @@ class PositionSizingNodeExecutor(NodeExecutorBase):
|
|
|
12812
13128
|
or "balance fetch partially failed"
|
|
12813
13129
|
)
|
|
12814
13130
|
return self._empty_result(EmptyOrderReason.FETCH_FAILED, detail)
|
|
13131
|
+
# 잔고 입력 자체가 없거나 미해석(`{{ … }}` 리터럴) = 읽지 못한 것이지 "오늘
|
|
13132
|
+
# 신호 없음" 이 아니다. 종목은 있는데 잔고가 안 묶인 채 no_signal 로 흘리면
|
|
13133
|
+
# 하류 주문 노드(D2 상속)까지 조용한 no-op 이 된다. 실제로 잔고 dict 가 있고
|
|
13134
|
+
# 매수가능금액이 0 인 경우만 정상 no_signal(현금 없음).
|
|
13135
|
+
if balance_data in (None, "", {}, [], ()) or (
|
|
13136
|
+
isinstance(balance_data, str) and "{{" in balance_data
|
|
13137
|
+
):
|
|
13138
|
+
return self._empty_result(
|
|
13139
|
+
EmptyOrderReason.FETCH_FAILED,
|
|
13140
|
+
"balance input is missing or unresolved — bind `balance` to "
|
|
13141
|
+
"AccountNode.balance (every sizing method except fixed_quantity needs it)",
|
|
13142
|
+
)
|
|
12815
13143
|
return self._empty_result(
|
|
12816
13144
|
EmptyOrderReason.NO_SIGNAL,
|
|
12817
13145
|
"No available balance for position sizing",
|
|
@@ -13914,7 +14242,7 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
13914
14242
|
if not normalized_order:
|
|
13915
14243
|
context.log("warning", f"{node_type}: 주문할 종목이 없습니다", node_id)
|
|
13916
14244
|
reason, detail = self._diagnose_empty_reason(
|
|
13917
|
-
order, config, raw_order_expr, context
|
|
14245
|
+
order, config, raw_order_expr, context, node_id=node_id
|
|
13918
14246
|
)
|
|
13919
14247
|
return self._empty_result(reason, detail)
|
|
13920
14248
|
|
|
@@ -14144,6 +14472,7 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
14144
14472
|
config: Dict[str, Any],
|
|
14145
14473
|
raw_order_expr: Any = None,
|
|
14146
14474
|
context: Optional[ExecutionContext] = None,
|
|
14475
|
+
node_id: Optional[str] = None,
|
|
14147
14476
|
) -> tuple:
|
|
14148
14477
|
"""빈 주문 결과의 reason 을 상류 신호로 판정.
|
|
14149
14478
|
|
|
@@ -14194,6 +14523,23 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
14194
14523
|
if err is not None:
|
|
14195
14524
|
return EmptyOrderReason.FETCH_FAILED, err
|
|
14196
14525
|
|
|
14526
|
+
# 3.5) 상류가 명시적으로 "오늘 신호 없음"(reason=no_signal) 을 냈으면 정상 no-op.
|
|
14527
|
+
# D2(벤치 2026-09-06): 사이징이 빈 종목 목록(조건 미통과)을 받아 orders=[] 를
|
|
14528
|
+
# 내면 주문 노드는 order=None 을 받는데, 아래 5) 가 그걸 "종목 미지정(no_symbol)"
|
|
14529
|
+
# 으로 분류해 `_order_failure_from_outputs` 가 매 실행 "설정 누락" 으로 보고했다.
|
|
14530
|
+
# 상류의 no_signal 은 실패 신호(error/_partial_failure/fetch_failed)가 없을 때만
|
|
14531
|
+
# 그대로 물려받는다.
|
|
14532
|
+
for candidate in (upstream, _input_namespace(context, node_id) if (context is not None and node_id) else None):
|
|
14533
|
+
if (
|
|
14534
|
+
isinstance(candidate, dict)
|
|
14535
|
+
and candidate.get("reason") == EmptyOrderReason.NO_SIGNAL.value
|
|
14536
|
+
and self._extract_upstream_error(candidate) is None
|
|
14537
|
+
):
|
|
14538
|
+
return (
|
|
14539
|
+
EmptyOrderReason.NO_SIGNAL,
|
|
14540
|
+
str(candidate.get("detail") or candidate.get("message") or ""),
|
|
14541
|
+
)
|
|
14542
|
+
|
|
14197
14543
|
# 4) 상류가 낸 빈 리스트에 실패 신호 동봉 → 조회 실패(파이프라인 고장).
|
|
14198
14544
|
# order/symbols 바인딩 결과가 dict 이면서 error/_partial_failure/
|
|
14199
14545
|
# reason='fetch_failed' 를 들고 있는 경우를 본다.
|
|
@@ -5,7 +5,7 @@ authors = [
|
|
|
5
5
|
homepage = "https://programgarden.com"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
name = "programgarden"
|
|
8
|
-
version = "1.33.
|
|
8
|
+
version = "1.33.4"
|
|
9
9
|
license = "AGPL-3.0-or-later"
|
|
10
10
|
description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
|
|
11
11
|
readme = "README.md"
|
|
@@ -30,7 +30,7 @@ pytickersymbols = {version = ">=1.17.5", python = ">=3.12,<4.0"}
|
|
|
30
30
|
aiosqlite = "^0.20.0"
|
|
31
31
|
litellm = ">=1.40.0"
|
|
32
32
|
programgarden-core = "^1.25.0"
|
|
33
|
-
programgarden-finance = "^1.9.
|
|
33
|
+
programgarden-finance = "^1.9.3"
|
|
34
34
|
programgarden-community = "^1.15.0"
|
|
35
35
|
|
|
36
36
|
[tool.poetry.group.dev.dependencies]
|
|
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-1.33.2 → programgarden-1.33.4}/programgarden/database/workflow_position_tracker.py
RENAMED
|
File without changes
|
{programgarden-1.33.2 → programgarden-1.33.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
|
|
File without changes
|
|
File without changes
|