programgarden-core 2.0.1__tar.gz → 2.2.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-2.0.1 → programgarden_core-2.2.0}/PKG-INFO +1 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/code_node.py +17 -9
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/expression/evaluator.py +20 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/i18n/locales/en.json +1 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/i18n/locales/ko.json +1 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/account_futures.py +6 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/account_korea_stock.py +7 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/account_stock.py +6 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest_futures.py +7 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest_korea_stock.py +7 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest_stock.py +7 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/base.py +16 -2
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/broker.py +10 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/code.py +24 -8
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/data.py +5 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/data_futures.py +8 -5
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/data_korea_stock.py +8 -4
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/data_stock.py +8 -23
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/display.py +1 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/infra.py +9 -2
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/open_orders_futures.py +7 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/open_orders_korea_stock.py +7 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/open_orders_stock.py +7 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/order.py +63 -2
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/realtime_futures.py +5 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/realtime_korea_stock.py +5 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/realtime_stock.py +5 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/risk.py +1 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/session_gate.py +1 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/symbol.py +9 -5
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/symbol_futures.py +1 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/trigger.py +40 -28
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/registry/node_registry.py +14 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/pyproject.toml +1 -1
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/README.md +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/__init__.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/bases/__init__.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/bases/client.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/bases/components.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/bases/listener.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/bases/mixins.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/bases/products.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/bases/sql.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/bases/storage.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/exceptions.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/expression/__init__.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/i18n/__init__.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/i18n/translator.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/korea_alias.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/__init__.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/connection_rule.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/credential.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/edge.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/event.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/exchange.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/field_binding.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/job.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/order_diagnostics.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/plugin_resource.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/resilience.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/resource.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/validation.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/models/workflow.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/__init__.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/ai.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/calculation.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/condition.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/event.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/fundamental_stock.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/market_external.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/market_status.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/orderable_quantity_futures.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/portfolio.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/symbol_stock.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/presets/__init__.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/presets/news_analyst.json +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/presets/risk_manager.json +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/presets/strategist.json +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/presets/technical_analyst.json +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/registry/__init__.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/registry/credential_registry.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/registry/plugin_registry.py +0 -0
- {programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/retry_executor.py +0 -0
|
@@ -104,9 +104,11 @@ FORBIDDEN_ATTR_NAMES: frozenset = frozenset({
|
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
# ── Denylist: introspection-escape / exfiltration dunders & frame attrs ──────
|
|
107
|
-
# NOTE: enforcement is the
|
|
108
|
-
#
|
|
109
|
-
#
|
|
107
|
+
# NOTE: enforcement is the underscore rule in _scan_ast — any ATTRIBUTE beginning
|
|
108
|
+
# with '_' is rejected (subsumes every dunder and private module re-export like
|
|
109
|
+
# random._os); STRING literals are rejected when they contain a dunder or name a
|
|
110
|
+
# blocked attribute re-export (bare single-underscore data-key strings such as
|
|
111
|
+
# "_partial_failure" are allowed). This explicit set is retained for message clarity.
|
|
110
112
|
FORBIDDEN_DUNDERS: frozenset = frozenset({
|
|
111
113
|
"__class__", "__bases__", "__base__", "__subclasses__", "__mro__",
|
|
112
114
|
"__globals__", "__code__", "__closure__", "__func__", "__self__",
|
|
@@ -224,12 +226,18 @@ def _scan_ast(tree: ast.AST, allowed_imports: Set[str]) -> List[Dict[str, Any]]:
|
|
|
224
226
|
elif isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
225
227
|
v = node.value
|
|
226
228
|
bad = None
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
# A single leading underscore names a documented native data marker
|
|
230
|
+
# read as a dict key (e.g. balance["_partial_failure"], "_source"),
|
|
231
|
+
# so it is allowed. Still rejected: dunder-style names, and blocked
|
|
232
|
+
# public/underscored attribute re-exports ("os"/"sys"/"system"/"_os"/
|
|
233
|
+
# "_sys"... -> in FORBIDDEN_ATTR_NAMES via lstrip("_")), and attribute-
|
|
234
|
+
# walking format fields. getattr/attrgetter/operator/format are
|
|
235
|
+
# independently blocked, so a bare underscore data-key string cannot
|
|
236
|
+
# be turned into attribute access.
|
|
237
|
+
if "__" in v:
|
|
238
|
+
bad = "string literal contains a dunder (introspection escape)"
|
|
239
|
+
elif v.lstrip("_") in FORBIDDEN_ATTR_NAMES:
|
|
240
|
+
bad = f"string literal '{v}' names a blocked attribute (introspection escape)"
|
|
233
241
|
elif _FORMAT_ATTR_RE.search(v):
|
|
234
242
|
bad = "format string performs attribute access (introspection escape)"
|
|
235
243
|
if bad:
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/expression/evaluator.py
RENAMED
|
@@ -56,6 +56,8 @@ def _get_nested_value(obj: Any, path: str) -> Any:
|
|
|
56
56
|
return None
|
|
57
57
|
if isinstance(current, dict):
|
|
58
58
|
current = current.get(key)
|
|
59
|
+
elif isinstance(current, NodeOutputProxy) and isinstance(current._data, dict) and key in current._data:
|
|
60
|
+
current = current._data[key] # port before helper method (see _eval_node)
|
|
59
61
|
else:
|
|
60
62
|
current = getattr(current, key, None)
|
|
61
63
|
return current
|
|
@@ -941,6 +943,15 @@ class SafeEvaluator:
|
|
|
941
943
|
obj = self._eval_node(node.value)
|
|
942
944
|
if isinstance(obj, dict):
|
|
943
945
|
return obj.get(node.attr)
|
|
946
|
+
# An output PORT wins over a proxy helper of the same name: a node
|
|
947
|
+
# whose port is called `count` (OpenOrdersNode), `sum`, `first`, `last`,
|
|
948
|
+
# `map` or `filter` must resolve to the port value, not to the bound
|
|
949
|
+
# helper method (observed 2026-09-24: `{{ nodes.open_orders.count }}`
|
|
950
|
+
# returned `NodeOutputProxy.count` and the CodeNode input could not be
|
|
951
|
+
# serialized — "Object of type method is not JSON serializable").
|
|
952
|
+
if isinstance(obj, NodeOutputProxy) and isinstance(obj._data, dict) and node.attr in obj._data:
|
|
953
|
+
value = obj._data[node.attr]
|
|
954
|
+
return NodeOutputProxy(value) if isinstance(value, list) else value
|
|
944
955
|
return getattr(obj, node.attr, None)
|
|
945
956
|
|
|
946
957
|
# 인덱싱 (arr[0], dict["key"])
|
|
@@ -952,6 +963,15 @@ class SafeEvaluator:
|
|
|
952
963
|
# 함수 호출
|
|
953
964
|
if isinstance(node, ast.Call):
|
|
954
965
|
func = self._eval_node(node.func)
|
|
966
|
+
if not callable(func) and isinstance(node.func, ast.Attribute):
|
|
967
|
+
# `{{ nodes.open_orders.count() }}`: the bare attribute resolves to
|
|
968
|
+
# the `count` PORT (see the Attribute branch), but a CALL of that
|
|
969
|
+
# name still means the proxy helper, so workflows written against
|
|
970
|
+
# the helper keep working on nodes that also declare the port.
|
|
971
|
+
owner = self._eval_node(node.func.value)
|
|
972
|
+
helper = getattr(owner, node.func.attr, None) if isinstance(owner, NodeOutputProxy) else None
|
|
973
|
+
if callable(helper):
|
|
974
|
+
func = helper
|
|
955
975
|
if not callable(func):
|
|
956
976
|
raise ExpressionError(f"호출 불가능한 객체: {func}")
|
|
957
977
|
args = [self._eval_node(arg) for arg in node.args]
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/i18n/locales/en.json
RENAMED
|
@@ -291,7 +291,7 @@
|
|
|
291
291
|
"fields.CurrencyRateNode.target_currencies": "Target currencies. Select currencies to get exchange rates against the base currency.",
|
|
292
292
|
"fields.CurrencyRateNode.timeout_seconds": "API request timeout (seconds). Default 30s, adjust based on your network conditions.",
|
|
293
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.",
|
|
294
|
+
"fields.CodeNode.outputs": "Output port declarations [{name, type}]. Leave empty for a single 'result' port carrying the whole return value: return 3 yields result=3; return {'result':3} yields result={'result':3}. Declare [{name:'result',type:'number'}] to map a returned dict's result key to the number port. 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
295
|
"fields.CodeNode.params": "Parameters passed to execute() as `params`. Expression bindings are allowed (e.g. a period or threshold).",
|
|
296
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.",
|
|
297
297
|
"fields.FieldMappingNode.data": "Input data to transform. Bind from HTTPRequestNode response or other node outputs.",
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/i18n/locales/ko.json
RENAMED
|
@@ -291,7 +291,7 @@
|
|
|
291
291
|
"fields.CurrencyRateNode.target_currencies": "대상 통화 목록. 기준 통화 대비 환율을 조회할 통화를 선택하세요.",
|
|
292
292
|
"fields.CurrencyRateNode.timeout_seconds": "API 요청 타임아웃 (초). 기본 30초, 네트워크 환경에 따라 조정하세요.",
|
|
293
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 키를 꺼내는 방식. 선언하면 정적 오타 검증이 적용됩니다.",
|
|
294
|
+
"fields.CodeNode.outputs": "출력 포트 선언 [{name, type}]. 비워두면 반환값 전체를 담는 단일 'result' 포트가 됩니다. 선언한 포트는 소비자 노드의 범용 입력 필드(TableDisplayNode.data, IfNode.left/right 등)에 {{ nodes.<id>.<port> }} 표현식으로 소비됩니다 — 타입드 포트 매칭이 아니라 이름으로 dict 키를 꺼내는 방식. 선언하면 정적 오타 검증이 적용됩니다. outputs를 비우면 반환값 전체가 result입니다. return 3은 result=3, return {'result':3}은 result={'result':3}이 됩니다. 반환 딕셔너리의 result 키를 숫자 포트로 사용하려면 outputs에 name:result, type:number를 선언하세요.",
|
|
295
295
|
"fields.CodeNode.params": "execute() 에 `params` 로 전달되는 파라미터. 표현식 바인딩 허용(예: 기간·임계값).",
|
|
296
296
|
"fields.CodeNode.data": "execute() 에 `data` 로 전달되는 입력 데이터. 상류 값 전체를 바인딩(예: {{ nodes.hist.values }})하고 함수 안에서 반복하세요.",
|
|
297
297
|
"fields.FieldMappingNode.data": "변환할 입력 데이터. HTTPRequestNode 응답이나 다른 노드 출력을 바인딩하세요.",
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/account_futures.py
RENAMED
|
@@ -65,6 +65,12 @@ class OverseasFuturesAccountNode(BaseNode):
|
|
|
65
65
|
"Returns three ports: held_symbols (symbol list), balance (margin/equity summary), positions (per-contract P&L)",
|
|
66
66
|
"is_tool_enabled=True — AI Agent can call this node as a tool to inspect futures portfolio state",
|
|
67
67
|
"One-shot REST call; supports paper_trading mode via OverseasFuturesBrokerNode",
|
|
68
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
69
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
70
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
71
|
+
"Its recording (held_symbols, balance, positions) is what the workflow's guard reads; the replay's simulated ledger is seeded only from fixture.broker.account, and a holding used by a scenario must appear in both with the same integer quantity",
|
|
72
|
+
"A frame that submits no order must expect exactly the seeded cash and positions; an insufficient_cash case needs both the seeded cash and the recorded orderable_amount below quantity times the quoted price",
|
|
73
|
+
"Retained across realtime events (queried once at startup); re-runs on every schedule tick",
|
|
68
74
|
]
|
|
69
75
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
70
76
|
{
|
|
@@ -64,6 +64,12 @@ class KoreaStockAccountNode(BaseNode):
|
|
|
64
64
|
"Returns three ports: held_symbols (symbol list with KRX codes), balance (KRW cash/equity summary), positions (per-symbol P&L in KRW)",
|
|
65
65
|
"is_tool_enabled=True — AI Agent can call this node as a tool to inspect Korea stock portfolio state",
|
|
66
66
|
"Real trading only (KoreaStockBrokerNode does not support paper_trading mode); no mock data",
|
|
67
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
68
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
69
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
70
|
+
"Its recording (held_symbols, balance, positions) is what the workflow's guard reads; the replay's simulated ledger is seeded only from fixture.broker.account, and a holding used by a scenario must appear in both with the same integer quantity",
|
|
71
|
+
"A frame that submits no order must expect exactly the seeded cash and positions; an insufficient_cash case needs both the seeded cash and the recorded orderable_amount below quantity times the quoted price",
|
|
72
|
+
"Retained across realtime events (queried once at startup); re-runs on every schedule tick",
|
|
67
73
|
]
|
|
68
74
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
69
75
|
{
|
|
@@ -111,7 +117,7 @@ class KoreaStockAccountNode(BaseNode):
|
|
|
111
117
|
}
|
|
112
118
|
],
|
|
113
119
|
},
|
|
114
|
-
"expected_output": "positions
|
|
120
|
+
"expected_output": "positions: observed holdings with symbol, quantity, avg_price, pnl_rate and cost/P&L availability; missing money stays null. balance: {deposit, total_eval, orderable_amount} in KRW; held_symbols: [{exchange, symbol}].",
|
|
115
121
|
},
|
|
116
122
|
{
|
|
117
123
|
"title": "Cash check before Korea stock order",
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/account_stock.py
RENAMED
|
@@ -66,6 +66,12 @@ class OverseasStockAccountNode(BaseNode):
|
|
|
66
66
|
"Returns three ports: held_symbols (symbol list), balance (cash/equity summary), positions (per-symbol P&L)",
|
|
67
67
|
"is_tool_enabled=True — AI Agent can call this node as a tool to inspect portfolio state",
|
|
68
68
|
"One-shot REST call: safe to use in scheduled or on-demand workflows without WebSocket overhead",
|
|
69
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
70
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
71
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
72
|
+
"Its recording (held_symbols, balance, positions) is what the workflow's guard reads; the replay's simulated ledger is seeded only from fixture.broker.account, and a holding used by a scenario must appear in both with the same integer quantity",
|
|
73
|
+
"A frame that submits no order must expect exactly the seeded cash and positions; an insufficient_cash case needs both the seeded cash and the recorded orderable_amount below quantity times the quoted price",
|
|
74
|
+
"Retained across realtime events (queried once at startup); re-runs on every schedule tick",
|
|
69
75
|
]
|
|
70
76
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
71
77
|
{
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest_futures.py
RENAMED
|
@@ -102,6 +102,13 @@ class OverseasFuturesHistoricalDataNode(BaseNode):
|
|
|
102
102
|
"adjust=True applies roll-adjusted prices for continuous contract backtesting",
|
|
103
103
|
"is_tool_enabled=True — AI Agent can fetch futures history for technical analysis autonomously",
|
|
104
104
|
"Item-based execution: pair with SplitNode to fetch OHLCV for multiple contracts",
|
|
105
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
106
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
107
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
108
|
+
"On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
|
|
109
|
+
"The value port is one row object whose time_series lists bars of exactly {date, open, high, low, close, volume}; the values port is the array of such rows",
|
|
110
|
+
"This ohlcv_data is not the realtime shape: a *RealMarketDataNode records ohlcv_data as a symbol-keyed object of bar lists, although both are typed ohlcv_data",
|
|
111
|
+
"A previous close is the close of the corresponding bar in time_series; a change cannot be computed from a single bar",
|
|
105
112
|
]
|
|
106
113
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
107
114
|
{
|
|
@@ -102,6 +102,13 @@ class KoreaStockHistoricalDataNode(BaseNode):
|
|
|
102
102
|
"start_date / end_date accept YYYYMMDD strings or `{{ date.ago(N, format='yyyymmdd') }}` expressions",
|
|
103
103
|
"is_tool_enabled=True — AI Agent can fetch domestic historical data for fundamental/technical analysis",
|
|
104
104
|
"Symbol format: 6-digit KRX code (e.g., '005930') without exchange field",
|
|
105
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
106
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
107
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
108
|
+
"On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
|
|
109
|
+
"The value port is one row object whose time_series lists bars of exactly {date, open, high, low, close, volume}; the values port is the array of such rows",
|
|
110
|
+
"This ohlcv_data is not the realtime shape: a *RealMarketDataNode records ohlcv_data as a symbol-keyed object of bar lists, although both are typed ohlcv_data",
|
|
111
|
+
"A previous close is the close of the corresponding bar in time_series; a change cannot be computed from a single bar",
|
|
105
112
|
]
|
|
106
113
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
107
114
|
{
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest_stock.py
RENAMED
|
@@ -102,6 +102,13 @@ class OverseasStockHistoricalDataNode(BaseNode):
|
|
|
102
102
|
"adjust=True applies split/dividend-adjusted prices for accurate long-term backtesting",
|
|
103
103
|
"is_tool_enabled=True — AI Agent can fetch historical data autonomously for technical analysis",
|
|
104
104
|
"Item-based execution: pair with SplitNode to fetch history for each symbol in a watchlist",
|
|
105
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
106
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
107
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
108
|
+
"On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
|
|
109
|
+
"The value port is one row object whose time_series lists bars of exactly {date, open, high, low, close, volume}; the values port is the array of such rows",
|
|
110
|
+
"This ohlcv_data is not the realtime shape: a *RealMarketDataNode records ohlcv_data as a symbol-keyed object of bar lists, although both are typed ohlcv_data",
|
|
111
|
+
"A previous close is the close of the corresponding bar in time_series; a change cannot be computed from a single bar",
|
|
105
112
|
]
|
|
106
113
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
107
114
|
{
|
|
@@ -328,7 +328,7 @@ OVERSEAS_STOCK_POSITION_FIELDS: List[Dict[str, str]] = [
|
|
|
328
328
|
{"name": "market", "type": "string", "description": "시장 구분명"},
|
|
329
329
|
]
|
|
330
330
|
|
|
331
|
-
#
|
|
331
|
+
# Domestic REST account: observed CSPAQ12300 evidence plus executor aliases.
|
|
332
332
|
KOREA_STOCK_POSITION_FIELDS: List[Dict[str, str]] = [
|
|
333
333
|
{"name": "exchange", "type": "string", "description": "거래소 코드 (항상 KRX)"},
|
|
334
334
|
{"name": "symbol", "type": "string", "description": "종목코드"},
|
|
@@ -342,6 +342,20 @@ KOREA_STOCK_POSITION_FIELDS: List[Dict[str, str]] = [
|
|
|
342
342
|
{"name": "sellable_qty", "type": "number", "description": "매도 가능 수량"},
|
|
343
343
|
{"name": "eval_amount", "type": "number", "description": "평가 금액"},
|
|
344
344
|
{"name": "product", "type": "string", "description": "상품 구분 (항상 korea_stock)"},
|
|
345
|
+
{"name": "symbol_name", "type": "string", "description": "Observed instrument name; name alias uses the same value"},
|
|
346
|
+
{"name": "currency", "type": "string", "description": "Valuation currency: KRW"},
|
|
347
|
+
{"name": "buy_price", "type": "number", "description": "Observed average acquisition price; null when unavailable"},
|
|
348
|
+
{"name": "average_price", "type": "number", "description": "Observed average acquisition price; null when unavailable"},
|
|
349
|
+
{"name": "acquisition_amount", "type": "number", "description": "Observed acquisition amount; null when unavailable"},
|
|
350
|
+
{"name": "purchase_amount", "type": "number", "description": "Acquisition amount alias; null when unavailable"},
|
|
351
|
+
{"name": "pnl_rate_basis", "type": "string", "description": "Broker average cost basis, commission excluded"},
|
|
352
|
+
{"name": "pnl_rate_status", "type": "string", "description": "available or unavailable; missing evidence is not zero return"},
|
|
353
|
+
{"name": "pnl_status", "type": "string", "description": "available or unavailable"},
|
|
354
|
+
{"name": "pnl_unavailable_reason", "type": "string", "description": "Missing P&L evidence reason; null when available"},
|
|
355
|
+
{"name": "cost_basis", "type": "string", "description": "Broker average cost basis, commission excluded"},
|
|
356
|
+
{"name": "cost_status", "type": "string", "description": "available or unavailable"},
|
|
357
|
+
{"name": "cost_unavailable_reason", "type": "string", "description": "Missing acquisition evidence reason; null when available"},
|
|
358
|
+
{"name": "observed_at", "type": "string", "description": "UTC ISO observation timestamp"},
|
|
345
359
|
]
|
|
346
360
|
|
|
347
361
|
# 해외선물 REST 계좌 (CIDBQ01500 block2)
|
|
@@ -1030,4 +1044,4 @@ class BaseMessagingNode(BaseNode):
|
|
|
1030
1044
|
category=FieldCategory.SETTINGS,
|
|
1031
1045
|
depends_on={"resilience.fallback.mode": "default_value"},
|
|
1032
1046
|
),
|
|
1033
|
-
}
|
|
1047
|
+
}
|
|
@@ -101,6 +101,10 @@ class OverseasStockBrokerNode(BaseBrokerNode):
|
|
|
101
101
|
"Creates and caches the LS-Sec session so every downstream overseas_stock node shares the same login",
|
|
102
102
|
"`connection` output is auto-injected into every overseas_stock node by the executor — no manual binding required",
|
|
103
103
|
"credential_types=['broker_ls_overseas_stock'] ensures only the correct credential is selected",
|
|
104
|
+
"On the first run its recording's request carries no connection; its output is {\"connection\": {provider, product, paper_trading, broker_node_id, and credential_id only when the account is linked}}, and an unlinked account has no credential_id key (never null)",
|
|
105
|
+
"On a schedule tick the executor injects this broker's own retained output.connection into its request, so a broker tick recording must repeat that connection or the replay refuses it",
|
|
106
|
+
"Downstream product-scoped nodes reference it by request.connection.broker_node_id set to this node's id; the compiler completes provider/product/paper_trading (and credential_id) from this broker's recorded output.connection",
|
|
107
|
+
"paper_trading must be false; the executor raises if it is true",
|
|
104
108
|
]
|
|
105
109
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
106
110
|
{
|
|
@@ -295,6 +299,9 @@ class OverseasFuturesBrokerNode(BaseBrokerNode):
|
|
|
295
299
|
"Only broker that natively supports LS-Sec paper trading (set paper_trading=True)",
|
|
296
300
|
"Auto-injects the connection into every overseas_futures node in the same workflow",
|
|
297
301
|
"credential_types=['broker_ls_overseas_futures'] keeps the selector scoped",
|
|
302
|
+
"On the first run its recording's request carries no connection; its output is {\"connection\": {provider, product, paper_trading, broker_node_id, and credential_id only when the account is linked}}, and an unlinked account has no credential_id key (never null)",
|
|
303
|
+
"On a schedule tick the executor injects this broker's own retained output.connection into its request, so a broker tick recording must repeat that connection or the replay refuses it",
|
|
304
|
+
"Downstream product-scoped nodes reference it by request.connection.broker_node_id set to this node's id; the compiler completes provider/product/paper_trading (and credential_id) from this broker's recorded output.connection",
|
|
298
305
|
]
|
|
299
306
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
300
307
|
{
|
|
@@ -455,6 +462,9 @@ class KoreaStockBrokerNode(BaseBrokerNode):
|
|
|
455
462
|
"Dedicated credential type `broker_ls_korea_stock` keeps KRX credentials isolated from overseas ones",
|
|
456
463
|
"Auto-injects connection into every korea_stock scoped node",
|
|
457
464
|
"Executes the Korean-specific workflow P&L tracker (KRW-denominated) when account nodes are wired",
|
|
465
|
+
"On the first run its recording's request carries no connection; its output is {\"connection\": {provider, product, paper_trading, broker_node_id, and credential_id only when the account is linked}}, and an unlinked account has no credential_id key (never null)",
|
|
466
|
+
"On a schedule tick the executor injects this broker's own retained output.connection into its request, so a broker tick recording must repeat that connection or the replay refuses it",
|
|
467
|
+
"Downstream product-scoped nodes reference it by request.connection.broker_node_id set to this node's id; the compiler completes provider/product/paper_trading (and credential_id) from this broker's recorded output.connection",
|
|
458
468
|
]
|
|
459
469
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
460
470
|
{
|
|
@@ -69,8 +69,10 @@ class CodeNode(BaseNode):
|
|
|
69
69
|
- `context` = a READ-ONLY scrubbed context: safe helper namespaces
|
|
70
70
|
(`context.date/finance/stats/format/lst`), a risk-tracker read snapshot,
|
|
71
71
|
and workflow meta. It exposes NO credentials, broker, or executor.
|
|
72
|
-
-
|
|
73
|
-
|
|
72
|
+
- With declared `outputs`, return a dict keyed by those ports. A single
|
|
73
|
+
declared port also accepts a scalar. With no declaration, the WHOLE return
|
|
74
|
+
value becomes `result`: return 3 gives result=3; return {'result': 3}
|
|
75
|
+
gives result={'result': 3}, not result=3.
|
|
74
76
|
|
|
75
77
|
Example DSL:
|
|
76
78
|
{
|
|
@@ -108,9 +110,15 @@ class CodeNode(BaseNode):
|
|
|
108
110
|
outputs: List[Dict[str, str]] = Field(
|
|
109
111
|
default_factory=list,
|
|
110
112
|
description=(
|
|
111
|
-
"Output port declarations [{name, type}]. Empty
|
|
113
|
+
"Output port declarations [{name, type}]. Empty exposes the WHOLE return on result: "
|
|
114
|
+
"return 3 gives result=3; return {'result': 3} gives result={'result': 3}. "
|
|
115
|
+
"With outputs=[{name:'result',type:'number'}], return {'result':3} gives result=3. "
|
|
112
116
|
"Declared ports are consumed downstream by {{ nodes.<id>.<port> }} expressions, "
|
|
113
|
-
"not by typed-port matching; declaring them enables static typo-guarding."
|
|
117
|
+
"not by typed-port matching; declaring them enables static typo-guarding. "
|
|
118
|
+
"Do NOT name a port 'error' or 'reason': the engine treats a top-level 'error' "
|
|
119
|
+
"value (and a top-level 'reason' of no_symbol/no_price/invalid_input) as a node "
|
|
120
|
+
"failure, so such a port makes a successful return read as an engine error. Put "
|
|
121
|
+
"any status/diagnostic under a differently named port or nested object instead."
|
|
114
122
|
),
|
|
115
123
|
)
|
|
116
124
|
|
|
@@ -160,6 +168,9 @@ class CodeNode(BaseNode):
|
|
|
160
168
|
"Safe helper namespaces available on context (date/finance/stats/format/lst) mirror expression bindings",
|
|
161
169
|
"Batch-friendly: receives the whole upstream array in `data` (one subprocess call, loop in-code)",
|
|
162
170
|
"Multi-upstream: accepts any number of upstream producers — bind each one to its own expression-bound `params` key ({{ nodes.<id>.<port> }}) and read it with params.get(...)",
|
|
171
|
+
"The top-level output keys error (truthy) and reason in {no_symbol, no_price, invalid_input} are engine failure markers; never declare or expect them as ports — put a diagnostic inside a nested object port (e.g. result.error) or a differently named port",
|
|
172
|
+
"A computation node: it receives no external recording in a suite (its code runs) and keeps no state across ticks; a durable one-shot guard needs ThrottleNode or SQLiteNode",
|
|
173
|
+
"Completing with a boolean false does not gate a downstream order node; route the boolean through an IfNode true edge",
|
|
163
174
|
]
|
|
164
175
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
165
176
|
{
|
|
@@ -174,9 +185,14 @@ class CodeNode(BaseNode):
|
|
|
174
185
|
},
|
|
175
186
|
{
|
|
176
187
|
"pattern": "Declaring 'outputs' ports whose names the return dict never sets",
|
|
177
|
-
"reason": "
|
|
188
|
+
"reason": "Deep/replay validation rejects missing declared ports and values that violate their declared types. Legacy live mapping to None is not evidence of a valid output.",
|
|
178
189
|
"alternative": "Return a dict whose keys exactly match every declared output port name.",
|
|
179
190
|
},
|
|
191
|
+
{
|
|
192
|
+
"pattern": "Naming a declared output port 'error' or 'reason'",
|
|
193
|
+
"reason": "The engine (live runtime and replay) treats a top-level 'error' value, and a top-level 'reason' of no_symbol/no_price/invalid_input, as a node failure. A port with either name makes the node's own successful output read as an engine execution error, and no calculation defect exists to repair.",
|
|
194
|
+
"alternative": "Rename the port (e.g. 'status', 'note', 'detail') or nest status inside another object such as {'result': {...}}; keep 'error'/'reason' out of the top-level return.",
|
|
195
|
+
},
|
|
180
196
|
{
|
|
181
197
|
"pattern": "Feeding a CodeNode return of non-standard shape into a typed node (order node, ConditionNode)",
|
|
182
198
|
"reason": "The binding layer checks port existence but never coerces shape; a wrong shape does not fail at the CodeNode — it fails downstream where the typed node reads it (e.g. order nodes expect the Symbol Data Format array of {symbol, exchange, ...}).",
|
|
@@ -347,7 +363,7 @@ class CodeNode(BaseNode):
|
|
|
347
363
|
]
|
|
348
364
|
_node_guide: ClassVar[Dict[str, Any]] = {
|
|
349
365
|
"input_handling": "SINGLE upstream: bind the whole upstream value to 'data' (e.g. \"{{ nodes.hist.values }}\") and loop over it inside execute() — CodeNode is not per-item auto-iterated. MULTIPLE upstreams (2+ producers feeding this node — fully supported): 'data' holds ONE whole value, so do NOT try to merge producers into it. Keep an edge from EACH producer and bind EACH upstream value to its own key under 'params' — params values are expression-bindable: \"params\": {\"fgi_value\": \"{{ nodes.fgi.value }}\", \"vix_response\": \"{{ nodes.vix_http.response }}\"} — then read them with params.get('fgi_value', 50) inside execute(). params.get(...) is the normal, always-allowed way to read bound inputs. 'params' also carries fixed knobs (literals). Declare 'outputs' when you want named ports; omit it to use the single 'result' port.",
|
|
350
|
-
"output_consumption": "Downstream nodes do NOT need a matching typed port. They consume a CodeNode output by writing a {{ nodes.<id>.<port> }} expression into their own generic input field (e.g. TableDisplayNode.data, IfNode.left/right, FieldMappingNode.data) — the binding layer resolves the dict key by name, it does not type-match ports. With no outputs declared, read {{ nodes.<id>.result }} (the whole return value). A declared output port that no node references is fine; a declared port absent from the return dict
|
|
366
|
+
"output_consumption": "Downstream nodes do NOT need a matching typed port. They consume a CodeNode output by writing a {{ nodes.<id>.<port> }} expression into their own generic input field (e.g. TableDisplayNode.data, IfNode.left/right, FieldMappingNode.data) — the binding layer resolves the dict key by name, it does not type-match ports. With no outputs declared, read {{ nodes.<id>.result }} (the whole return value). A declared output port that no node references is fine; a declared port absent from the return dict fails deep/replay validation. Do not rely on legacy live None substitution. Because ports are declared, validate() typo-guards these references.",
|
|
351
367
|
"common_combinations": [
|
|
352
368
|
"CodeNode → TableDisplayNode / LineChartNode / TelegramNode (display or sink — any return shape works)",
|
|
353
369
|
"CodeNode → OverseasStockNewOrderNode / ConditionNode (typed node — return the standard [{symbol, exchange, ...}] shape)",
|
|
@@ -362,7 +378,7 @@ class CodeNode(BaseNode):
|
|
|
362
378
|
"sandbox, and I/O/system stdlib (os, sys, socket, subprocess, urllib, http, requests, open) is "
|
|
363
379
|
"blocked. Re-implement such functionality by hand in pure Python using the allowed stdlib.",
|
|
364
380
|
"No credential/broker/network access — CodeNode cannot place orders or fetch data itself; feed it data from typed nodes.",
|
|
365
|
-
"
|
|
381
|
+
"Every declared output must be present and match its type in deep/replay validation. A single declared port also supports a scalar return.",
|
|
366
382
|
"There is NO one-upstream limit. Only the `data` field carries a single whole value — "
|
|
367
383
|
"the node accepts any number of upstream producers. With 2+ producers, keep an edge "
|
|
368
384
|
"from each and bind each value to its own `params` key "
|
|
@@ -440,7 +456,7 @@ class CodeNode(BaseNode):
|
|
|
440
456
|
expression_mode=ExpressionMode.FIXED_ONLY,
|
|
441
457
|
category=FieldCategory.PARAMETERS,
|
|
442
458
|
ui_component=UIComponent.CUSTOM_CODE_EDITOR,
|
|
443
|
-
example="async def execute(data, params, context):\n return
|
|
459
|
+
example="async def execute(data, params, context):\n return data",
|
|
444
460
|
expected_type="str",
|
|
445
461
|
help_text=(
|
|
446
462
|
"Define async def execute(data, params, context). Sandboxed: no "
|
|
@@ -176,6 +176,10 @@ class SQLiteNode(BaseNode):
|
|
|
176
176
|
"Outputs rows (list), affected_count (int), and last_insert_id (int) to cover all read/write patterns",
|
|
177
177
|
"is_tool_enabled=True — AI Agent can use SQLiteNode as a tool to read or write local state",
|
|
178
178
|
"Databases are scoped to /app/data/ directory; db_name selects the file",
|
|
179
|
+
"Has a dedicated replay adapter (execute_sqlite) and takes no external recording; it runs against a file-backed database rooted at the run's storage directory",
|
|
180
|
+
"State written on one tick or event frame persists to that database and stays visible on later frames within the run, so a durable duplicate guard can be built on it",
|
|
181
|
+
"Each execution runs exactly ONE SQL statement (execute_query and every simple-mode action call db.execute once); 'CREATE TABLE …; INSERT …' in one query fails with 'You can only execute one statement at a time'",
|
|
182
|
+
"Neither mode creates a table: an insert/select into a missing table fails with 'no such table'; create it first with a separate execute_query SQLiteNode (CREATE TABLE IF NOT EXISTS …) placed upstream, then read/write it here",
|
|
179
183
|
]
|
|
180
184
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
181
185
|
{
|
|
@@ -212,7 +216,7 @@ class SQLiteNode(BaseNode):
|
|
|
212
216
|
"operation": "simple",
|
|
213
217
|
"table": "hwm",
|
|
214
218
|
"action": "upsert",
|
|
215
|
-
"values": {"symbol": "{{ nodes.market.
|
|
219
|
+
"values": {"symbol": "{{ nodes.market.values[0].symbol }}", "peak": '{{ nodes.market.values[0].price }}'},
|
|
216
220
|
"on_conflict": "symbol",
|
|
217
221
|
},
|
|
218
222
|
{
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/data_futures.py
RENAMED
|
@@ -90,6 +90,11 @@ class OverseasFuturesMarketDataNode(BaseNode):
|
|
|
90
90
|
"is_tool_enabled=True — AI Agent can call this node to look up live futures prices autonomously",
|
|
91
91
|
"Supported exchanges: CME, EUREX, SGX, HKEX — symbol format includes contract month code (e.g., ESH26)",
|
|
92
92
|
"Broker connection is auto-injected via DAG traversal from OverseasFuturesBrokerNode",
|
|
93
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
94
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
95
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
96
|
+
"On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
|
|
97
|
+
"In replay the one-shot quote records its `values` port as a flat row list at nodes.<id>.output.values; this is not the realtime symbol-keyed ohlcv_data shape",
|
|
93
98
|
]
|
|
94
99
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
95
100
|
{
|
|
@@ -196,8 +201,6 @@ class OverseasFuturesMarketDataNode(BaseNode):
|
|
|
196
201
|
InputPort(name="trigger", type="signal", description="i18n:ports.trigger", required=False),
|
|
197
202
|
]
|
|
198
203
|
_outputs: List[OutputPort] = [
|
|
199
|
-
OutputPort(name="value", type="market_data", description="i18n:ports.market_data_value",
|
|
200
|
-
fields=FUTURES_REST_PRICE_FIELDS),
|
|
201
204
|
OutputPort(
|
|
202
205
|
name="values",
|
|
203
206
|
type="array",
|
|
@@ -206,9 +209,9 @@ class OverseasFuturesMarketDataNode(BaseNode):
|
|
|
206
209
|
),
|
|
207
210
|
]
|
|
208
211
|
|
|
209
|
-
_version: ClassVar[str] = "1.0.
|
|
210
|
-
_updated_at: ClassVar[str] = "2026-
|
|
211
|
-
_change_note: ClassVar[Optional[str]] =
|
|
212
|
+
_version: ClassVar[str] = "1.0.1"
|
|
213
|
+
_updated_at: ClassVar[str] = "2026-09-23"
|
|
214
|
+
_change_note: ClassVar[Optional[str]] = "Remove nonexistent singular quote output; bind the values array."
|
|
212
215
|
|
|
213
216
|
@classmethod
|
|
214
217
|
def get_field_schema(cls) -> Dict[str, "FieldSchema"]:
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/data_korea_stock.py
RENAMED
|
@@ -85,6 +85,11 @@ class KoreaStockMarketDataNode(BaseNode):
|
|
|
85
85
|
"Item-based execution: pair with SplitNode to query multiple domestic stocks in sequence",
|
|
86
86
|
"is_tool_enabled=True — AI Agent can call this node to look up KRX stock prices autonomously",
|
|
87
87
|
"Real-trading only — KoreaStock product does not support paper trading (paper_trading=False enforced)",
|
|
88
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
89
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
90
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
91
|
+
"On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
|
|
92
|
+
"In replay the one-shot quote records its `values` port as a flat row list at nodes.<id>.output.values; this is not the realtime symbol-keyed ohlcv_data shape",
|
|
88
93
|
]
|
|
89
94
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
90
95
|
{
|
|
@@ -195,7 +200,6 @@ class KoreaStockMarketDataNode(BaseNode):
|
|
|
195
200
|
InputPort(name="trigger", type="signal", description="i18n:ports.trigger", required=False),
|
|
196
201
|
]
|
|
197
202
|
_outputs: List[OutputPort] = [
|
|
198
|
-
OutputPort(name="value", type="market_data", description="i18n:ports.market_data_value", fields=KOREA_STOCK_PRICE_DATA_FIELDS),
|
|
199
203
|
OutputPort(
|
|
200
204
|
name="values",
|
|
201
205
|
type="array",
|
|
@@ -204,9 +208,9 @@ class KoreaStockMarketDataNode(BaseNode):
|
|
|
204
208
|
),
|
|
205
209
|
]
|
|
206
210
|
|
|
207
|
-
_version: ClassVar[str] = "1.0.
|
|
208
|
-
_updated_at: ClassVar[str] = "2026-
|
|
209
|
-
_change_note: ClassVar[Optional[str]] =
|
|
211
|
+
_version: ClassVar[str] = "1.0.1"
|
|
212
|
+
_updated_at: ClassVar[str] = "2026-09-23"
|
|
213
|
+
_change_note: ClassVar[Optional[str]] = "Remove nonexistent singular quote output; bind the values array."
|
|
210
214
|
|
|
211
215
|
@classmethod
|
|
212
216
|
def get_field_schema(cls) -> Dict[str, "FieldSchema"]:
|
{programgarden_core-2.0.1 → programgarden_core-2.2.0}/programgarden_core/nodes/data_stock.py
RENAMED
|
@@ -87,6 +87,11 @@ class OverseasStockMarketDataNode(BaseNode):
|
|
|
87
87
|
"is_tool_enabled=True — AI Agent can call this node to look up live prices autonomously",
|
|
88
88
|
"Broker connection is auto-injected via DAG traversal — no explicit binding needed",
|
|
89
89
|
"REST-based polling; for continuous streaming use OverseasStockRealMarketDataNode",
|
|
90
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
91
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
92
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
93
|
+
"On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
|
|
94
|
+
"In replay the one-shot quote records its `values` port as a flat row list at nodes.<id>.output.values; this is not the realtime symbol-keyed ohlcv_data shape",
|
|
90
95
|
]
|
|
91
96
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
92
97
|
{
|
|
@@ -196,26 +201,6 @@ class OverseasStockMarketDataNode(BaseNode):
|
|
|
196
201
|
InputPort(name="trigger", type="signal", description="i18n:ports.trigger", required=False),
|
|
197
202
|
]
|
|
198
203
|
_outputs: List[OutputPort] = [
|
|
199
|
-
OutputPort(
|
|
200
|
-
name="value",
|
|
201
|
-
type="market_data",
|
|
202
|
-
description="i18n:ports.market_data_value",
|
|
203
|
-
fields=PRICE_DATA_FIELDS,
|
|
204
|
-
example={
|
|
205
|
-
"symbol": "AAPL",
|
|
206
|
-
"exchange": "NASDAQ",
|
|
207
|
-
"price": 187.45,
|
|
208
|
-
"change": -2.34,
|
|
209
|
-
"change_pct": -1.23,
|
|
210
|
-
"volume": 12_345_678,
|
|
211
|
-
"open": 189.10,
|
|
212
|
-
"high": 190.02,
|
|
213
|
-
"low": 186.90,
|
|
214
|
-
"close": 187.45,
|
|
215
|
-
"per": 28.5,
|
|
216
|
-
"eps": 6.57,
|
|
217
|
-
},
|
|
218
|
-
),
|
|
219
204
|
OutputPort(
|
|
220
205
|
name="values",
|
|
221
206
|
type="array",
|
|
@@ -224,9 +209,9 @@ class OverseasStockMarketDataNode(BaseNode):
|
|
|
224
209
|
),
|
|
225
210
|
]
|
|
226
211
|
|
|
227
|
-
_version: ClassVar[str] = "1.0.
|
|
228
|
-
_updated_at: ClassVar[str] = "2026-
|
|
229
|
-
_change_note: ClassVar[Optional[str]] =
|
|
212
|
+
_version: ClassVar[str] = "1.0.1"
|
|
213
|
+
_updated_at: ClassVar[str] = "2026-09-23"
|
|
214
|
+
_change_note: ClassVar[Optional[str]] = "Remove nonexistent singular quote output; bind the values array."
|
|
230
215
|
|
|
231
216
|
@classmethod
|
|
232
217
|
def get_field_schema(cls) -> Dict[str, "FieldSchema"]:
|
|
@@ -190,7 +190,7 @@ class TableDisplayNode(BaseDisplayNode):
|
|
|
190
190
|
{
|
|
191
191
|
"pattern": "Binding a single dict (not a list) to TableDisplayNode data",
|
|
192
192
|
"reason": "TableDisplayNode expects list[dict]. A plain dict renders as a single-column table with keys, which is rarely useful.",
|
|
193
|
-
"alternative": "Wrap single records in a list ([{{ nodes.market.
|
|
193
|
+
"alternative": "Wrap single records in a list ([{{ nodes.market.values[0] }}]) or use SummaryDisplayNode for single-object display.",
|
|
194
194
|
},
|
|
195
195
|
{
|
|
196
196
|
"pattern": "Setting limit=100 for a live dashboard that updates every second",
|
|
@@ -66,6 +66,9 @@ class StartNode(BaseNode):
|
|
|
66
66
|
"Zero configuration — no fields, no credentials",
|
|
67
67
|
"Produces a simple trigger signal that flows through main edges",
|
|
68
68
|
"Always completes instantly and never fails",
|
|
69
|
+
"Exactly one StartNode per workflow; a graph rooted at a ScheduleNode is rejected (MISSING_START_NODE)",
|
|
70
|
+
"Emits no subsequent event: a StartNode-only workflow runs once and cannot carry a duplicate or events scenario",
|
|
71
|
+
"The start port is a bare trigger signal; an output contract cannot assert a data type on it",
|
|
69
72
|
]
|
|
70
73
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
71
74
|
{
|
|
@@ -75,7 +78,7 @@ class StartNode(BaseNode):
|
|
|
75
78
|
},
|
|
76
79
|
{
|
|
77
80
|
"pattern": "Omitting StartNode and wiring ScheduleNode as the root",
|
|
78
|
-
"reason": "
|
|
81
|
+
"reason": "The engine rejects a graph rooted at a ScheduleNode (MISSING_START_NODE); a ScheduleNode is never the root.",
|
|
79
82
|
"alternative": "Always include a StartNode and connect it to the ScheduleNode as the first main edge.",
|
|
80
83
|
},
|
|
81
84
|
]
|
|
@@ -233,6 +236,9 @@ class ThrottleNode(BaseNode):
|
|
|
233
236
|
"Two modes: 'skip' (drop during cooldown) and 'latest' (buffer newest, emit at window close)",
|
|
234
237
|
"pass_first=True emits the first event immediately, useful for warm-start flows",
|
|
235
238
|
"Emits _throttle_stats output for observability (received / passed / skipped counts)",
|
|
239
|
+
"Cooldown state persists in context.node_state, so it survives re-triggers within one execution: with pass_first=true the first event passes and a further event within interval_sec is throttled",
|
|
240
|
+
"While throttled it emits only its `_`-prefixed ports (_throttled/_throttle_stats), which never leak downstream, so a node placed after it does not re-execute",
|
|
241
|
+
"interval_sec is bounded between 0.1 and 300 seconds",
|
|
236
242
|
]
|
|
237
243
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
238
244
|
{
|
|
@@ -974,7 +980,8 @@ class IfNode(BaseNode):
|
|
|
974
980
|
"12 comparison operators including ==, !=, >, >=, <, <=, in, not_in, contains, not_contains, is_empty, is_not_empty",
|
|
975
981
|
"Three outputs: `true` / `false` payloads + `result` boolean — edges use from_port to route",
|
|
976
982
|
"Expression binding on both `left` and `right` operands — supports full `{{ nodes.X.Y }}` syntax",
|
|
977
|
-
"
|
|
983
|
+
"Skips only descendants of the untaken port that have no other active path; a node also fed by a non-If upstream still runs when that upstream completes",
|
|
984
|
+
"Evaluates exactly one left/operator/right comparison per node; the untaken branch is the false port, and a CodeNode returning false skips nothing",
|
|
978
985
|
]
|
|
979
986
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
980
987
|
{
|
|
@@ -62,6 +62,13 @@ class OverseasFuturesOpenOrdersNode(BaseNode):
|
|
|
62
62
|
"Returns `open_orders` (list of unfilled futures order dicts) and `count` (integer) ports",
|
|
63
63
|
"is_tool_enabled=True — AI Agent can query pending futures orders as a tool call",
|
|
64
64
|
"Works in both paper_trading and real trading modes via the upstream OverseasFuturesBrokerNode",
|
|
65
|
+
"Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
|
|
66
|
+
"Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
|
|
67
|
+
"A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
|
|
68
|
+
"Retained across realtime events (queried once at startup); re-runs on every schedule tick",
|
|
69
|
+
"Each recorded row needs a non-empty unique order_id, and count must equal the number of rows; a snapshot lists only still-open orders, so a guard blocks on presence (count > 0), not on a derived remaining_quantity",
|
|
70
|
+
"order_events (modify/cancel completions: [{request_node, symbol_key, event_id, applied}]) is a top-level field of the recording beside request/as_of/output, never inside output",
|
|
71
|
+
"An order this workflow did not place (any non-SIM- id) is taken exactly as recorded: never retired by order_events, never reserving simulated cash, and listed in every OpenOrders recording of the same tick or in none",
|
|
65
72
|
]
|
|
66
73
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
67
74
|
{
|