programgarden-core 1.15.2__tar.gz → 1.17.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/PKG-INFO +4 -4
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/__init__.py +1 -0
- programgarden_core-1.17.0/programgarden_core/code_node.py +411 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/exceptions.py +39 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/i18n/locales/en.json +25 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/i18n/locales/ko.json +25 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/validation.py +7 -3
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/__init__.py +2 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/account_futures.py +2 -2
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/account_stock.py +1 -1
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/backtest.py +5 -5
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/backtest_futures.py +2 -2
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/backtest_korea_stock.py +2 -2
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/backtest_stock.py +2 -2
- programgarden_core-1.17.0/programgarden_core/nodes/code.py +383 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/data.py +5 -5
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/display.py +40 -24
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/order.py +15 -14
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/realtime_futures.py +22 -3
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/realtime_korea_stock.py +22 -3
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/realtime_stock.py +22 -3
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/risk.py +64 -30
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/symbol.py +6 -4
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/registry/__init__.py +0 -12
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/registry/node_registry.py +2 -2
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/pyproject.toml +1 -1
- programgarden_core-1.15.2/programgarden_core/registry/dynamic_node_registry.py +0 -409
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/README.md +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/bases/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/bases/client.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/bases/components.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/bases/listener.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/bases/mixins.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/bases/products.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/bases/sql.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/bases/storage.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/expression/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/expression/evaluator.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/i18n/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/i18n/translator.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/korea_alias.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/connection_rule.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/credential.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/edge.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/event.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/exchange.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/field_binding.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/job.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/order_diagnostics.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/plugin_resource.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/resilience.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/resource.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/models/workflow.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/account_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/ai.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/base.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/broker.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/calculation.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/condition.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/data_futures.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/data_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/data_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/event.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/fundamental_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/infra.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/market_external.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/market_status.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/open_orders_futures.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/open_orders_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/portfolio.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/symbol_futures.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/symbol_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/nodes/trigger.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/presets/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/presets/news_analyst.json +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/presets/risk_manager.json +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/presets/strategist.json +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/presets/technical_analyst.json +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/registry/credential_registry.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/registry/plugin_registry.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/retry_executor.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: programgarden-core
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.17.0
|
|
4
4
|
Summary: ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의
|
|
5
|
-
License
|
|
5
|
+
License: AGPL-3.0-or-later
|
|
6
6
|
Author: 프로그램동산
|
|
7
7
|
Author-email: coding@programgarden.com
|
|
8
8
|
Requires-Python: >=3.12
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
9
10
|
Classifier: Programming Language :: Python :: 3
|
|
10
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
13
13
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
|
|
@@ -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
|
|
@@ -152,6 +152,45 @@ class ConditionEvaluationError(ExecutionError):
|
|
|
152
152
|
super().__init__(message, node_id=node_id, details=details)
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
class MissingDependencyError(ExecutionError):
|
|
156
|
+
"""선택적 heavy 의존성(extra) 미설치/부분설치 — silent no-op 금지, 명시 에러.
|
|
157
|
+
|
|
158
|
+
Raised by community plugins/nodes that lazily import an optional heavy
|
|
159
|
+
dependency shipped behind a pyproject extra (e.g. ``portfolio`` →
|
|
160
|
+
pyportfolioopt, ``perf`` → quantstats). Importing must fail loud with a
|
|
161
|
+
concrete install hint instead of silently no-op'ing.
|
|
162
|
+
|
|
163
|
+
``transitive=True`` distinguishes the case where the extra itself is
|
|
164
|
+
installed but one of its transitive dependencies is missing/broken
|
|
165
|
+
(e.g. quantstats present but ``pkg_resources``/matplotlib absent) from
|
|
166
|
+
the plain "extra not installed at all" case (``transitive=False``).
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
message: str,
|
|
172
|
+
extra: Optional[str] = None,
|
|
173
|
+
package: Optional[str] = None,
|
|
174
|
+
install_hint: Optional[str] = None,
|
|
175
|
+
transitive: bool = False,
|
|
176
|
+
node_id: Optional[str] = None,
|
|
177
|
+
details: Optional[dict] = None,
|
|
178
|
+
):
|
|
179
|
+
d = {
|
|
180
|
+
"code": "MISSING_OPTIONAL_DEPENDENCY",
|
|
181
|
+
"extra": extra,
|
|
182
|
+
"package": package,
|
|
183
|
+
"install_hint": install_hint,
|
|
184
|
+
"transitive": transitive,
|
|
185
|
+
}
|
|
186
|
+
d.update(details or {})
|
|
187
|
+
super().__init__(message, node_id=node_id, details=d)
|
|
188
|
+
self.extra = extra
|
|
189
|
+
self.package = package
|
|
190
|
+
self.install_hint = install_hint
|
|
191
|
+
self.transitive = transitive
|
|
192
|
+
|
|
193
|
+
|
|
155
194
|
class DuplicateJobIdError(ProgramGardenError):
|
|
156
195
|
"""중복된 Job ID 오류
|
|
157
196
|
|
{programgarden_core-1.15.2 → programgarden_core-1.17.0}/programgarden_core/i18n/locales/en.json
RENAMED
|
@@ -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.",
|
|
@@ -64,6 +66,8 @@
|
|
|
64
66
|
"nodes.FearGreedIndexNode.name": "Fear & Greed Index",
|
|
65
67
|
"nodes.FundamentalDataNode.description": "Fetches company financial data via Financial Modeling Prep (FMP) API. Supports four data types: company profile, income statement, balance sheet, and key metrics. Configure period (annual/quarter) and limit (number of periods to fetch). Requires FMP API credentials. Outputs financial data array and query summary. Typical connection: WatchlistNode → FundamentalDataNode → ConditionNode (for fundamental screening) or → TableDisplayNode.",
|
|
66
68
|
"nodes.FundamentalDataNode.name": "Fundamental Data (FMP)",
|
|
69
|
+
"nodes.PerformanceReportNode.name": "Performance Report (quantstats)",
|
|
70
|
+
"nodes.PerformanceReportNode.description": "Computes risk/return metrics (Sharpe, Sortino, max drawdown, CAGR, annualized volatility, Calmar) from an equity curve, price series, or return series using quantstats. Optionally computes beta and alpha against a benchmark series. Emits a drawdown time series and a formatted text report, and runs headless via the matplotlib Agg backend. Requires the optional 'perf' extra (pip install 'programgarden-community[perf]'); a missing or partially-installed extra raises a structured MissingDependencyError. Typical connection: BacktestEngineNode → PerformanceReportNode → TableDisplayNode.",
|
|
67
71
|
"nodes.FieldMappingNode.description": "Renames field names in data objects to a standard format. Use to normalize external API responses so downstream nodes can access data with consistent field names (e.g., rename 'adj_close' to 'close', 'vol' to 'volume'). Configure mappings as from/to pairs. Outputs mapped_data with renamed fields. Set preserve_unmapped=true to keep original fields alongside renamed ones. Note: this node only renames fields — it does not generate or transform data values. Typical connection: HTTPRequestNode → FieldMappingNode → ConditionNode.",
|
|
68
72
|
"nodes.FieldMappingNode.name": "Field Mapping",
|
|
69
73
|
"nodes.FileReaderNode.description": "Reads PDF, TXT, CSV, JSON, MD, DOCX, and XLSX files and converts them to text and structured data. Supports batch processing of multiple files via file paths or base64 data input. The node handles parsing only — file sourcing (uploads, S3, HTTP, etc.) is handled externally. PDF: page range selection, table extraction support. CSV: configurable delimiter and header. XLSX: sheet selection. Security: only /app/data/ paths allowed, file size limits enforced. Outputs text array, data array, and metadata array (auto-iterate compatible). Typical connection: HTTPRequestNode → FileReaderNode → AIAgentNode (document analysis) or → ConditionNode. Can be used as an AI Agent tool.",
|
|
@@ -288,6 +292,10 @@
|
|
|
288
292
|
"fields.CurrencyRateNode.resilience": "Retry and fallback settings for API call failures",
|
|
289
293
|
"fields.CurrencyRateNode.target_currencies": "Target currencies. Select currencies to get exchange rates against the base currency.",
|
|
290
294
|
"fields.CurrencyRateNode.timeout_seconds": "API request timeout (seconds). Default 30s, adjust based on your network conditions.",
|
|
295
|
+
"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.",
|
|
296
|
+
"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.",
|
|
297
|
+
"fields.CodeNode.params": "Parameters passed to execute() as `params`. Expression bindings are allowed (e.g. a period or threshold).",
|
|
298
|
+
"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
299
|
"fields.FieldMappingNode.data": "Input data to transform. Bind from HTTPRequestNode response or other node outputs.",
|
|
292
300
|
"fields.FieldMappingNode.mappings": "Field name mapping table. Converts source field names (from) to standard field names (to).",
|
|
293
301
|
"fields.FieldMappingNode.mappings.description": "Field description (for AI agent auto-mapping)",
|
|
@@ -317,6 +325,12 @@
|
|
|
317
325
|
"fields.FundamentalDataNode.limit": "Number of periods to fetch (most recent N). Default 1.",
|
|
318
326
|
"fields.FundamentalDataNode.timeout_seconds": "API request timeout (seconds). Default 30s.",
|
|
319
327
|
"fields.FundamentalDataNode.resilience": "Retry and fallback settings for API call failures",
|
|
328
|
+
"fields.PerformanceReportNode.data": "Input series: an array of numbers or of {date, <value_field>} objects (equity curve, price levels, or periodic returns).",
|
|
329
|
+
"fields.PerformanceReportNode.data_kind": "Kind of the input series: 'equity', 'prices', or 'returns'. Level series ('equity'/'prices') are converted to returns internally.",
|
|
330
|
+
"fields.PerformanceReportNode.value_field": "Key holding the numeric value when rows are objects (default 'close'; falls back to value/equity/return).",
|
|
331
|
+
"fields.PerformanceReportNode.benchmark": "Optional benchmark series (same format as data). When provided, beta and alpha are added to metrics.",
|
|
332
|
+
"fields.PerformanceReportNode.periods_per_year": "Annualization factor (252 for daily, 12 for monthly data). Default 252.",
|
|
333
|
+
"fields.PerformanceReportNode.risk_free_rate": "Annual risk-free rate as a fraction (e.g. 0.02 for 2%) for Sharpe/Sortino. Default 0.",
|
|
320
334
|
"fields.HTTPRequestNode.body": "Request body for POST/PUT/PATCH. Automatically serialized to JSON.",
|
|
321
335
|
"fields.HTTPRequestNode.credential_id": "Credential ID for authentication. Supports: http_bearer, http_header, http_basic, http_query",
|
|
322
336
|
"fields.HTTPRequestNode.headers": "HTTP headers. Click + to add custom headers (e.g., Content-Type, X-API-Key).",
|
|
@@ -646,6 +660,12 @@
|
|
|
646
660
|
"fieldNames.FundamentalDataNode.limit": "Limit",
|
|
647
661
|
"fieldNames.FundamentalDataNode.timeout_seconds": "Timeout (sec)",
|
|
648
662
|
"fieldNames.FundamentalDataNode.resilience": "Resilience",
|
|
663
|
+
"fieldNames.PerformanceReportNode.data": "Data Series",
|
|
664
|
+
"fieldNames.PerformanceReportNode.data_kind": "Data Kind",
|
|
665
|
+
"fieldNames.PerformanceReportNode.value_field": "Value Field",
|
|
666
|
+
"fieldNames.PerformanceReportNode.benchmark": "Benchmark",
|
|
667
|
+
"fieldNames.PerformanceReportNode.periods_per_year": "Periods / Year",
|
|
668
|
+
"fieldNames.PerformanceReportNode.risk_free_rate": "Risk-Free Rate",
|
|
649
669
|
"fieldNames.HTTPRequestNode.body": "Request Body",
|
|
650
670
|
"fieldNames.HTTPRequestNode.credential_id": "Credential",
|
|
651
671
|
"fieldNames.HTTPRequestNode.headers": "Headers",
|
|
@@ -847,6 +867,7 @@
|
|
|
847
867
|
"fieldNames.TradingHoursFilterNode.timezone": "Timezone",
|
|
848
868
|
"fieldNames.WatchlistNode.symbols": "Symbols",
|
|
849
869
|
"outputs.AIAgentNode.response": "AI agent response (text or JSON object depending on output format)",
|
|
870
|
+
"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
871
|
"outputs.ExclusionListNode.count": "Number of excluded symbols",
|
|
851
872
|
"outputs.ExclusionListNode.excluded": "Final exclusion list (manual + dynamic, deduplicated)",
|
|
852
873
|
"outputs.ExclusionListNode.filtered": "Source symbols minus excluded symbols (when input_symbols connected)",
|
|
@@ -864,6 +885,10 @@
|
|
|
864
885
|
"outputs.FearGreedIndexNode.value": "Fear & Greed Index (0~100)",
|
|
865
886
|
"outputs.FundamentalDataNode.data": "Financial data array [{symbol, exchange, ...}]",
|
|
866
887
|
"outputs.FundamentalDataNode.summary": "Query summary {data_type, symbol_count, record_count}",
|
|
888
|
+
"outputs.PerformanceReportNode.metrics": "Metrics object {sharpe, sortino, max_drawdown, cagr, volatility, calmar[, beta, alpha]} (null on insufficient data).",
|
|
889
|
+
"outputs.PerformanceReportNode.report": "Formatted multi-line performance report text.",
|
|
890
|
+
"outputs.PerformanceReportNode.drawdown_series": "Drawdown time series as an array of {date, drawdown}.",
|
|
891
|
+
"outputs.PerformanceReportNode.summary": "Summary {observations, return_points, data_kind, has_benchmark[, error]}.",
|
|
867
892
|
"outputs.LLMModelNode.connection": "LLM connection info (connect to AI Agent node's ai_model port)",
|
|
868
893
|
"outputs.MarketStatusNode.statuses": "Real-time market status list. Each entry: {market: canonical key (e.g. 'US'), jangubun: raw LS code (e.g. '9'), jstatus: raw LS status code (e.g. '21'), jstatus_label: English status label from the internal technical table (e.g. 'Market open'), is_open: bool respecting include_extended_hours, is_regular_open: regular-hours only, is_extended_open: includes pre/after-market, updated_at: ISO8601 timestamp}. Markets that have not received an event yet stay at jstatus='unknown' with is_open=False. The jstatus_label field is English-only (internal technical code table, not UI-translated).",
|
|
869
894
|
"outputs.MarketStatusNode.event": "Most recent status transition event. Shape: {market, jstatus, jstatus_label, prev_jstatus, prev_jstatus_label, transitioned_at}. None until the first event arrives.",
|