programgarden-core 1.15.2__tar.gz → 1.16.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 (85) hide show
  1. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/PKG-INFO +1 -1
  2. programgarden_core-1.16.0/programgarden_core/code_node.py +411 -0
  3. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/locales/en.json +7 -0
  4. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/locales/ko.json +7 -0
  5. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/validation.py +7 -3
  6. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/__init__.py +2 -0
  7. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/account_futures.py +2 -2
  8. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/account_stock.py +1 -1
  9. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest.py +5 -5
  10. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest_futures.py +2 -2
  11. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest_korea_stock.py +2 -2
  12. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest_stock.py +2 -2
  13. programgarden_core-1.16.0/programgarden_core/nodes/code.py +338 -0
  14. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/data.py +5 -5
  15. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/display.py +40 -24
  16. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/order.py +15 -14
  17. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/realtime_futures.py +22 -3
  18. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/realtime_korea_stock.py +22 -3
  19. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/realtime_stock.py +22 -3
  20. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/risk.py +64 -30
  21. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/symbol.py +6 -4
  22. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/registry/__init__.py +0 -12
  23. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/registry/node_registry.py +2 -2
  24. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/pyproject.toml +1 -1
  25. programgarden_core-1.15.2/programgarden_core/registry/dynamic_node_registry.py +0 -409
  26. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/README.md +0 -0
  27. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/__init__.py +0 -0
  28. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/__init__.py +0 -0
  29. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/client.py +0 -0
  30. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/components.py +0 -0
  31. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/listener.py +0 -0
  32. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/mixins.py +0 -0
  33. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/products.py +0 -0
  34. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/sql.py +0 -0
  35. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/storage.py +0 -0
  36. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/exceptions.py +0 -0
  37. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/expression/__init__.py +0 -0
  38. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/expression/evaluator.py +0 -0
  39. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/__init__.py +0 -0
  40. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/translator.py +0 -0
  41. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/korea_alias.py +0 -0
  42. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/__init__.py +0 -0
  43. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/connection_rule.py +0 -0
  44. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/credential.py +0 -0
  45. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/edge.py +0 -0
  46. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/event.py +0 -0
  47. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/exchange.py +0 -0
  48. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/field_binding.py +0 -0
  49. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/job.py +0 -0
  50. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/order_diagnostics.py +0 -0
  51. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/plugin_resource.py +0 -0
  52. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/resilience.py +0 -0
  53. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/resource.py +0 -0
  54. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/workflow.py +0 -0
  55. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/account_korea_stock.py +0 -0
  56. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/ai.py +0 -0
  57. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/base.py +0 -0
  58. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/broker.py +0 -0
  59. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/calculation.py +0 -0
  60. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/condition.py +0 -0
  61. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/data_futures.py +0 -0
  62. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/data_korea_stock.py +0 -0
  63. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/data_stock.py +0 -0
  64. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/event.py +0 -0
  65. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
  66. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/fundamental_stock.py +0 -0
  67. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/infra.py +0 -0
  68. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/market_external.py +0 -0
  69. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/market_status.py +0 -0
  70. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/open_orders_futures.py +0 -0
  71. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
  72. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/open_orders_stock.py +0 -0
  73. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/portfolio.py +0 -0
  74. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/symbol_futures.py +0 -0
  75. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
  76. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/symbol_stock.py +0 -0
  77. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/trigger.py +0 -0
  78. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/__init__.py +0 -0
  79. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/news_analyst.json +0 -0
  80. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/risk_manager.json +0 -0
  81. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/strategist.json +0 -0
  82. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/technical_analyst.json +0 -0
  83. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/registry/credential_registry.py +0 -0
  84. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/registry/plugin_registry.py +0 -0
  85. {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/retry_executor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden-core
3
- Version: 1.15.2
3
+ Version: 1.16.0
4
4
  Summary: ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -0,0 +1,411 @@
1
+ """ProgramGarden Core — CodeNode static compilation & AST security screening.
2
+
3
+ This module is the single source of truth for the CodeNode security lists and
4
+ the compile/screen pipeline. It is intentionally dependency-free (stdlib only)
5
+ so it can be reused verbatim by:
6
+
7
+ 1. `WorkflowResolver.validate()` — pre-flight structured errors before run.
8
+ 2. `CodeNodeExecutor` / subprocess worker — screen again right before `exec`.
9
+ 3. An external web-platform "share gate" server — re-run the same hard AST scan.
10
+
11
+ Security model (see plan §2): CodeNode runs untrusted Python. Defense is layered:
12
+
13
+ * Layer 1 — scrubbed context (runtime, programgarden): the code never receives
14
+ credential accessors.
15
+ * Layer 2 — restricted builtins + AST denylist (THIS module): a curated
16
+ `__builtins__` (no eval/exec/open/getattr/...), a safe `__import__` gated by a
17
+ whitelist, and a static AST scan that rejects dangerous imports, builtin
18
+ calls, and introspection-escape dunders BEFORE any code runs.
19
+ * Layer 3 — binding seal (runtime, resolver): credentials cannot flow into
20
+ `data`/`params`.
21
+ * Layer 4 — subprocess isolation (runtime, programgarden): the code always runs
22
+ in a credential-free child process.
23
+
24
+ `compile_code_node` NEVER executes the code — it only parses, screens, and
25
+ compiles. Execution (exec + calling `execute`) is the runtime's responsibility.
26
+
27
+ All user-facing strings are English (AI-chatbot consumer contract).
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import ast
32
+ import builtins as _builtins
33
+ from dataclasses import dataclass, field
34
+ from types import CodeType
35
+ from typing import Any, Dict, List, Optional, Set, Tuple
36
+
37
+ # Name of the entry-point coroutine the code text must define.
38
+ EXECUTE_FN_NAME = "execute"
39
+
40
+ # ⚠️ SECURITY POSTURE — READ THIS.
41
+ # AST screening + restricted builtins CANNOT be a sound sandbox for CPython.
42
+ # A determined attacker who can run Python can, in the general case, escape any
43
+ # in-process denylist (this is a well-known result; see the pysandbox author's
44
+ # own withdrawal). An adversarial audit of an earlier version of this file
45
+ # reproduced RCE via `operator.attrgetter`, `random._os`, and string-obfuscated
46
+ # dunders. The rules below are HARDENED defense-in-depth that close every
47
+ # demonstrated vector and raise the bar substantially — but they are NOT a
48
+ # containment guarantee. TRUE containment of untrusted code is the runtime's
49
+ # job (subprocess with no in-memory keys) + the operator's job (per-user pod
50
+ # isolation, no-egress NetworkPolicy, restricted file perms, scrubbed env).
51
+ # Treat CodeNode as "run trusted code, defense-in-depth against mistakes and
52
+ # opportunistic abuse", not "safe to run arbitrary hostile code".
53
+
54
+ # ── Whitelist: modules importable from CodeNode code ────────────────────────
55
+ # Pure-computation stdlib only. `operator` is deliberately EXCLUDED — its
56
+ # attrgetter/methodcaller are getattr/dispatch substitutes. Modules that
57
+ # re-export os/sys as public attributes (e.g. random._os, uuid.os) are still
58
+ # covered by the attribute screen below, not by this set. The embedding app may
59
+ # EXTEND this via `allowed_imports=` (e.g. numpy) but the default is minimal.
60
+ DEFAULT_ALLOWED_IMPORTS: frozenset = frozenset({
61
+ "math", "cmath", "statistics", "decimal", "fractions", "numbers",
62
+ "random", "json", "re", "datetime", "time", "calendar",
63
+ "collections", "itertools", "functools",
64
+ "string", "textwrap", "unicodedata",
65
+ "heapq", "bisect", "array", "copy",
66
+ "dataclasses", "enum", "typing",
67
+ "hashlib", "hmac", "base64", "binascii", "zlib",
68
+ })
69
+
70
+ # ── Denylist: modules called out for a clearer error message ────────────────
71
+ # Enforcement is whitelist-based (not in DEFAULT_ALLOWED_IMPORTS → blocked);
72
+ # this set only makes the message explicit for well-known dangerous modules.
73
+ FORBIDDEN_MODULES: frozenset = frozenset({
74
+ "os", "sys", "subprocess", "socket", "shutil", "importlib", "imp",
75
+ "ctypes", "cffi", "gc", "inspect", "pathlib", "glob", "tempfile", "io",
76
+ "urllib", "http", "requests", "ftplib", "smtplib", "asyncio",
77
+ "multiprocessing", "threading", "concurrent", "_thread", "operator",
78
+ "builtins", "marshal", "pickle", "shelve", "dbm", "sqlite3",
79
+ "code", "codeop", "pty", "resource", "signal", "mmap", "fcntl",
80
+ "platform", "webbrowser", "ssl", "select", "selectors", "uuid",
81
+ })
82
+
83
+ # ── Denylist: builtin names that must never be called or aliased ────────────
84
+ FORBIDDEN_BUILTINS: frozenset = frozenset({
85
+ "eval", "exec", "compile", "open", "__import__",
86
+ "getattr", "setattr", "delattr",
87
+ "globals", "locals", "vars",
88
+ "input", "breakpoint", "help", "memoryview",
89
+ "exit", "quit", "__build_class__",
90
+ })
91
+
92
+ # ── Denylist: dangerous PUBLIC attribute names (module re-exports & escapes) ──
93
+ # Many stdlib modules re-export `os`/`sys` as public attributes (statistics.sys,
94
+ # uuid.os, ...) — walking those reaches os.system. `mro`/`modules`/`system`/etc.
95
+ # are the follow-on hops. Blocking these attribute NAMES (in addition to the
96
+ # blanket underscore rule below) severs the module-attribute escape path.
97
+ FORBIDDEN_ATTR_NAMES: frozenset = frozenset({
98
+ "os", "sys", "subprocess", "socket", "builtins", "importlib",
99
+ "modules", "system", "popen", "spawn", "spawnl", "spawnv", "spawnve",
100
+ "fork", "forkpty", "execv", "execve", "execl", "execlp", "execvp",
101
+ "mro", "import_module", "load_module", "reload", "getattr", "setattr",
102
+ "delattr", "globals", "environ", "getenv", "putenv", "connect",
103
+ "urlopen", "call", "run", "check_output", "Popen", "loader", "spec",
104
+ })
105
+
106
+ # ── Denylist: introspection-escape / exfiltration dunders & frame attrs ──────
107
+ # NOTE: enforcement is the BLANKET underscore rule in _scan_ast (any attribute
108
+ # or string beginning with '_' is rejected), which subsumes every dunder. This
109
+ # explicit set is retained for documentation / message clarity.
110
+ FORBIDDEN_DUNDERS: frozenset = frozenset({
111
+ "__class__", "__bases__", "__base__", "__subclasses__", "__mro__",
112
+ "__globals__", "__code__", "__closure__", "__func__", "__self__",
113
+ "__dict__", "__getattribute__", "__setattr__", "__delattr__",
114
+ "__reduce__", "__reduce_ex__", "__builtins__", "__import__",
115
+ "__loader__", "__spec__", "__init_subclass__", "__subclasshook__",
116
+ "__getattr__", "__wrapped__", "__objclass__",
117
+ "gi_frame", "gi_code", "cr_frame", "cr_code", "ag_frame",
118
+ "f_back", "f_globals", "f_locals", "f_builtins", "tb_frame",
119
+ })
120
+
121
+ # Format replacement-field that performs attribute access, e.g. "{0.__class__}"
122
+ # or "{x.sys}" — used to walk attributes via str.format without an AST Attribute
123
+ # node. Rejected wholesale in string constants.
124
+ import re as _re
125
+ _FORMAT_ATTR_RE = _re.compile(r"\{[^{}]*\.[^{}]*\}")
126
+
127
+
128
+ @dataclass
129
+ class CodeScreenResult:
130
+ """Structured outcome of `compile_code_node`. `ok=False` carries an
131
+ AI-chatbot-consumable error (code + message + suggestion + location)."""
132
+
133
+ ok: bool
134
+ code_object: Optional[CodeType] = None
135
+ error_code: Optional[str] = None # one of CODE_NODE_SYNTAX_ERROR / _FORBIDDEN / _NO_EXECUTE
136
+ message: Optional[str] = None
137
+ suggestion: Optional[str] = None
138
+ line: Optional[int] = None
139
+ offset: Optional[int] = None
140
+ details: Dict[str, Any] = field(default_factory=dict)
141
+
142
+
143
+ def _module_root(name: Optional[str]) -> str:
144
+ return (name or "").split(".")[0]
145
+
146
+
147
+ def _scan_ast(tree: ast.AST, allowed_imports: Set[str]) -> List[Dict[str, Any]]:
148
+ """Return a list of violation dicts (empty when clean).
149
+
150
+ Each violation: {kind, name, line, col, reason}. `kind` ∈
151
+ {import, builtin, dunder}.
152
+ """
153
+ violations: List[Dict[str, Any]] = []
154
+
155
+ for node in ast.walk(tree):
156
+ # ── Imports (whitelist-enforced) ──
157
+ if isinstance(node, ast.Import):
158
+ for alias in node.names:
159
+ root = _module_root(alias.name)
160
+ if root not in allowed_imports:
161
+ reason = (
162
+ f"module '{alias.name}' is blocked (filesystem/process/network access)"
163
+ if root in FORBIDDEN_MODULES
164
+ else f"module '{alias.name}' is not in the allowed import list"
165
+ )
166
+ violations.append({
167
+ "kind": "import", "name": alias.name,
168
+ "line": node.lineno, "col": node.col_offset, "reason": reason,
169
+ })
170
+ elif isinstance(node, ast.ImportFrom):
171
+ root = _module_root(node.module)
172
+ # `from . import x` (level>0) is always blocked — no package context.
173
+ if node.level and node.level > 0:
174
+ violations.append({
175
+ "kind": "import", "name": node.module or ".",
176
+ "line": node.lineno, "col": node.col_offset,
177
+ "reason": "relative imports are not allowed in CodeNode",
178
+ })
179
+ elif root not in allowed_imports:
180
+ reason = (
181
+ f"module '{node.module}' is blocked (filesystem/process/network access)"
182
+ if root in FORBIDDEN_MODULES
183
+ else f"module '{node.module}' is not in the allowed import list"
184
+ )
185
+ violations.append({
186
+ "kind": "import", "name": node.module or "",
187
+ "line": node.lineno, "col": node.col_offset, "reason": reason,
188
+ })
189
+
190
+ # ── Forbidden builtin names (call or alias) ──
191
+ elif isinstance(node, ast.Name):
192
+ if node.id in FORBIDDEN_BUILTINS:
193
+ violations.append({
194
+ "kind": "builtin", "name": node.id,
195
+ "line": node.lineno, "col": node.col_offset,
196
+ "reason": f"use of '{node.id}' is not allowed",
197
+ })
198
+
199
+ # ── Attribute access screen ──
200
+ # (1) BLANKET: any attribute beginning with '_' is rejected — this
201
+ # subsumes every dunder (__class__/__globals__/__subclasses__/...)
202
+ # AND private module re-exports (random._os, random._urandom, ...).
203
+ # (2) A denylist of dangerous PUBLIC names (module re-exports & shell
204
+ # hops): statistics.sys, uuid.os, x.system, x.modules, type().mro().
205
+ elif isinstance(node, ast.Attribute):
206
+ if node.attr.startswith("_"):
207
+ violations.append({
208
+ "kind": "dunder", "name": node.attr,
209
+ "line": node.lineno, "col": node.col_offset,
210
+ "reason": f"access to underscore attribute '{node.attr}' is not allowed (introspection escape)",
211
+ })
212
+ elif node.attr in FORBIDDEN_ATTR_NAMES:
213
+ violations.append({
214
+ "kind": "attr", "name": node.attr,
215
+ "line": node.lineno, "col": node.col_offset,
216
+ "reason": f"attribute '{node.attr}' access is not allowed (module/escape re-export)",
217
+ })
218
+
219
+ # ── String-constant screen ──
220
+ # Defeats attribute-walk via strings: getattr/attrgetter args, str.format
221
+ # attribute fields, subscript-string dunder access. Reject strings that
222
+ # start with '_', contain a dunder, name a dangerous attribute, or embed
223
+ # a format field with attribute access.
224
+ elif isinstance(node, ast.Constant) and isinstance(node.value, str):
225
+ v = node.value
226
+ bad = None
227
+ if v.startswith("_"):
228
+ bad = f"string literal '{v}' begins with '_' (introspection escape)"
229
+ elif "__" in v:
230
+ bad = f"string literal contains a dunder (introspection escape)"
231
+ elif v in FORBIDDEN_ATTR_NAMES:
232
+ bad = f"string literal '{v}' names a blocked attribute"
233
+ elif _FORMAT_ATTR_RE.search(v):
234
+ bad = "format string performs attribute access (introspection escape)"
235
+ if bad:
236
+ violations.append({
237
+ "kind": "string", "name": v[:40],
238
+ "line": node.lineno, "col": node.col_offset,
239
+ "reason": bad,
240
+ })
241
+
242
+ return violations
243
+
244
+
245
+ def _has_execute(tree: ast.Module) -> bool:
246
+ for node in tree.body:
247
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == EXECUTE_FN_NAME:
248
+ return True
249
+ return False
250
+
251
+
252
+ def compile_code_node(
253
+ code: str,
254
+ node_id: str,
255
+ *,
256
+ screen: bool = True,
257
+ allowed_imports: Optional[Set[str]] = None,
258
+ ) -> CodeScreenResult:
259
+ """Parse, security-screen, and compile CodeNode source WITHOUT executing it.
260
+
261
+ Pipeline:
262
+ 1. `ast.parse` — SyntaxError → CODE_NODE_SYNTAX_ERROR (line/offset).
263
+ 2. AST denylist scan (when `screen=True`) — CODE_NODE_FORBIDDEN.
264
+ 3. `execute` function must be defined — CODE_NODE_NO_EXECUTE.
265
+ 4. `compile(filename=f'<code:{node_id}>')` — preserves traceback lines.
266
+
267
+ Args:
268
+ code: Python source text (must define `execute`).
269
+ node_id: node id, used only for the compiled filename (traceback anchor).
270
+ screen: run the AST denylist scan (always True in production).
271
+ allowed_imports: override the import whitelist (defaults to
272
+ DEFAULT_ALLOWED_IMPORTS). Enforced both here and by the runtime
273
+ safe `__import__`.
274
+
275
+ Returns:
276
+ CodeScreenResult — `ok=True` with `code_object`, or `ok=False` with a
277
+ structured, English, chatbot-consumable error. Never raises for
278
+ screening/syntax failures (they are returned as data).
279
+ """
280
+ allowed = set(allowed_imports) if allowed_imports is not None else set(DEFAULT_ALLOWED_IMPORTS)
281
+
282
+ if not isinstance(code, str) or not code.strip():
283
+ return CodeScreenResult(
284
+ ok=False,
285
+ error_code="CODE_NODE_SYNTAX_ERROR",
286
+ message="CodeNode 'code' is empty.",
287
+ suggestion="Provide Python source that defines `async def execute(data, params, context)`.",
288
+ line=1, offset=0,
289
+ )
290
+
291
+ # 1. Parse
292
+ try:
293
+ tree = ast.parse(code, filename=f"<code:{node_id}>", mode="exec")
294
+ except SyntaxError as e:
295
+ return CodeScreenResult(
296
+ ok=False,
297
+ error_code="CODE_NODE_SYNTAX_ERROR",
298
+ message=f"CodeNode syntax error: {e.msg}",
299
+ suggestion="Fix the Python syntax at the reported line/offset.",
300
+ line=e.lineno,
301
+ offset=e.offset,
302
+ details={"raw": str(e)},
303
+ )
304
+
305
+ # 2. Screen
306
+ if screen:
307
+ violations = _scan_ast(tree, allowed)
308
+ if violations:
309
+ first = violations[0]
310
+ names = sorted({v["name"] for v in violations})
311
+ return CodeScreenResult(
312
+ ok=False,
313
+ error_code="CODE_NODE_FORBIDDEN",
314
+ message=(
315
+ f"CodeNode uses forbidden construct(s): {', '.join(names)} "
316
+ f"({first['reason']})."
317
+ ),
318
+ suggestion=(
319
+ "Remove the blocked import/builtin/introspection. CodeNode allows "
320
+ "pure-computation stdlib only (math, statistics, json, datetime, ...); "
321
+ "credentials, filesystem, network, subprocess, and object-graph "
322
+ "introspection are not available."
323
+ ),
324
+ line=first["line"],
325
+ offset=first["col"],
326
+ details={"violations": violations, "forbidden_names": names},
327
+ )
328
+
329
+ # 3. execute() must exist
330
+ if not _has_execute(tree):
331
+ return CodeScreenResult(
332
+ ok=False,
333
+ error_code="CODE_NODE_NO_EXECUTE",
334
+ message=f"CodeNode code must define a function named '{EXECUTE_FN_NAME}'.",
335
+ suggestion=(
336
+ "Define `async def execute(data, params, context):` (a plain "
337
+ "`def execute(...)` is also accepted and auto-wrapped)."
338
+ ),
339
+ line=1, offset=0,
340
+ )
341
+
342
+ # 4. Compile (never exec here)
343
+ try:
344
+ code_object = compile(tree, filename=f"<code:{node_id}>", mode="exec")
345
+ except (SyntaxError, ValueError) as e: # e.g. too-deeply-nested
346
+ return CodeScreenResult(
347
+ ok=False,
348
+ error_code="CODE_NODE_SYNTAX_ERROR",
349
+ message=f"CodeNode failed to compile: {e}",
350
+ suggestion="Simplify the code so it compiles as a module.",
351
+ line=getattr(e, "lineno", 1) or 1,
352
+ offset=getattr(e, "offset", 0) or 0,
353
+ details={"raw": str(e)},
354
+ )
355
+
356
+ return CodeScreenResult(ok=True, code_object=code_object)
357
+
358
+
359
+ # The real import machinery, captured once so the sandbox can delegate to it
360
+ # after the whitelist check.
361
+ _REAL_IMPORT = _builtins.__import__
362
+
363
+ # Curated safe builtins — everything a pure-compute function needs, and nothing
364
+ # that grants escape (no eval/exec/compile/open/getattr/setattr/globals/...).
365
+ _SAFE_BUILTIN_NAMES: Tuple[str, ...] = (
366
+ "abs", "all", "any", "ascii", "bin", "bool", "bytearray", "bytes",
367
+ "callable", "chr", "complex", "dict", "divmod", "enumerate", "filter",
368
+ "float", "format", "frozenset", "hash", "hex", "int", "isinstance",
369
+ "issubclass", "iter", "len", "list", "map", "max", "min", "next", "oct",
370
+ "ord", "pow", "print", "range", "repr", "reversed", "round", "set",
371
+ "slice", "sorted", "str", "sum", "tuple", "type", "zip", "hasattr",
372
+ # exceptions & sentinels
373
+ "Exception", "BaseException", "ValueError", "TypeError", "KeyError",
374
+ "IndexError", "AttributeError", "ZeroDivisionError", "ArithmeticError",
375
+ "OverflowError", "RuntimeError", "StopIteration", "StopAsyncIteration",
376
+ "NotImplementedError", "AssertionError", "LookupError", "NameError",
377
+ "RecursionError", "FloatingPointError", "GeneratorExit", "KeyboardInterrupt",
378
+ "True", "False", "None", "NotImplemented", "Ellipsis",
379
+ "object", "property", "staticmethod", "classmethod", "super",
380
+ # __build_class__ is required for `class` statements in user code; it is not
381
+ # itself an escape vector (dunder screen blocks the dangerous follow-ups).
382
+ "__build_class__",
383
+ )
384
+
385
+
386
+ def build_restricted_builtins(allowed_imports: Optional[Set[str]] = None) -> Dict[str, Any]:
387
+ """Build the `__builtins__` dict used when exec-ing CodeNode source.
388
+
389
+ Contains only the curated safe builtins plus a whitelist-gated
390
+ `__import__`. Pure function (no exec, stdlib only) so it lives in core and
391
+ is reused by the runtime worker.
392
+ """
393
+ allowed = set(allowed_imports) if allowed_imports is not None else set(DEFAULT_ALLOWED_IMPORTS)
394
+
395
+ def _safe_import(name, globals=None, locals=None, fromlist=(), level=0):
396
+ if level and level != 0:
397
+ raise ImportError("relative imports are not allowed in CodeNode")
398
+ root = _module_root(name)
399
+ if root not in allowed:
400
+ raise ImportError(
401
+ f"import of '{name}' is not allowed in CodeNode "
402
+ f"(allowed: pure-computation stdlib only)"
403
+ )
404
+ return _REAL_IMPORT(name, globals, locals, fromlist, level)
405
+
406
+ safe: Dict[str, Any] = {}
407
+ for bn in _SAFE_BUILTIN_NAMES:
408
+ if hasattr(_builtins, bn):
409
+ safe[bn] = getattr(_builtins, bn)
410
+ safe["__import__"] = _safe_import
411
+ return safe
@@ -54,6 +54,8 @@
54
54
  "nodes.CandlestickChartNode.name": "Candlestick Chart",
55
55
  "nodes.ConditionNode.description": "Evaluates trading conditions using one of 67 community strategy plugins (RSI, MACD, Bollinger Bands, Ichimoku, ZScore, TurtleBreakout, MagicFormula, SupportResistanceLevels, etc.). Feed OHLCV or price data through the items field with from/extract mapping, and select a plugin with optional parameters. Outputs is_condition_met (boolean), passed_symbols, failed_symbols, and calculated indicator values. Auto-iterates over input symbol arrays. Bind positions field for sell-side conditions (profit target, stop loss). Typical connection: HistoricalDataNode → ConditionNode → LogicNode or → NewOrderNode. Can be used as an AI Agent tool.",
56
56
  "nodes.ConditionNode.name": "Condition",
57
+ "nodes.CodeNode.name": "Code",
58
+ "nodes.CodeNode.description": "Runs a custom Python function for logic no existing typed node covers — a bespoke indicator, a custom scoring formula, ad-hoc reshaping. The code defines async def execute(data, params, context) and returns a dict mapped to declared output ports (or a single 'result' port). Always sandboxed: it runs in a credential-free subprocess with restricted builtins, an AST denylist (no os/socket/urllib/subprocess/open or introspection), and a scrubbed read-only context — no credential, broker, or network access. Bind the whole upstream value to 'data' and loop in-code. Downstream nodes consume its outputs by writing {{ nodes.<id>.<port> }} into a generic input field (TableDisplayNode.data, IfNode.left/right, ...), not by typed-port matching — so when feeding a typed order/condition node, return the standard [{symbol, exchange, ...}] shape; for a display/sink or If scalar, any shape works. Typical connection: HistoricalDataNode → CodeNode → IfNode. Prefer typed nodes (ConditionNode/FieldMappingNode/IfNode) whenever they already cover the logic.",
57
59
  "nodes.CurrencyRateNode.description": "Fetches real-time currency exchange rates based on European Central Bank (ECB) data. No broker connection or API key required. Configure a base currency and target currencies to get conversion rates. Includes a convenience KRW rate output port for Korean investors tracking USD/KRW. Built-in resilience with retry on timeout and rate-limit errors, plus fallback to open.er-api.com. Typical connection: CurrencyRateNode → ConditionNode (for FX-based signals) or → FieldMappingNode. Can be used as an AI Agent tool.",
58
60
  "nodes.CurrencyRateNode.name": "Currency Rate",
59
61
  "nodes.ErrorHandlerNode.description": "Handles errors from upstream nodes and executes recovery logic. Routes error data for logging, alerting, or fallback processing. Use to build resilient workflows that gracefully handle API failures, timeout errors, or invalid data.",
@@ -288,6 +290,10 @@
288
290
  "fields.CurrencyRateNode.resilience": "Retry and fallback settings for API call failures",
289
291
  "fields.CurrencyRateNode.target_currencies": "Target currencies. Select currencies to get exchange rates against the base currency.",
290
292
  "fields.CurrencyRateNode.timeout_seconds": "API request timeout (seconds). Default 30s, adjust based on your network conditions.",
293
+ "fields.CodeNode.code": "Python source that defines async def execute(data, params, context). Sandboxed: only pure-computation stdlib imports (math, statistics, json, datetime, ...) are allowed; no credentials, network, filesystem, or introspection.",
294
+ "fields.CodeNode.outputs": "Output port declarations [{name, type}]. Leave empty for a single 'result' port carrying the whole return value. Declared ports are consumed downstream by {{ nodes.<id>.<port> }} expressions written into a consumer's generic input field (TableDisplayNode.data, IfNode.left/right, ...), not by typed-port matching; declaring them enables static typo-guarding.",
295
+ "fields.CodeNode.params": "Parameters passed to execute() as `params`. Expression bindings are allowed (e.g. a period or threshold).",
296
+ "fields.CodeNode.data": "Input data passed to execute() as `data`. Bind the whole upstream value (e.g. {{ nodes.hist.values }}) and loop over it inside the function.",
291
297
  "fields.FieldMappingNode.data": "Input data to transform. Bind from HTTPRequestNode response or other node outputs.",
292
298
  "fields.FieldMappingNode.mappings": "Field name mapping table. Converts source field names (from) to standard field names (to).",
293
299
  "fields.FieldMappingNode.mappings.description": "Field description (for AI agent auto-mapping)",
@@ -847,6 +853,7 @@
847
853
  "fieldNames.TradingHoursFilterNode.timezone": "Timezone",
848
854
  "fieldNames.WatchlistNode.symbols": "Symbols",
849
855
  "outputs.AIAgentNode.response": "AI agent response (text or JSON object depending on output format)",
856
+ "outputs.CodeNode.result": "The whole value returned by execute() when no output ports are declared. When outputs are declared, read the named ports instead.",
850
857
  "outputs.ExclusionListNode.count": "Number of excluded symbols",
851
858
  "outputs.ExclusionListNode.excluded": "Final exclusion list (manual + dynamic, deduplicated)",
852
859
  "outputs.ExclusionListNode.filtered": "Source symbols minus excluded symbols (when input_symbols connected)",
@@ -54,6 +54,8 @@
54
54
  "nodes.CandlestickChartNode.name": "캔들스틱차트",
55
55
  "nodes.ConditionNode.description": "67개 커뮤니티 전략 플러그인(RSI, MACD, Bollinger Bands, Ichimoku, ZScore, TurtleBreakout, MagicFormula, SupportResistanceLevels 등)으로 매매 조건을 평가합니다. items 필드의 from/extract 매핑으로 OHLCV 또는 가격 데이터를 입력하고, 플러그인과 매개변수를 선택합니다. 출력: is_condition_met (boolean), passed_symbols, failed_symbols, 계산된 지표 값. 입력 종목 배열에 대해 자동 반복(auto-iterate) 실행합니다. 매도 조건에는 positions 필드를 바인딩하세요(익절, 손절). 일반적인 연결: HistoricalDataNode → ConditionNode → LogicNode 또는 → NewOrderNode. AI Agent 도구로 사용 가능.",
56
56
  "nodes.ConditionNode.name": "조건",
57
+ "nodes.CodeNode.name": "코드",
58
+ "nodes.CodeNode.description": "기존 타입 노드로 표현할 수 없는 커스텀 로직(고유 지표, 커스텀 점수 산식, 임시 데이터 가공)을 파이썬 함수로 실행합니다. 코드는 async def execute(data, params, context) 를 정의하고, 반환한 dict 가 선언한 출력 포트(또는 단일 'result' 포트)로 매핑됩니다. 항상 샌드박스 실행: 앱키 없는 자식 프로세스, 제한된 builtins, AST 차단목록(os/socket/urllib/subprocess/open·introspection 불가), 스크럽된 읽기전용 context — credential·broker·네트워크 접근 없음. 상류 값 전체를 'data' 에 바인딩해 코드 안에서 반복하세요. 다운스트림은 소비자 노드의 범용 입력 필드(TableDisplayNode.data, IfNode.left/right 등)에 {{ nodes.<id>.<port> }} 를 써서 출력을 소비합니다 — 타입드 포트 매칭이 아님. 따라서 타입드 주문/조건 노드로 흘릴 땐 표준 [{symbol, exchange, ...}] 모양을 반환하고, 디스플레이/싱크나 If 스칼라엔 아무 모양이나 됩니다. 일반적 연결: HistoricalDataNode → CodeNode → IfNode. 타입 노드(ConditionNode/FieldMappingNode/IfNode)로 가능한 로직은 그쪽을 우선하세요.",
57
59
  "nodes.CurrencyRateNode.description": "유럽중앙은행(ECB) 기반 실시간 환율을 조회합니다. 브로커 연결이나 API 키가 필요 없습니다. 기준 통화와 대상 통화를 설정하여 환율을 가져옵니다. 한국 투자자를 위한 USD/KRW 환율 전용 출력 포트를 제공합니다. 타임아웃 및 속도 제한 오류 시 자동 재시도, open.er-api.com 폴백 기능 내장. 일반적인 연결: CurrencyRateNode → ConditionNode (환율 기반 시그널) 또는 → FieldMappingNode. AI Agent 도구로 사용 가능.",
58
60
  "nodes.CurrencyRateNode.name": "환율 조회",
59
61
  "nodes.ErrorHandlerNode.description": "상위 노드의 에러를 처리하고 복구 로직을 실행합니다. 에러 데이터를 로깅, 알림, 또는 폴백 처리로 라우팅합니다. API 장애, 타임아웃 에러, 유효하지 않은 데이터를 우아하게 처리하는 견고한 워크플로우를 구축하세요.",
@@ -288,6 +290,10 @@
288
290
  "fields.CurrencyRateNode.resilience": "API 호출 실패 시 재시도 및 폴백 설정",
289
291
  "fields.CurrencyRateNode.target_currencies": "대상 통화 목록. 기준 통화 대비 환율을 조회할 통화를 선택하세요.",
290
292
  "fields.CurrencyRateNode.timeout_seconds": "API 요청 타임아웃 (초). 기본 30초, 네트워크 환경에 따라 조정하세요.",
293
+ "fields.CodeNode.code": "async def execute(data, params, context) 를 정의하는 파이썬 소스. 샌드박스: 순수 계산용 표준 라이브러리(math, statistics, json, datetime, ...) import 만 허용, credential·네트워크·파일시스템·introspection 불가.",
294
+ "fields.CodeNode.outputs": "출력 포트 선언 [{name, type}]. 비워두면 반환값 전체를 담는 단일 'result' 포트가 됩니다. 선언한 포트는 소비자 노드의 범용 입력 필드(TableDisplayNode.data, IfNode.left/right 등)에 {{ nodes.<id>.<port> }} 표현식으로 소비됩니다 — 타입드 포트 매칭이 아니라 이름으로 dict 키를 꺼내는 방식. 선언하면 정적 오타 검증이 적용됩니다.",
295
+ "fields.CodeNode.params": "execute() 에 `params` 로 전달되는 파라미터. 표현식 바인딩 허용(예: 기간·임계값).",
296
+ "fields.CodeNode.data": "execute() 에 `data` 로 전달되는 입력 데이터. 상류 값 전체를 바인딩(예: {{ nodes.hist.values }})하고 함수 안에서 반복하세요.",
291
297
  "fields.FieldMappingNode.data": "변환할 입력 데이터. HTTPRequestNode 응답이나 다른 노드 출력을 바인딩하세요.",
292
298
  "fields.FieldMappingNode.mappings": "필드명 매핑 테이블. 원본 필드명(from)을 표준 필드명(to)으로 변환합니다.",
293
299
  "fields.FieldMappingNode.mappings.description": "필드 설명 (AI 에이전트 자동 매핑용)",
@@ -847,6 +853,7 @@
847
853
  "fieldNames.TradingHoursFilterNode.timezone": "시간대",
848
854
  "fieldNames.WatchlistNode.symbols": "종목",
849
855
  "outputs.AIAgentNode.response": "AI 에이전트 응답 (출력 형식에 따라 텍스트 또는 JSON 객체)",
856
+ "outputs.CodeNode.result": "출력 포트를 선언하지 않았을 때 execute() 가 반환한 값 전체. 출력 포트를 선언하면 해당 포트를 대신 읽으세요.",
850
857
  "outputs.ExclusionListNode.count": "제외 종목 수",
851
858
  "outputs.ExclusionListNode.excluded": "최종 제외 종목 목록 (수동 + 동적 합산, 중복 제거)",
852
859
  "outputs.ExclusionListNode.filtered": "원본 종목에서 제외 종목을 뺀 결과 (input_symbols 연결 시)",
@@ -31,12 +31,16 @@ class ErrorCode(str, Enum):
31
31
 
32
32
  # Node / plugin registry
33
33
  UNKNOWN_NODE_TYPE = "UNKNOWN_NODE_TYPE"
34
- UNKNOWN_DYNAMIC_NODE_SCHEMA = "UNKNOWN_DYNAMIC_NODE_SCHEMA"
35
- DYNAMIC_NODE_CREDENTIAL_FORBIDDEN = "DYNAMIC_NODE_CREDENTIAL_FORBIDDEN"
36
- DYNAMIC_NODE_CLASS_NOT_INJECTED = "DYNAMIC_NODE_CLASS_NOT_INJECTED"
37
34
  MISSING_PLUGIN = "MISSING_PLUGIN"
38
35
  UNKNOWN_PLUGIN = "UNKNOWN_PLUGIN"
39
36
 
37
+ # CodeNode (custom Python code node)
38
+ CODE_NODE_SYNTAX_ERROR = "CODE_NODE_SYNTAX_ERROR" # ast.parse / compile failed (line/offset)
39
+ CODE_NODE_FORBIDDEN = "CODE_NODE_FORBIDDEN" # AST denylist violation (blocked import/builtin/dunder)
40
+ CODE_NODE_NO_EXECUTE = "CODE_NODE_NO_EXECUTE" # code does not define execute()
41
+ CODE_NODE_EXEC_ERROR = "CODE_NODE_EXEC_ERROR" # runtime exception / worker crash / timeout / bad serialization
42
+ CODE_NODE_DISABLED = "CODE_NODE_DISABLED" # allow_code_node=False environment
43
+
40
44
  # Edges
41
45
  INVALID_EDGE_REF = "INVALID_EDGE_REF"
42
46
  INVALID_EDGE_PORT = "INVALID_EDGE_PORT"
@@ -48,6 +48,7 @@ from programgarden_core.nodes.symbol_stock import OverseasStockSymbolQueryNode
48
48
  from programgarden_core.nodes.symbol_futures import OverseasFuturesSymbolQueryNode
49
49
  # Data (상품 무관)
50
50
  from programgarden_core.nodes.data import SQLiteNode, HTTPRequestNode, FieldMappingNode
51
+ from programgarden_core.nodes.code import CodeNode
51
52
  # Market External (credential 불필요, 무료 외부 API)
52
53
  from programgarden_core.nodes.market_external import CurrencyRateNode
53
54
  # Market Status (JIF 장운영정보 — broker credential agnostic)
@@ -146,6 +147,7 @@ __all__ = [
146
147
  "SQLiteNode",
147
148
  "HTTPRequestNode",
148
149
  "FieldMappingNode",
150
+ "CodeNode",
149
151
  # Market External (credential 불필요)
150
152
  "CurrencyRateNode",
151
153
  # Market Status (JIF 장운영정보)
@@ -124,7 +124,7 @@ class OverseasFuturesAccountNode(BaseNode):
124
124
  {"id": "start", "type": "StartNode"},
125
125
  {"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "futures_cred", "paper_trading": True},
126
126
  {"id": "account", "type": "OverseasFuturesAccountNode"},
127
- {"id": "if_margin", "type": "IfNode", "left": "{{ nodes.account.balance.orderable_margin }}", "operator": ">=", "right": 10000},
127
+ {"id": "if_margin", "type": "IfNode", "left": "{{ nodes.account.balance.orderable_amount }}", "operator": ">=", "right": 10000},
128
128
  {"id": "order", "type": "OverseasFuturesNewOrderNode", "symbol": "ESH26", "exchange": "CME", "side": "buy", "order_type": "limit", "quantity": 1, "price": 5200.0},
129
129
  ],
130
130
  "edges": [
@@ -144,7 +144,7 @@ class OverseasFuturesAccountNode(BaseNode):
144
144
  }
145
145
  ],
146
146
  },
147
- "expected_output": "When orderable_margin >= 10000: NewOrderNode places a buy limit order. Otherwise the order branch is skipped.",
147
+ "expected_output": "When orderable_amount >= 10000: NewOrderNode places a buy limit order. Otherwise the order branch is skipped.",
148
148
  },
149
149
  ]
150
150
  _node_guide: ClassVar[Dict[str, Any]] = {
@@ -125,7 +125,7 @@ class OverseasStockAccountNode(BaseNode):
125
125
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
126
126
  {"id": "account", "type": "OverseasStockAccountNode"},
127
127
  {"id": "market", "type": "OverseasStockMarketDataNode", "symbol": "{{ item }}"},
128
- {"id": "display", "type": "TableDisplayNode", "title": "Live Prices", "data": "{{ nodes.market.data }}"},
128
+ {"id": "display", "type": "TableDisplayNode", "title": "Live Prices", "data": "{{ nodes.market.values }}"},
129
129
  ],
130
130
  "edges": [
131
131
  {"from": "start", "to": "broker"},
@@ -203,7 +203,7 @@ class BacktestEngineNode(BaseNode):
203
203
  {"id": "start", "type": "StartNode"},
204
204
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
205
205
  {"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbols": [{"symbol": "AAPL", "exchange": "NASDAQ"}], "period": "1d", "count": 252},
206
- {"id": "condition", "type": "ConditionNode", "plugin": "RSI", "data": "{{ nodes.historical.values }}", "period": 14, "oversold": 30, "overbought": 70},
206
+ {"id": "condition", "type": "ConditionNode", "plugin": "RSI", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 14, "threshold": 30, "direction": "below"}},
207
207
  {
208
208
  "id": "backtest",
209
209
  "type": "BacktestEngineNode",
@@ -261,7 +261,7 @@ class BacktestEngineNode(BaseNode):
261
261
  {"id": "start", "type": "StartNode"},
262
262
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
263
263
  {"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbols": [{"symbol": "SPY", "exchange": "NYSE"}], "period": "1d", "count": 504},
264
- {"id": "condition", "type": "ConditionNode", "plugin": "MACD", "data": "{{ nodes.historical.values }}"},
264
+ {"id": "condition", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
265
265
  {
266
266
  "id": "backtest",
267
267
  "type": "BacktestEngineNode",
@@ -748,8 +748,8 @@ class BenchmarkCompareNode(BaseNode):
748
748
  {"id": "start", "type": "StartNode"},
749
749
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
750
750
  {"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbols": [{"symbol": "SPY", "exchange": "NYSE"}], "period": "1d", "count": 252},
751
- {"id": "rsi_cond", "type": "ConditionNode", "plugin": "RSI", "data": "{{ nodes.historical.values }}", "period": 14, "oversold": 30, "overbought": 70},
752
- {"id": "macd_cond", "type": "ConditionNode", "plugin": "MACD", "data": "{{ nodes.historical.values }}"},
751
+ {"id": "rsi_cond", "type": "ConditionNode", "plugin": "RSI", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 14, "threshold": 30, "direction": "below"}},
752
+ {"id": "macd_cond", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
753
753
  {
754
754
  "id": "backtest_rsi",
755
755
  "type": "BacktestEngineNode",
@@ -807,7 +807,7 @@ class BenchmarkCompareNode(BaseNode):
807
807
  {"id": "start", "type": "StartNode"},
808
808
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
809
809
  {"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbols": [{"symbol": "QQQ", "exchange": "NASDAQ"}], "period": "1d", "count": 252},
810
- {"id": "condition", "type": "ConditionNode", "plugin": "MACD", "data": "{{ nodes.historical.values }}"},
810
+ {"id": "condition", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
811
811
  {
812
812
  "id": "backtest",
813
813
  "type": "BacktestEngineNode",
@@ -121,7 +121,7 @@ class OverseasFuturesHistoricalDataNode(BaseNode):
121
121
  {"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
122
122
  {"id": "split", "type": "SplitNode", "items": [{"symbol": "ESH26", "exchange": "CME"}]},
123
123
  {"id": "historical", "type": "OverseasFuturesHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(60, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "1d", "adjust": False},
124
- {"id": "condition", "type": "ConditionNode", "plugin": "BollingerBands", "data": "{{ nodes.historical.value.time_series }}"},
124
+ {"id": "condition", "type": "ConditionNode", "plugin": "BollingerBands", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 20, "std": 2.0, "direction": "below_lower"}},
125
125
  ],
126
126
  "edges": [
127
127
  {"from": "start", "to": "broker"},
@@ -154,7 +154,7 @@ class OverseasFuturesHistoricalDataNode(BaseNode):
154
154
  {"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
155
155
  {"id": "split", "type": "SplitNode", "items": [{"symbol": "MHIH26", "exchange": "HKEX"}]},
156
156
  {"id": "historical", "type": "OverseasFuturesHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(10, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "1h", "adjust": False},
157
- {"id": "condition", "type": "ConditionNode", "plugin": "MACD", "data": "{{ nodes.historical.value.time_series }}"},
157
+ {"id": "condition", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
158
158
  ],
159
159
  "edges": [
160
160
  {"from": "start", "to": "broker"},