Privora 多资产金融数据后端,AI 代理统一 API(A股/基金/黄金/财报事件)+ Python 策略回测 + 模拟交易 + 7×24 云端告警(港股整理中),Bearer Token 接入,兼容 Hermes/Claude/GPT/OpenClaw。
给你的 AI Agent 一个统一的投资研究工作流后端 —— 数据查询 + 策略回测 + 组合归因 + 云端告警 + 流程编排一个 Token 全覆盖。
Hermes / Claude / GPT / OpenClaw 任何 Agent,通过一个 Bearer Token 即可访问:
stock_day,含沪深300 / 上证综指 / 中证A500 / 深证成指 4 主要指数)+ 港股( stock_day_hk,如 00700.HK)+ 美股( stock_day_us,如 AAPL); 分钟 K 线 A 股( stock_kline)+ 港股( stock_kline_hk),1/5/15/30/60 分钟; 持仓、 黄金、 基金、 财报事件(业绩预告 / 快报)——一个 API 全覆盖,详见下方「数据资产可用性」表。场内基金(ETF/LOF)日线( fund_quote_day)+ 分钟 K 线( fund_kline)表已就绪(🟡 尚未开放跨团队订阅,见下方表格)。让普通人也能拥有私募级别的工作流——不需要私募的预算,就能像私募研究员一样在同一条流水线里跑数据 + 分析 + Agent + 告警。
🆕 版本与变更历史:当前版本号以文件头 frontmatter 的 version: 字段为准,正文不重复这个数字;每次发布的完整变更说明见仓库内 CHANGELOG.md,随包分发的历史摘要见文末 §最近更新。
🎯 最适合:想把整个投研工作流(数据查询 + 回测 + 模拟交易 + 告警 + 流程编排)交给 AI Agent 自动化的散户 / 小工作室;把 Hermes / Claude / GPT / OpenClaw 当量化助手用的开发者。 注意:Bearer Token 是"工作流授权凭证",不只是"数据 API key" —— 授予前先按 §Scope 分类明白 要给 Agent 哪些能力。
🌐 产品主页: https://privora.cn · 注册即拿 Token

打破生态壁垒,本技能不仅专供某一平台,而是 完美兼容 Hermes、OpenClaw、Claude Code、GitHub Copilot 等所有支持外挂工具/技能的通用大模型 Agent。只需简单配置环境变量,您的通用 AI 助手瞬间化身专业量化分析师。
如果你的 Agent 是 Codex CLI / Claude Code / Cursor / Windsurf / Cline,除了本包的环境变量 + 脚本方式,还有一个 原生 MCP 通道:一个 stdio MCP server,把同一个 dispatcher 直接暴露成 tools/list / tools/call 工具,你的 Agent 不必再学 shell 脚本的调用形状。
两条通道并存,没有一条被弃用,而且它们是同一个后端接口的两个门:
GET /agent/skills + POST /agent/skills/execute 这两条路由。LG_AGENT_BASE_URL + LG_AGENT_TOKEN。 配好了本包,就等于配好了 MCP server,不用重新申请或配置任何东西。403、 remediation 全由后端裁定并原样返回;MCP server 自己不计算也不缓存任何授权判断。 换通道不会让同一个 token 多拿到或少拿到任何能力。⚠️ 它目前 还没发布到 npm( npm view @privora/mcp-server 返回 404),需要从仓库检出后用绝对路径接入。用法见仓库里的 mcp-server/README.md(安装、客户端配置块、12 个工具、每个工具的 annotations 披露信号——只读 / 破坏性——以及错误映射,以及两个值得在第一次调用前读的平台坑)。annotations 是 披露,不是安全边界:宿主可以完全忽略它们,真正且唯一不可绕过的授权判定始终在服务端 scope 校验。
该选哪条? 如果你的客户端原生支持 MCP,用 MCP —— Agent 少一层要学的东西。其它情况(Hermes、OpenClaw、自己写的脚本、CI 里直接 curl)继续用本包,它不会走。
每个账户的持仓数据在数据库中以 per-account 独立密钥密文存储。 这是防"库泄露 / 平台 admin 跨账户读取"的加密,不是 E2E 加密:
策略云端托管运行,无需您购买第三方行情 API,无需自建服务器维护 Cron 任务,无 Token 消耗税。策略触发后,毫秒级推送到您的飞书机器人或微信 Webhook。
核心功能详细说明资产盈亏巡航一键查询持仓明细、当日盈亏、历史收益率,数据由 privora.cn 闭环处理。云端自动盯盘设置预警条件(突破均线、涨跌幅、换手率等),触发即通知,7x24小时云端值守。多终端实时推送策略触发毫秒级推送到飞书、微信 Webhook,不错过任何交易信号。行情数据日线按市场段分别发布,均 🟢 生产可用:A 股( stock_day,沪深京 5500+ 股票 + 4 指数)、港股( stock_day_hk,如 00700.HK)、美股( stock_day_us,如 AAPL);分钟 K 线:A 股( stock_kline)+ 港股( stock_kline_hk),1/5/15/30/60 分钟;基金日 NAV( fund_day);场内基金(ETF/LOF)日线( fund_quote_day)+ 分钟 K 线( fund_kline,🟡 已建库尚未开放订阅);SGE 黄金日线( metal_day);财报事件( stock_forecast 业绩预告 + stock_express 业绩快报,11 年历史已回填)。详见下方「数据资产可用性」表。Python 策略回测 ✨用平台日线数据跑单股 / 多股组合回测,输出 Sharpe / 最大回撤 / 交易明细 / equity curve;结果持久化到 process_backtest_result,可通过 investment.stock.backtest.list 检索历史审计记录(平台已积累 44+ 次持久化回测)。模拟交易 (Paper Trading) ✨MARKET / LIMIT 两种委托类型,调度器驱动,模拟完整委托 → 成交 → 盈亏核算链路;账户按 user_name 唯一(DB-level UNIQUE),订单按 (user_name, client_order_id) 幂等,Agent 重复调用不重建。适合策略 6 阶段验证的最终纸面交易关卡。用户声音收集支持 Agent 代客户提交 Bug 和需求,无缝对接后台反馈系统。
发布模型说明:底层物理表按市场是合表存储的(同一张表可能物理上含多市场行),但 对外发布是按市场段分别发布为独立 DataAsset 的。
stock_day段族现已全市场覆盖并各自独立上线:A 股段 =stock_day,港股段 =stock_day_hk,美股段 =stock_day_us;分钟 K 线段族同理:A 股段 =stock_kline,港股段 =stock_kline_hk。三段各自独立发布、独立 asset id,调用时请用下表的 assetName,不要假设同一 asset 覆盖多市场。
DataAsset状态覆盖频率备注stock_day🟢 生产可用****A 股(沪深京)5500+ 股票日线 + 4 主要指数(沪深300 1B0300 / 上证综指 1A0001 / 中证A500 1B0510 / 深证成指 399001)日id=1; 000001 等 A 股 ticker 自动路由( segmentValues SH/SZ/BJ); 399001 自 2026-03-17 起停更,请求会返回 meta.benchmarkWarning``stock_day_hk🟢 生产可用港股日线(如 00700.HK)日id=204;数据新鲜(2026-07-16 校验通过)stock_day_us🟢 生产可用美股日线(如 AAPL)日id=206;由 stock_day_us_backfill_to_mc 任务从 PG 同步至 MCstock_kline🟢 生产可用A 股 1/5/15/30/60 分钟 K 线( interval_type 区分周期)分钟id=202;日线 + 日内同步,由 stock_kline_daily_sync 等调度维护stock_kline_hk🟢 生产可用港股 1/5/15/30/60 分钟 K 线分钟id=207;与 stock_kline 同结构,段独立stock_minutes⚪ 已弃用(旧) 分钟 K 线,已被 stock_kline / stock_kline_hk 取代—id=154;仅历史兼容保留,新集成请改用 stock_kline/ stock_kline_hk``fund_day🟢 生产可用公募基金日 NAV日 (T+1)数据延迟约 1 个工作日; 长期回测/算收益率必须用 adj_nav(复权净值),不能用 unit_nav(会被拆分/分红污染,且约 49% 行 adj_nav 为 NULL)——详见下方「 adj_nav 缺失信号」一节fund_quote_day🟡 数据已就绪,尚未开放跨团队订阅场内基金(ETF/LOF)价格日线;PG 单表不分区; market ∈ {ETF, LOF}—— 与 fund_day/ fund_codes 的 {E,O} 词表不同,跨表 join 只能用 fund_code; turnover 是成交额(元),不是换手率日source 分 akshare_hist_em(官方历史, is_final=true/ calibration_status='confirmed')与 fund_realtime_t0(当日 T+0 推导, is_final=false/ pending);两者交叉验证收盘价/成交量完全一致;详见下方「场内基金 K 线」一节fund_kline🟡 数据已就绪,尚未开放跨团队订阅场内基金(ETF/LOF)分钟 K 线, interval_type ∈ {1m,5m,15m,30m,60m}(无 1d,日线见 fund_quote_day)分钟PG 原生 RANGE 分区表(按 day_id), 保留期仅 3 天(非 MC 资产,不受 ODPS 分区自动注入影响); bar_time 是 VARCHAR 不是 timestamp; tick_count 量化稀疏度;详见下方「场内基金 K 线」一节metal_day🟢 生产可用SGE 黄金 / 白银日线日上海黄金交易所stock_forecast🟢 生产可用 (NEW 2026-06-22)A 股上市公司业绩预告;11 年历史 82,457 行已回填日财报季 (1/4/7/10 月底前后) 集中发布stock_express🟢 生产可用 (NEW 2026-06-22)A 股上市公司业绩快报;11 年历史 19,945 行已回填日比业绩预告更精确但发布更稀疏stock_dividend🟢 生产可用 (NEW v1.0.32)A 股上市公司现金分红事件;进入 portfolio.attribution 归因事件驱动除权除息日发布
对 Agent 的指导:调用 dataasset.list 看完整列表;标 🔴 / ⚫ / ⚪ 的资产请避免在策略里硬编码依赖(⚪ = 已弃用,改用其后继 asset)。 dataasset.metadata.get (2026-06-22 新上) 可查每张表的 lastUpdated / expectedUpdateCadence / cronExpression 来判断当前状态—— 2026-09 起这个 scope 已在默认 read-data 预设里,用默认预设建的 token 直接调即可,无需额外勾选。
📌 本节是 2026-07-17 的一次平台盘点快照,随时间推移可能与实际覆盖漂移。各已发布资产的完整覆盖范围、分市场明细、更新频率与数据起始日期,见持续维护的公开清单页: privora.cn/features/realtime-minute-data-coverage(按六类分组,含 A股/港股/美股/北交所分市场明细)。
本 skill 是通过 Bearer Token 对接 Privora 平台的能力。操作类别的副作用不同, 操作者负责按类别 scope token 并为需要的类别加入确认门槛:
user_name + client_order_id,同 key 重试返回同一记录)、marketplace subscribe(ON CONFLICT 返回已有订阅)、告警配置更新。 同输入多次调用只产生一次逻辑效果,可安全重试。process.ingestion.execute 触发已授权 python_script 运行并写入 process_backtest_result 表;scheduler-instance 的 redo / hold / resume / reset-priority 转移 trigger row 状态。 每次调用创建或修改持久化记录。schedule.job.plugin.webhook.trigger 与告警评估路径向操作者配置的外部端点(飞书 / 微信 / 通用 webhook)发送通知。 副作用在 Privora 之外,平台不可撤销。本 skill 部分暴露(其余需人在 platform UI 手动完成):
schedule.job.online / schedule.job.offline 可达,同样需要两步确认握手,见上方「确认门槛类」——这两个操作不是"不暴露"。schedule.job.plugins.save(详见下方作业插件小节, §调度作业字段契约)可全量替换一个作业绑定的插件列表(旧绑定先整体删除再写入新列表), 且不经确认握手——传入的数组即视为该作业插件的完整期望状态,遗漏的既有绑定会被静默清空,不是增量操作。调用前请先 schedule.job.plugins.list 确认当前绑定,再拼出完整数组。本 skill 不预先声明 任何操作是"agent-safe"——这个分类取决于操作者的风险偏好、agent 的可靠性、以及具体用例。 推荐姿势:只读 + 幂等写允许 agent 自主调用;流程状态转移和外发 webhook 建议先经过用户确认门槛(约定俗成,非平台强制);标记 confirmRequired:true 的高风险操作则由 平台强制 要求两步确认握手,不依赖操作者自律—— 但这道门防的是单次误触,不是他人审批:两步可以由同一个 agent 完成( approval_mode='self-confirm', approver_user 就是 requester 本人,见下方 §高风险操作确认握手);要真正的人工把关,只有两条路:不授予这些 scope,或走平台 UI 由管理员 approve/reject。另外请注意上面「Webhook 插件生命周期变更」是 例外:风险不低(全量替换、旧绑定先删),却不在 confirmRequired 名单内,调用前务必自行核实完整期望状态。
新建 token 的默认 scope 就能取数(2026-08-03 起)。点"创建"不改任何选项,你会得到
read-data 这一组:
text
dataasset.list dataasset.get dataasset.schema.get dataasset.metadata.get
dataasset.data.get dataasset.data.getRealtime marketplace.item.list
需要别的能力时,按场景挑一组(token 创建页有同名的场景按钮,点一下即可全选):
场景preset idscopes取行情 / 资产数据( 默认)read-data``dataasset.list dataasset.get dataasset.schema.get dataasset.metadata.get dataasset.data.get dataasset.data.getRealtime marketplace.item.list读取数据 + 管理市场订阅( 含写权限:订阅市场条目拿自己团队的 clonedAssetId 等;取消订阅会 删除 该团队克隆副本)subscribe-and-read``read-data 全部 + marketplace.item.subscribe marketplace.item.unsubscribe读仪表板read-dashboard``read-data 全部 + dashboard.list dashboard.get dashboard.data.get触发并追踪流程run-process``process.ingestion.list process.ingestion.get process.component.list process.ingestion.execute process.ingestion.execute.log.get配置指标告警(不含建通知通道)manage-alerts``metric.alert.list metric.alert.get metric.alert.create metric.alert.update metric.alert.toggle metric.alert.test实时告警(通道 + 规则,端到端)realtime-alerting``dataasset.{list,get,schema.get,metadata.get,data.get} datasource.list alert.channel.create plugin.webhook.send metric.alert.{list,get,create,update,patch,toggle,test,snooze,unsnooze,acknowledge}读写持仓与交易( 含写权限)portfolio``investment.stock.portfolio.{list,create,update} investment.stock.trading.{list,create} investment.stock.watchlist.list investment.stock.signal.{list,get} investment.fund.portfolio.{list,create} investment.fund.trading.{list,create} investment.gold.portfolio.{list,create} investment.gold.trading.{list,create}
三个查询入口(三处读的是同一份定义,不会互相打架):
你是谁去哪查人privora.cn/profile/tokens 创建 token 时的场景按钮AgentGET /agent/scope-presets —— 返回 {presets[], defaultScopes[], grantedScopes[]},每个 preset 带 scopes[]、 skillIds[] 和 satisfied(当前 token 是否已满足)AgentGET /agent/skills —— 全量 技能目录。每条带 granted(你现在能不能跑)、 scope(需要哪个 scope)、 params schema、 exampleInvocation。跑不了的条目额外带 presetsGrantingScope
GET /agent/skills过去只返回你 已有 scope 的技能,所以 scope 不足时你根本看不到目标技能存在, 只能靠猜 skillId。现在默认返回全量并用granted标注;要恢复旧行为传?granted=true。
scope 不足时不用猜:403 响应体直接给出 requiredScope、你当前的 grantedScopes、
哪个 preset 含它( presetsGrantingScope)以及去哪改( remediationUrl)。
skillId 写错时 400 响应体给 didYouMean[] 候选。
Token 使用建议:
read-data 就够;
要跑流程再加 run-process;agent 真的要下模拟单才加 paper.*(该命名空间由平台内部签发,
见下方"模拟交易"章节)。 不要为"以防万一"打包无关 scope。LG_AGENT_BASE_URL=https://privora.cn🛑 绝对不要让你的 agent 代替你 mint token。Token 创建是 operator 动作,不是 agent 动作。Agent 应该消费 operator 签发的 Bearer Token, 不应该 自己调
POST /api/subscription/tokens。
本 skill 的输出(行情数据 / 组合分析 / 回测报告 / 模拟交易 / 告警评估)是 供操作者审查的分析结果,不是投资建议、不是交易指令、也不能替代持牌财务咨询。
装完 skill 想立刻看看能干什么? 打开 privora.cn/marketplace:
lastUpdated / 数据源 / cron 表达式等)marketplace.item.subscribe 不在默认 read-data 预设里,预设场景按钮里只有 subscribe-and-read 带它,创建 token 时请选 subscribe-and-read 预设(或手动勾选 marketplace.item.subscribe / marketplace.item.unsubscribe 这两个 scope),用默认 read-data 预设建的 token 调这一步会 403——然后调 marketplace.item.subscribe(幂等——哪怕你之前已经订阅过,重复调用同一个 item 也照样成功),响应体里的 clonedAssetId 就是你自己团队里那份克隆资产的数字 id,直接拿去跑 §4 First Call Recipe(2 步 / 约 1 分钟)验证 Bearer Token 对同一资产能跑通。 兜底路径:如果响应丢了这个字段、或你不想再调一次 subscribe, dataasset.list 里按 tags 含 Subscribed 也能扫到同一份克隆资产的 id为什么先看再注册:Privora 是投研工作流平台,"你的数据是否值得订阅"应该 30 秒能判断 —— 不需要注册墙。marketplace UI 是 发现工具,Bearer Token 是 同一批数据的程序化访问入口,两者对应关系明确。
https://privora.cn/profile/tokensLG_AGENT_TOKEN在您使用的 Agent 终端(如 Hermes、Claude Code、GitHub Copilot 或 OpenClaw)中注入以下环境变量:
bash
export LG_AGENT_BASE_URL="https://privora.cn"
export LG_AGENT_TOKEN="***"
公开版主要走以上 Bearer Token 方式;如果 Agent 只是浏览 marketplace / 预览已发布看板 & 资产,也可以走 匿名模式(不需要 token,见下方 §🌐 匿名预览)。session cookie / CSRF 兼容调用不支持。
现在,您可以直接用自然语言向您的 Agent 下达指令了!
最容易踩的坑:URL 里的 {id} 必须是 数字型 asset ID(如 42), 不是 asset 名字(如 fund_day / stock_day)。传成名字后端 Spring 转 Long 失败会返回 500——错误信息不会明确告诉你原因。
正确的 recipe(用默认 read-data 预设的 token 即可全部跑通):
bash
# Step 1: 先 list 拿数字 id ← 别跳过这步
curl -H "Authorization: Bearer $LG_AGENT_TOKEN" \
https://privora.cn/api/data-assets | jq '.data[] | {id, assetName}'
# 输出示例:
# {"id": 42, "assetName": "fund_day"}
# {"id": 8, "assetName": "stock_day"}
# {"id": 15, "assetName": "stock_dividend"}
# Step 2: 用数字 id (不是 assetName!) 查实际数据
curl -H "Authorization: Bearer $LG_AGENT_TOKEN" \
"https://privora.cn/api/data-assets/42/data?page=1&size=10"
Step 1 返回 []? 这对刚注册、没有自有资产、也没有订阅任何 marketplace 条目的团队是预期结果,不是 bug——你的团队此时确实没有任何 dataasset.list 能看到的资产。见上方 §0 的 subscribe → clonedAssetId 路径:调 marketplace.item.subscribe 需要持有该 scope 的 token(预设场景按钮里只有 subscribe-and-read 带它,默认 read-data 预设没有),拿到 clonedAssetId 后回来重跑本节 Step 1,这时就能在列表里看到刚订阅的克隆资产了。
(可选)Step 3:查富元数据—— GET /api/data-assets/{id}/metadata(对应 skill dataasset.metadata.get) 2026-09 起已在 Token Management 页面 read-data 默认预设里,用默认预设建的 token 直接调即可:
bash
curl -H "Authorization: Bearer $LG_AGENT_TOKEN" \
https://privora.cn/api/data-assets/42/metadata
Agent 侧用 lg_agent_exec.sh 调用同理(v1.0.45 起支持命名参数扁平写法,不用手拼 JSON):
bash
scripts/lg_agent_exec.sh dataasset.list
scripts/lg_agent_exec.sh dataasset.data.get id=42 filter_column=stock_num filter_value=600519
# scripts/lg_agent_exec.sh dataasset.metadata.get id=42 ← 可选;2026-09 起已在默认 read-data 预设里,直接跑即可
id 必须是数字(先 dataasset.list 拿到再传,不是资产名字如 fund_day)。想看某个 skill 接受哪些 key,先 scripts/lg_agent_list.sh describe dataasset.data.get 看 schema + 示例,再照着填。
如果你已经踩到 500:不用改代码逻辑,只需把 {name} 换成对应的数字 id 即可。数据资产的可用列表见下方「 数据资产可用性」表 —— 那里的名字对应 dataasset.list 返回的 assetName 字段,需要先 list 拿到本 team 里对应的数字 id。
如果 Agent 只是想 浏览 marketplace 或预览已发布的看板/数据资产/流程——比如帮用户看看 Privora 有什么数据源、有哪些现成看板可订阅、某个流程做什么用—— 不需要 Bearer Token 也能直接跑。这条通路和 privora.cn/marketplace 页面上未登录访客看到的内容是 同一套数据,只是把它变成 machine-readable 的 skill 调用。
留空 LG_AGENT_TOKEN 或直接不传 Authorization header 即可:
bash
# 无 token 调用 —— 直接返回 mode:"anonymous" + 10 个可用 skill
curl https://privora.cn/agent/skills
# 无 token 拿 marketplace 列表
# Windows Git Bash 提醒:curl.exe 是原生 Windows 程序,MSYS2 会按本地 ANSI 代码页重编码命令行参数,
# 如果把下面的 body 换成含中文/非 ASCII 的内容(如搜索关键字),-d '...' 会被静默改坏——改用 --data-binary @file。
printf '%s' '{"skillId":"marketplace.item.list"}' > /tmp/lg_body.json
curl -X POST https://privora.cn/agent/skills/execute \
-H "Content-Type: application/json" \
--data-binary @/tmp/lg_body.json
# 无 token 拿某个已发布看板的 widget 数据(同上:非 ASCII 内容一律 --data-binary @file,不要用 -d)
printf '%s' '{"skillId":"dashboard.data.get","params":{"pathParams":{"id":"<published-dashboard-uuid>"}}}' > /tmp/lg_body.json
curl -X POST https://privora.cn/agent/skills/execute \
-H "Content-Type: application/json" \
--data-binary @/tmp/lg_body.json
响应体的 mode 字段会明确标 "anonymous", grantedScopes 列出下方 10 个允许的 skill。
skillId用途marketplace.item.list列出所有可订阅的 marketplace 条目(看板 / 资产 / 流程)—— discovery 入口dashboard.get按 id 拿某个已发布看板的元数据 + widget 定义dashboard.data.get一次拿某个已发布看板所有 widget 的数据dataasset.get拿某个 allowSubscription=true 资产的详情dataasset.schema.get拿该资产的列 schemadataasset.metadata.get拿该资产的富元数据( lastUpdated / expectedUpdateCadence / cron / 数据源描述等 20 字段)dataasset.data.get拿该资产的历史数据(预览有效范围内)dataasset.data.getRealtime拿该资产的实时镜像数据(若配置了 realtime mirror)process.ingestion.get拿某个 allowSubscription=true 流程的结构( 不含 stepCfg 源码)process.component.list列平台可用的步骤组件类型(rendering diagram preview 用)
只读白名单: 仅上表 10 个 skill 可调,其余 skill(包括其它只读 GET,如 investment.stock.portfolio.list / dashboard.list / dataasset.list)无论是否存在都返回 403 missing-scope。任何写操作(subscribe / create / update / delete)同样 403;哪怕手工构造匿名 PUT/POST 到底层 /api/**,Node 代理层也会先 401 拦截,不会到 Spring。
每 IP 限流(三桶,任一超限即 429):
桶范围限额429 响应体通用桶所有匿名 skill60 / IP / 分钟{"success":false, "message":"Too many anonymous agent requests, ..."} (无 bucket 字段)数据爆发桶dataasset.data.get + dataasset.data.getRealtime10 / IP / 分钟{"success":false, "traceId":"...", "message":"Anonymous data-fetch burst-limit exhausted (10/min per IP). ...", "bucket":"burst"}数据日总桶同上100 / IP / 天{"success":false, "traceId":"...", "message":"Anonymous data-fetch daily budget exhausted (100/day per IP). ...", "bucket":"daily"}
收到 429 时读 response.body.bucket 判断: "burst" 等 60 秒重试; "daily" 今日不再放行该 skill,建议引导用户注册 Bearer Token;无 bucket 字段则是通用 60/min 桶命中。
Rate-limit 存储: v1.0.37 起三桶都存 Redis sorted sets ( anonratelimit:{burst,daily,general}:<ip>),通过 Lua 原子脚本实现 sliding window。fleet 内所有 Node worker + 所有 host 共享一份 counter,反爬承诺 真的 成立。 Redis 不可用时 fail-open:静默放行 + ERROR 日志 event:"rate-limit-redis-fail",反爬承诺仅在 Redis 健康时有效(运维监控 fail-open 日志识别异常)。
preview token 服务端自动签:不用你手工去 /preview-token 拿;Node 按 skill 类型选:dashboard 类( dashboard.get / dashboard.data.get) 要求调用方在 params.pathParams.id 里传目标看板 id,Node 会用该 id 签 dashboardId-bound token;未传 id 或其它 skill 一律降级为 standalone sentinel(等同 dataasset 类行为)。
无效 Bearer 不降级:如果传了 Authorization: Bearer <bogus>,返回 401 而 不会 悄悄退回匿名模式给你部分数据。要匿名就别传 header。
无跨租户 leak:所有资产读都走 canReadAsset gate;只有 allowSubscription=true 的资产/看板/流程会被返回,其它一律 404。跟浏览器 /marketplace 未登录访客看到的是同一套子集。
Dashboard-scoped token 不能跨团枚举:dashboard-A(发布者 = team-A)签的 preview token 无法通过 dataasset.metadata.get 读到 team-B 的资产元信息,哪怕 team-B 资产 allowSubscription=true。仅 standalone sentinel token 可读所有公开挂牌资产。
数据获取类 skill( dataasset.data.get / dataasset.data.getRealtime / dataasset.get / dataasset.metadata.get / process.ingestion.get)在匿名模式下 服务端会自动应用以下约束,参数会被静默改写或剥离——不是 400,你的 curl 依然能正常拿到响应,但拿到的不是你请求的形状:
page=1, size=25:传任何其他值都被服务端硬覆盖。响应 pageSize=25, currentPage=1。想拿更多请订阅后用 Bearer Token 调。filterColumn / filterValue / filterOp / orderBy / orderDirection 一律置 null 再进服务层。想按条件过滤请订阅后再来。dataasset.get): dataSource / realtimeDataSource / businessOwner / technicalOwner / teamName / jobCode / createdBy / createdDate / updatedBy / updatedDate 一律返回 null。Metadata map 中 teamName / dataSource / jobCode / createdBy / createdDate / sourceDescription 也被剥离。process.ingestion.get 的 stepCfg 被剥离:匿名调用者拿不到 process step 的源码 / SQL / Python 内容。totalElements 是哨兵值 0,不是真实总数:匿名调用不消耗后端 COUNT(*) 查询(防止 JDBC 池 DoS)。分页导航请以 data.length 为准。配合分页固定 + rate limit,匿名调用者理论上每 IP 每天最多拿到 2,500 行数据(100 次 × 25 行)——真的想跑分析请注册。
totalElements 反映真实行数 —— 匿名调用永远返回 0,这是设计意图,不是 bug。filterColumn=... —— 参数会被静默丢弃,返回的是无过滤的前 25 行,不是过滤后的结果。技术契约锚点(review 用):匿名 skill 白名单 = Node
app.js的ANONYMOUS_SKILL_SCOPES常量;匿名 rate limit =canAnonymousAgentCall(60/min) +canAnonymousDataFetchCall(10/min burst + 100/day daily);preview + HMAC 验证 =docs/auth-flow-invariants.md§1 + §2.6 + §2.6.1;能力锁定实现与残留 test 缺口 =docs/plans/2026-07-07-anon-preview-dataasset-lockdown.md。
匿名浏览完,如果用户想真正订阅一个看板 / 用私有数据 / 跑回测,需要 注册并领 token:
https://privora.cn/register( marketplace.item.subscribe 是 🟡 写操作,不在匿名 scope 里)auth.user.register skill(也在匿名 scope 之外 —— 需要一层 signup 意图确认,见 §用户注册 & 反馈)您: “帮我查下今天的账户盈亏情况。”
Agent(调用
dataasset.data.get): “为您同步 privora.cn 的最新分析结果: 💰 当日盈亏: +319 元 | 累计浮动: -19,135 元 📊 持仓明细:
- 中国核电:+2.06%
- 永和股份:-32.45%
- 中国联通:-16.25%”
您: “帮我监控贵州茅台,只要突破MA20均线就通知我。”
Agent(调用监控接口): “✅ 已在云端成功创建监控任务:
- 标的:贵州茅台 (SH600519)
- 条件:价格突破 MA20
- 通知:飞书/微信推送 任务将在 Serverless 云端静默运行,触发时您将立刻收到推送。”
bash
# 触发执行(异步),记下返回的 executionId
# 自定义 CLI 参数直接当 flat key 传(key 以 - / -- 开头,与 process.ingestion.execute
# 的 Map<String,String> body 逐字对应)。后端会自动注入 `-f <procName>` —— 不用自己传 -f。
RESP=$(scripts/lg_agent_exec.sh process.ingestion.execute id=123 \
-start_date=20260419 -end_date=20260420 --env=dev)
EXEC_ID=$(echo "$RESP" | jq -r '.executionId')
# 轮询日志,直到 completed=true
OFFSET=0
while :; do
LOG=$(scripts/lg_agent_exec.sh process.ingestion.execute.log.get \
id=123 executionId="$EXEC_ID" offset="$OFFSET")
echo "$LOG" | jq -r '.logLines[]'
[ "$(echo "$LOG" | jq -r '.completed')" = "true" ] && break
OFFSET=$(echo "$LOG" | jq -r '.nextOffset')
sleep 1
done
echo "exitCode=$(echo "$LOG" | jq -r '.exitCode')"
返回: status 由 running 过渡到 completed 或 failed, exitCode 为脚本退出码, logLines 为增量日志行。
您: “用双均线(5日/20日)对茅台 SH600519 过去三年跑个回测”
在平台新建一个 python_script 流程节点,脚本如下( lg_utils 已预装):
💡
stock_day回测用现成的run_stock_day_backtest就好——它已经把列名大小写(STOCK_NUM/OPEN_PRICE/CLOSE_PRICE)和日期格式(day_id的YYYYMMDD)配好了,别再手动传price_columns={“open”:”open_price”,...}或 ISO 日期,那些是 2026-04-21 踩过的坑。
python
from lg_utils import get_variable
from lg_utils.backtest_examples.dual_ma import DualMA
from lg_utils.backtest_examples.stock_day import run_stock_day_backtest
result = run_stock_day_backtest(
strategy=DualMA(fast=5, slow=20),
stock_num=”600519”,
start=”20220101”,
end=”20241231”,
initial_cash=1_000_000,
commission_bps=3, slippage_bps=1,
benchmark_asset=”stock_day”, # 可选:跟某只指数/股票对比
benchmark_filter_column=”STOCK_NUM”,
benchmark_filter_value=”000001”,
)
print(result.summary())
result.export_to_context(“maotai_ma520”) # stdout 日志快照
result.persist(name=”maotai_ma520”) # 持久化到 process_backtest_result 表
组合回测(共享现金池、多标的同时跑):
python
from lg_utils.backtest_examples.stock_day import run_stock_day_portfolio_backtest
from lg_utils.backtest_examples.dual_ma import DualMA
result = run_stock_day_portfolio_backtest(
strategies={“600519”: DualMA(5, 20), “000001”: DualMA(10, 30)},
stock_nums=[“600519”, “000001”], # 决定 size='all' 结算先后
start=”20240101”, end=”20241231”,
initial_cash=1_000_000,
)
# result.metrics[“per_asset”] 给出每只股票的贡献度/回撤/交易数
任务日志里会出现:
text
=== Backtest Summary ===
asset : stock_day
period : 20220101 ~ 20241231 (bars=725)
total_return : 23.1500%
sharpe : 0.8412
max_drawdown : 18.2300%
num_trades : 14
win_rate : 57.1429%
__LG_BACKTEST_RESULT__:maotai_ma520:{"metrics":...,"trades":...}
完整 JSON(含 trades / equity_curve)会被下游节点或监控面板消费。
您: "帮我配个告警,招商银行股价跌破 30 通知我。"
Agent 调用流程(之前 6 步深埋,2026-06-05 起 1 步):
bash
# 1) Agent 帮用户订阅相关 dashboard
RESP=$(scripts/lg_agent_exec.sh marketplace.item.subscribe itemId=dashboard-china-merchants-bank-watch)
# 2) 从 response 拿到本租户的 cloned dashboard ID
DASH_ID=$(echo "$RESP" | jq -r '.clonedDashboardId')
# 3) 构造 1-click deeplink — Agent 把这个 URL 给用户
DEEPLINK="https://privora.cn/dashboards?selectId=${DASH_ID}&openAlerts=true"
echo "请打开此链接配置告警:${DEEPLINK}"
用户点链接进去,metric alert modal 自动打开——已经对准刚订阅的 dashboard,剩下用户填阈值 + 选 webhook 渠道 finalize 就完。 user-in-the-loop 边界保留(敏感操作仍需用户在 web 上确认),但 5 步导航 + 选 dashboard + 翻 toolbar 找 "Alerts" button 这些都省了。
这是平台活跃用户反馈最集中的需求——以前的路径是:订阅 → 跳到 dashboard 列表 → 找到目标 dashboard → 打开 toolbar → 找 "Alerts" button → (第一次还要去 /datasources 配 webhook,回来再继续)→ 配置 → 保存。这次更新把这条路径压到 1 步。
您: "用模拟账户跑一笔 600519 的市价买单 100 股,看看现在能不能成交。"
这条路径目前不能靠通用 Agent + 本包 Bearer Token 一次调用走完。 本节曾给出一段示例,调用 paper.account.create / paper.order.place / paper.order.get 三个 id —— 均不存在于 catalog,跑起来只会拿到 400 Skill not found(#787)。而且光改 id 也走不通: paper.* 是 保留 scope 命名空间——自己去 个人设置 → Token 管理 创建一个带 paper.account.read / paper.orders.write 的 PAT,后端会直接拒绝 400 RESERVED_SCOPE,不存在"自助签发"这条路。
真实能力叫 investment.paper.*(见下方「investment.paper.* — 模拟盘交易」小节,含真实 skillId investment.paper.account.get / investment.paper.orders.submit / investment.paper.orders.list / investment.paper.positions.list),但入口不是 scripts/lg_agent_exec.sh,而是 一个 Process 里的 python_script 节点,节点里用 lg.paper.* SDK( lg.paper.get_account() / lg.paper.submit_order(...) / lg.paper.get_orders(...))发起调用。Process 起跑时,后端会给这个节点自动注入一个 scope 限定的短期 Bearer( paper.orders.write paper.account.read dataasset.read)——Agent 不需要、也不能自己去申请这个 token。
该怎么做:
starter_paper_trade_strategy—— marketplace.item.subscribe 一键复制到你的 tenant,改写里面的 lg.paper.submit_order(...) 调用即可,不需要从零搭 Process。process.create( kind: "run_python")建一个含模拟下单脚本的步骤, process.ingestion.execute 跑起来——节点内的 lg.paper.* 调用由平台自动授权,不经过本包的 Bearer。investment.paper.*:只能由平台管理员按"策略绑定模拟账户"流程走 UI 手动铸一个 process-execution token 再转交给你的 Agent—— 这不是本包能自助完成的操作,不要承诺用户"改个 id 就行"。支持涨跌停 / 停牌 / suspended-stocks 信号、scheduler-driven 撮合。适合的 use case:策略上真实交易前 12 个月 paper trade 验证(per 6 阶段量化研究流水线最终关卡)——但入口是 Process 编排,不是本包 Bearer 的直接调用。
scripts/lg_agent_exec.sh 调用)#公开版 skill 覆盖 4 类操作,见 §🛡️ Scope & Operator Responsibility 完整的 read / idempotent-write / workflow-transition / outbound-webhook 分类。 大部分 删除、撤销等破坏性/管理类操作不在本 skill 范围内,需通过 platform UI 或 admin 工具完成; 例外 是标记 🔴/🟡 且
confirmRequired:true的一小撮 workflow-transition 类操作(process.ingestion.delete等 14 个,见下方 §高风险操作确认握手),这些 Bearer token 可达,但必须先完成两步确认握手——单次调用不会直接执行。系统级审批(管理员 approve/reject 一个陌生人发起的请求)仍然不在本 skill 范围内,需要 platform UI。 风险标记:🟢 low / 🟡 medium / 🔴 high。所有GET技能默认对会话用户开放;写操作需显式授予 scope。
📦 Request shape (v1.0.45+): 命名参数扁平写法 —— 直接用
key=value传参,不用手拼 JSON:bash
scripts/lg_agent_exec.sh dataasset.data.get id=42 filter_column=code filter_value=000135等价于旧 envelope 形式
{"skillId":"dataasset.data.get","params":{"pathParams":{"id":42},"query":{"filter_column":"code","filter_value":"000135"}}}——网关会按每个 skill 的 path 模板自动把 flat key 分类到pathParams/query/body。key=value一律当字符串(保留stock_num=000135这类前导零);数字/布尔/数组用key:=value(如qty:=100)。 旧 envelope 形式 100% 继续可用,两种写法可以在同一次调用里混用(例:数组 body 用--json,path 参数用 flat key)。想看某个 skill 接受哪些 key,跑scripts/lg_agent_list.sh describe <skillId>。完整规则 + 历史踩坑 + envelope 手工写法见文末 §高级 / 兼容性附录。
14 个标记 confirmRequired:true 的技能对 Bearer token 可达( process.ingestion.delete、 schedule.job.{online,offline,delete}、 schedule.instance.{redo,hold,kill,cancel,force_start,mark_success}、 subscription.token.revoke、 metric.alert.delete、 investment.paper.account.reset、 team.python.method.delete),但 单次调用永远不会直接执行——第一次调用总是返回 HTTP 409,必须完成两步握手才能真正执行。(另有 6 个 investment.{stock,fund,gold}.{portfolio,trading}.delete 目前设计上暂不对 token 开放,见下方「不可达」说明。)
① 第一次调用(不带 approvalId)→ 总是 409:
bash
scripts/lg_agent_exec.sh process.ingestion.delete id=42
json
{
"success": false,
"traceId": "...",
"message": "Approval required for high-risk action",
"requiredScope": "process.ingestion.delete",
"confirmRequired": true,
"approvalId": 27,
"expiresAt": "2026-08-18T19:30:00",
"skillId": "process.ingestion.delete",
"next": "self-confirm-or-await-admin-then-resend",
"nextAction": {
"method": "POST",
"url": "/agent/skills/execute",
"body": {
"skillId": "process.ingestion.delete",
"params": { "pathParams": { "id": 42 }, "approvalId": 27 }
}
},
"hint": "approvalId must be nested inside \"params\" (params.approvalId), never a sibling of \"skillId\" — see nextAction for the exact resend request. Optionally POST /api/agent/approvals/27/confirm ahead of time to self-confirm without waiting for the resend (idempotent — resending via nextAction afterwards still succeeds either way). Window closes at expiresAt."
}
② 把 ① 的内容原样展示给人类,等待其同意(这是防误操作装置,不是授权检查——真正的权限仍然是 token 的 scope;见 §🛡️ Scope & Operator Responsibility)。
③ 重发,把 approvalId 嵌进 params 里 → 200:
nextAction.body 就是可以直接拿去重发的完整请求体—— 逐字 用它,不要自己重新拼:
bash
# Windows Git Bash 提醒:--data '...' 把 body 放进命令行参数,curl.exe 是原生 Windows 程序,
# MSYS2 会按本地 ANSI 代码页重编码 argv——如果 approve/reject 理由等字段含中文/非 ASCII,
# 内容会被静默改坏。优先用下面的 lg_agent_exec.sh;必须裸 curl 时改用 --data-binary @file。
curl -X POST "$LG_AGENT_BASE_URL/agent/skills/execute" \
-H "Authorization: Bearer $LG_AGENT_TOKEN" -H "Content-Type: application/json" \
--data '{"skillId":"process.ingestion.delete","params":{"pathParams":{"id":42},"approvalId":27}}'
优先用 lg_agent_exec.sh 的扁平写法(也修好了上面这个 Windows 编码坑), approvalId 作为一个 flat key 传即可(网关不会把它当作业务参数转发下游,也不会因为它出现在 params 顶层而拒绝识别):
bash
scripts/lg_agent_exec.sh process.ingestion.delete id=42 approvalId:=27
**唯一会生效的判据是 params.approvalId。**踩坑记录(issue #74,2026-08-18 修复):
{"skillId":"process.ingestion.delete","approvalId":27,"params":{"pathParams":{"id":42}}}—— approvalId 和 skillId 同级、不在 params 里, 网关只会把顶层的 pathParams/ query/ body 折进 params, approvalId 不在这个白名单内,会被静默丢弃,网关认为你还没有 approvalId,重新建一条 pending 审批( approvalId 会一次次往上涨),永远卡在 ①。confirm:true(不带 approvalId,或 approvalId 放错位置)—— confirm 字段本身 不会被读取,只是网关内部保留字(防止它泄漏到下游请求),加不加、真不真都不影响判定。params.approvalId(嵌套在 params 内)才会被网关识别为「已经有一个待处理的审批」。expiresAt 是这条审批任务的过期时间(TTL 10 分钟)——超时后 approvalId 失效,重发 ③ 也会 409,须重新走 ①。
(可选)提前自确认: agent-skill/scripts/lg_agent_approval.sh confirm <approvalId> 直接命中 POST /api/agent/approvals/{id}/confirm(同一 requester 才能确认自己发起的审批),可以在等待人类确认期间提前调用;之后按 ③ 正常重发仍会成功(幂等)。这 不是 必需步骤——③ 本身已经原子地完成"自确认 + 消费", confirm 只是给需要分两次操作的场景用的便利工具。管理员批准/拒绝走 lg_agent_approval.sh approve|reject(需要 userLevel>=8,非管理员 token 调用会 403——这不是本 skill 的入口,除非你就是管理员)。
不可达(暂不支持): investment.{stock,fund,gold}.{portfolio,trading}.delete(6 个)目前设计上对 token 模式仍然 409——它们背后共用的几个 handler 目前只能声明单一权限校验字符串,无法表达"以下 12 个 scope 名中的任意一个",属于已知限制,不是本次修复范围。
本节只覆盖上面 确认握手 流程里出现的 409——即 confirmRequired:true 技能在 token 模式下走 ①/②/③ 过程中收到的 409。判别请优先看 status 字段;只有响应没带 status 时才退回看 message 原文。
情形信号可恢复?技能不在可握手集合内(即那 6 个 investment.*.delete)无 status; message 原文为 "High-risk approval flow for token mode is not enabled yet for this skill"否 —— 终态,换个技能/等待后端扩大集合,重试无用① 第一次调用的响应带 approvalId + nextAction是 —— 照 §②③ 走握手status:"expired"``message 为 "approval task expired"是 —— 从 ① 重新开始status:"rejected" / "consumed" / "unknown"(resend 时先命中 self-confirm 端点,这是 token 模式下 最常见 的一类)message 原文为 "approval task is not pending",仅靠 status 字段区分三种原因;同一状态族在更窄的竞态窗口下(confirm 成功后、consume 执行前被并发消费)也可能改由 consume 端点报出 message 原文 "approval is not approved"——两种 message 字符串对应同一组 status 取值,都只能靠 status 区分是 —— 从 ① 重新开始"approval task not found" / "approval task owner mismatch" / "approval skill mismatch" / "approval team mismatch" / "approval scope mismatch" / "approval payload hash missing or mismatched"只有 message,无 status否 —— 调用方自身的 bug,不是重试能解决的( approvalId 传错、跨用户/跨团队复用、resend body 与①不一致等)PROCESS_NAME_EXISTS( process.create / process.pipeline.build)code:"PROCESS_NAME_EXISTS"与确认握手无关,非破坏性——换个 name 重试即可
范围声明:session 模式(Cookie 会话)另有一条独立的幂等冲突 409( app.js:2854,仅在 mode !== 'token' 时触发),本节 不覆盖——它对本 skill 的 Bearer token 使用者结构性不可达(公开版 skill 只支持 Bearer Token 模式,见 §Security Notes),是一个本 PR 不修的、独立的既有未文档化缺口,这里明确写出原因而不是略过不提。
skillIdmethod功能风险process.ingestion.listGET列出所有流程🟢process.ingestion.getGET根据 id 获取流程详情🟢process.ingestion.executePOST异步触发流程执行(返回 executionId)。 body 接收自定义 CLI 参数,如 {"-start_date":"20260419","--env":"dev"}。后端自动注入 -f <procName>,不要自己传 -f。🟡process.ingestion.execute.log.getGET按 executionId 拉取日志+状态,支持 offset 增量轮询。记录持久化在 process_execution 表 + 磁盘文件,重启不丢。🟢process.component.listGET列出当前团队可用的步骤组件(含 Markdown 使用说明)🟢process.pipeline.buildPOST一次性创建完整 pipeline(节点+组件+边)。 python_script 节点的 stepCfg 必须使用执行器字段名 "script"(不是 "pythonScript"),例: {"script":"import sys\nprint(sys.version)","requirements":"pandas"}。 process.component.list 返回的 form-schema 中的显示名 pythonScript 是 UI 表单专用名,不等于执行器运行时 JSON key——混用会导致执行时报 "Python script is empty or NULL"(2026-07-10 incident,已在后端加翻译层向前兼容,但规范写法仍推荐用 script)。🟡process.createPOST业务字段封装层( POST /api/ingestions/create-simple),比 process.pipeline.build 更简单:不需要知道 node id / stepSeq / aftId/ sStep/ nStep/ fStep 拓扑字段 / 画布坐标 / 执行器 stepCfg 信封——只填 {name, description?, steps:[{kind, label?, ...}]},后端自动组装成 process.pipeline.build 同款请求并复用同一条持久化路径。 kind 是封闭枚举,只支持 单链路顺序执行(不支持分支/for 循环/if 节点,那些场景仍用 process.pipeline.build)。详见下方 process.create 详情。🟡process.pipeline.updatePUT全量更新已有 pipeline( PUT /api/ingestions/{id},同形 BuildPipelineRequest)。 nodes 省略=仅改名/描述,保留现有步骤; nodes=[] 显式清空; nodes=[...] 全量替换。每次 PUT 自动写一条 dacp_meta_proc_version,可 /versions/{n}/restore 回滚。legacy team_name IS NULL 的流程会直接 403,需先 backfill。 **想只改一个步骤的 label/conf/remark 而保留 DAG?用 process.pipeline.update_node PATCH,避免 aftId 等拓扑字段被默认值 "-1" 误清空。想只改流程名称/描述?用 process.pipeline.patch_meta。**🟡process.pipeline.update_nodePATCH部分更新单个步骤( PATCH /api/processes/{procId}/steps/{stepId})。 字段掩码语义:仅 stepLabel / stepConf / remark 三个安全字段可被更新;缺失字段、显式 null、 以及空字符串 "" 都视为"跳过"( 不 清空)。 严格拒绝: aftId / sStep / nStep / fStep(DAG 拓扑)出现在 body 即返回 HTTP 200 success:false, code:"TOPOLOGY_FIELD_REJECTED" —— 要改变 DAG 拓扑请用 PUT process.pipeline.update 全量替换所有节点。 stepName / stepSeq / parentId 静默丢弃。 要清空 stepLabel/stepConf/remark 字段也必须走 PUT 全量替换 —— PATCH 设计为"只增改、不清空"。 **此 skill 需要 process.pipeline.update_node scope(独立于 process.pipeline.update),现有 token 须重新签发方可使用。**🟢process.pipeline.patch_metaPATCH部分更新流程级别元数据( PATCH /api/ingestions/{id}/meta)。 字段掩码语义:仅 procLabel / procDescr 两个安全字段可被更新;缺失字段、显式 null、 以及空字符串 "" 都视为"跳过"( 不 清空)。 严格拒绝: nodes(拓扑变更)/ procName(标识符)/ creater(归属)/ teamName 出现在 body 即返回 HTTP 200 success:false, code:"FIELD_NOT_PATCHABLE" —— 要改名称/DAG 结构请用 PUT process.pipeline.update。 **此 skill 需要 process.pipeline.patch_meta scope(独立于 process.pipeline.update),现有 token 须重新签发方可使用。**🟢
⚠️ 推大
stepCfg/ pipeline 载荷(process.pipeline.build/process.pipeline.update/process.pipeline.update_node/process.create)之前请先看这条(v1.0.56 起):POST /agent/skills/execute是这几个 skill 共用的唯一 dispatcher,后端专门为它把 JSON body 上限放宽到 262,144 字节(lib/body-parser-limits.js的LARGE_JSON_LIMIT='256kb', 平台大多数路由是 100KB 默认值)——这也是为什么这几个 skill 的 payload 会比大多数 skill 更容易撞到本节要说的这条 更窄 的本地限制:scripts/lg_agent_exec.sh/lg_agent_approval.sh把请求体和凭据一起放进同一条 curl-K -配置流发送,转义后的那一行不能超过 102,400 字节—— 引号密集的 JSON(stepCfg/SQL/脚本正文常见)转义后大约翻倍,所以 原始 body 约 77KB 起就可能 撞上这条本地限制,即使后端本可以收下(262,144 字节上限还没到)。这条限制在 curl 7.81–8.1.x (Ubuntu 22.04、Debian 12 的系统自带版本)上是 curl 自己真实的配置行上限;curl 8.2.0(2023-07) 起该上限被 curl 官方抬到 10 MiB,但这个 wrapper 不探测本地 curl 版本,对所有宿主统一取较低的 那个数字(换取"同一份脚本到处行为一致"),所以在新版 curl 上这条 102,400 是 wrapper 自己的保守 选择,不是 curl 的限制。 遇到时的可行做法:把大stepCfg拆成多次process.pipeline.update_nodePATCH 调用,或改走平台网页 UI 直接编辑;不要指望升级本机 curl 能绕开(本 wrapper 不会因此放宽)。 公网privora.cn入口不受影响——那里 nginx 在约 5–8KB 就已经 414(见下方"最近更新")。
process.create 详情路径: POST /api/ingestions/create-simple,scope process.create(独立于 process.pipeline.build,现有 token 须重新签发方可使用)。
为什么需要它: process.pipeline.build 要求调用方懂平台内部的 DAG 拓扑(node id、 stepSeq、 aftId/ sStep/ nStep/ fStep 边字段、画布 x/y/width/height)和执行器 stepCfg JSON 信封格式。 process.create 只暴露业务字段——一个有序、有类型的步骤列表;封装层在服务端补平台管道,不生成任何业务代码(SQL/Python/消息文本仍由调用方自己写)。
请求体:
json
{
"name": "fund_dividend_wrapper_test",
"description": "可选描述",
"steps": [
{ "kind": "run_sql", "label": "create tables", "sql": "CREATE TABLE IF NOT EXISTS ...", "dataSourceName": "pg_main" },
{ "kind": "run_python", "label": "fetch akshare", "script": "import akshare as ak\nprint(ak.fund_fh_em())", "pipRequirements": ["akshare"] },
{ "kind": "run_python", "label": "tushare enrich", "script": "import tushare as ts\n# ...", "pipRequirements": ["tushare"] },
{ "kind": "print_summary", "label": "done", "message": "fund_dividend ingest complete" }
]
}
kind 封闭枚举,映射到真实平台组件(已核实执行器 *StepMeta 字段名,不是猜的):
kind平台组件必填业务字段可选字段组装出的 stepCfg 信封run_sql``sql``sql、 dataSourceName—{"sql":..,"dsName":..,"needSplit":"true"}``run_python``python_script``script``pipRequirements(字符串数组,pip 包名){"script":..,"requirements":".."}( requirements 由 pipRequirements 换行 join;未提供则省略该 key)。 注意 key 是 script 不是 pythonScript —— 同 process.pipeline.build 那条 2026-07-10 坑,封装层已按执行器真实字段名组装,调用方不会踩到。print_summary``print``message—{"message":..}
body 里没有 teamName / userName 字段——不是被忽略,是这个 DTO 上根本没有这个属性。租户归属 100% 走鉴权上下文(同 process.pipeline.build),payload 传了也不会被采纳。
拓扑组装(调用方不需要关心,仅供理解结果):第 i 步(1-based)node id = "<name>_step<i>", stepSeq="<i>",画布自动横向布局(120×60,间距 220)。相邻两步之间只支持 单链路顺序执行—— step[i] 的 sStep 与 aftId 都指向 step[i+1] 的 stepSeq, nStep/ fStep 保持 "-1"。这是 fail-fast 语义:任一步骤失败即中止整个 pipeline,不会静默跳到下一步。 不支持分支 / for 循环 / if 节点 / 并行——那些场景请用 process.pipeline.build 或先用 process.create 建好线性骨架、再用 process.pipeline.update 全量替换加拓扑。
返回:与 process.pipeline.build 相同的信封 {success, data:{id,name,description,created,teamName}, message}。 name 冲突返回 HTTP 409 {success:false, code:"PROCESS_NAME_EXISTS"}; steps 缺失某个 kind 必填字段返回 HTTP 400 {success:false, code:"VALIDATION_FAILED", message:"..."}。
示例调用:
bash
scripts/lg_agent_exec.sh process.create name=fund_dividend_wrapper_test --json '{"body":{
"description":"fund dividend ingest",
"steps":[
{"kind":"run_sql","label":"create tables","sql":"CREATE TABLE IF NOT EXISTS fund_dividend (...)","dataSourceName":"pg_main"},
{"kind":"run_python","label":"fetch akshare","script":"import akshare as ak\nprint(ak.fund_fh_em())","pipRequirements":["akshare"]},
{"kind":"run_python","label":"tushare enrich","script":"import tushare as ts\n# ...","pipRequirements":["tushare"]},
{"kind":"print_summary","label":"done","message":"fund_dividend ingest complete"}
]
}}'
团队级可复用 Python 模块( team_lib)—— enabled 的模块可在 python_script 流程步骤里以 from team_lib.<name> import ... 导入。执行器每次运行前都会把所有 enabled 行重新物化成 team_lib 包(先整体删除、再重建),所以这里的改动从 下一次 运行才生效,不是立即生效。跨团队或不存在的 id 一律返回 403(不是 404,避免 id 枚举)。
skillIdmethod功能风险team.python.method.listGET列出当前团队的可复用 Python 模块。返回 id / name / code(完整源码,不截断)/ description / enabled / created_by / updated_at。🟢team.python.method.getGET按 id 获取单个模块详情(含完整 code)。🟢team.python.method.createPOST新建一个团队级可复用 Python 模块。 name 必须匹配 ^[a-z][a-z0-9_]{0,127}$;撞上保留包名( team_lib/ __init__/ __main__)或 Python 关键字/软关键字(如 class/ import/ match/ case)会 400(会成为字面上的 team_lib/<name>.py 模块文件);同团队内 name 唯一,重复返回 409 "Module name already exists"。 code 落库前会过一遍安全黑名单( os.system(、 subprocess.*(、 eval(、 exec(、 __import__(、 os.environ、 socket.socket(、 shutil.rmtree( 等),命中即 400 "Security check failed: code contains forbidden pattern [...]"——这是防误用护栏,不是沙箱边界。 enabled 默认 true;设为 false 可先保存、暂不物化进 team_lib。🟡team.python.method.updatePUT按 id 部分更新( 字段掩码语义:只有 body 里出现的字段会被改; name / team_name 通过此接口不可改)。 code 若出现须非空,且过与 create 相同的安全黑名单。🟡team.python.method.deleteDELETE永久删除 一个团队 Python 模块。 不可恢复——代码行被硬删( repo.delete,无软删除/版本历史),且从 下一次 python_script 执行开始,任何依赖 from team_lib.<name> import ... 的脚本都会抛 ImportError(物化是"每次运行先删再重建",正在跑的执行不受影响,但之后每一次都会受影响)。经两步确认握手,见 §高风险操作确认握手。🔴
skillIdmethod功能风险schedule.job.listGET列出调度作业🟢schedule.job.getGET获取调度作业详情🟢schedule.workgroup.listGET发现 当前平台注册的 workgroup / namespace(从已注册 broker 聚合),是 schedule.job.create 两个必填字段的唯一合法来源🟢schedule.scripts.getGET发现 平台配置的 jobScript 默认模板( {dp, sh, py}),给 schedule.job.create 的 jobScript 字段用🟢schedule.job.createPOST创建调度作业( POST /api/schedule/jobs)。 新建后 state="0",上线操作需通过平台 UI 执行。🟡schedule.job.updatePUT全量替换作业配置( PUT /api/schedule/job/{jobId})。 全部字段都会被覆盖——缺失字段会被写成 null,可能清空 cronExp 等关键字段。 推荐用 schedule.job.patch 做部分更新;PUT 只在需要显式清空某字段时使用。 state 字段静默丢弃。🟡schedule.job.patchPATCH部分更新作业配置( PATCH /api/schedule/job/{jobId})。 字段掩码语义:只有非 null 字段会被覆盖到已存在的行,缺失字段和显式 null 都视为"跳过"。要清空字段请用 PUT schedule.job.update。 state / teamName 同样静默丢弃(与 PUT 一致)。 此 skill 需要 schedule.job.patch scope(独立于 schedule.job.update),现有 token 须重新签发方可使用。🟢schedule.job.depends.listGET列出作业依赖(按 jobCode)。每行带 dependGroup——同组 OR、组间 AND🟢schedule.job.depends.savePOST全量替换作业依赖列表(旧的先删再写,不经确认握手——传入数组即为完整期望状态,遗漏的依赖会被静默清空)。支持按 dependGroup 分组做 OR/AND 组合🟡schedule.job.plugins.listGET列出作业绑定的插件(按 jobCode)🟢schedule.job.plugins.savePOST全量替换作业插件列表(旧的先删再写,不经确认握手——传入数组即为完整期望状态,遗漏的绑定会被静默清空)🟡schedule.instance.listGET列出作业实例(一次运行=一条 trigger 行);ops 操作所需的 jobTriggerId 都从这里拿🟢schedule.instance.status.getGET按 (jobCode, batchNo) 查单条最新状态,用于轮询🟢schedule.instance.log.getGET按 jobTriggerId 拉取执行日志🟢schedule.instance.redoPOST重跑 失败/已完成实例(保留依赖链语义)🟡schedule.instance.holdPOST暂停 运行中的实例(不杀进程,可恢复)🟡schedule.instance.resumePOST恢复之前 hold 住的实例🟡schedule.instance.reset_priorityPOST调等待队列里实例的优先级( priority 1-9,越小越先跑)🟡schedule.job.lineageGET作业的上下游依赖图( includeAssets=true 时附带每个节点的输出资产)🟢schedule.job.by_processGET用 process 名反查 jobCode(拿到后才能调 ops skill)🟢schedule.broker.listGET列当前注册的 broker(排"无人认领 workgroup"类问题时用)🟢schedule.broker.latencyGETBroker 队列长度 + 消费速率 + 推算的等待延迟(诊断"上线但跑得慢"类问题)🟢schedule.job.plugin.webhook.triggerPOST手动触发作业绑定的 webhook 插件🟡
外部 agent 在调 schedule.job.create 之前,先走一遍"发现"(这几个字段没有硬编码枚举,值取决于当前部署):
schedule.workgroup.list → 拿到 {workgroups, namespaces},从中各选一个赋给 workgroup / namespace。 传一个没人认领的 workgroup 不会报错,但没 broker 会去跑——这是最典型的"创建完成但永远不执行"陷阱。schedule.scripts.get → 拿到 {dp, sh, py},按 jobType 选对应字段赋给 jobScript( dp 作业用 dp, python 作业用 py, shell 作业用 sh;空字符串表示该类型没有在这套部署上配好)。schedule.job.list + schedule.job.get 挑一个已上线的作业 clone 一份。schedule.job.create / schedule.job.update 的 body(DataflowJob 形):
字段必填说明jobCode后端强制团队内唯一业务编码。已存在时 create 幂等返回旧 jobId。jobLabelUI 强制展示名jobTypeUI 强制枚举: dp / datastash / python / shell``workgroupUI 强制集群组名。 合法值来自 schedule.workgroup.list,不要自己编namespaceUI 强制命名空间。 合法值来自 schedule.workgroup.listjobScriptUI 强制执行命令行。 默认模板来自 schedule.scripts.get(按 jobType 取对应字段)batchTypeUI 强制枚举: monthly / daily / hourly / minutely / once / daemon``cronExp条件Quartz 6 段式(秒起头),如 0 5 15 * * ?``jobParam条件JSON 字符串 数组: "[{\"paramName\":\"-f\",\"paramVal\":\"my_proc\"}, ...]"; jobType=dp 时后端按 paramName="-f" 自动回写 procName``procName可选dp 作业通常交给后端从 jobParam 反推;其他 type 可显式传runConstraint可选"1" =顺序执行(默认), "2" =并发执行batchNo / batchOffset / batchStep可选批次计算相关jobPriority可选1–9,数字越小越高(默认 5)redoNum可选失败重试次数lastdtOffset可选最晚启动偏移(秒),0 为不宽限maxElapsed可选最长运行时间(秒)jobExtCfg可选≤1024 字符的扩展配置 JSONtag可选自由标签jobDescr可选描述state—update 时静默丢弃,上下线状态变更需通过平台 UI 操作服务端自动填充—jobId(UUID)、 state="0"、 version=1、 teamName / memberName / createUser(取自会话)
schedule.job.depends.save 的 body(JSON 数组, 全量替换):
json
[
{ "dependCode": "upstream_a", "dependType": "10", "dependGroup": "g1" },
{ "dependCode": "upstream_b", "dependType": "10", "dependGroup": "g1" },
{ "dependCode": "20260424", "dependType": "20",
"batchCalExp": "${batchNo?calDate(-1,'d','yyyyMMdd')}" }
]
上面这份表示 (upstream_a OR upstream_b) AND 时间依赖。
dependType="10" — 任务依赖, dependCode 是 另一个 jobCode(同团队内可见)dependType="20" — 时间/批次依赖, dependCode 是时间字符串, batchCalExp 是批次偏移表达式( ${batchNo?calDate(...)})dependGroup(可选)—— 分组键:同组 OR、组间 AND。同一个非空 dependGroup 的多行任一满足即算该组满足;不同组(包括每一行 dependGroup 为空/不传,各自独立成组)之间要求全部满足——即退化为原来的全 AND 语义。合法格式 ^[A-Za-z0-9_-]{1,32}$;空白会被规范化为 null。 任何一行格式不合法,整次保存都会失败( {success:false, message}),且 不会删除任何已有依赖行——可以放心重试。procName、 output、 isDefault( "1" 标默认)都可选dependId 每次保存都由服务端重新生成(UUID16),不用自己传,也不要依赖它在两次保存之间保持不变schedule.job.plugins.save 的 body(JSON 数组, 全量替换):
json
[
{
"pluginCode": "webhook",
"state": "1",
"pluginCfg": "{\"webhookDsName\":\"feishu_ds\",\"dataSourceName\":\"feishu_ds\",\"triggerStates\":[\"1\",\"-2\"]}",
"isBlock": "0",
"isDefault": "1"
}
]
pluginCode + pluginCfg(JSON 字符串)为必填state 为要监听的任务状态: "1" 成功 / "-2" 失败 / "2" 结束 / "0" 启动 / "-1" 中止( dacp_dataflow_job_trigger.state 的子集)pluginCfg 里 必须 带 webhookDsName,否则返回 {"success":false, "message":"Webhook plugin requires pluginCfg.webhookDsName"}jobPluginId 服务端生成典型的"从零到调度可跑"流程(外部 agent 视角):
schedule.workgroup.list+schedule.scripts.get→ 发现合法的workgroup/namespace/jobScriptschedule.job.create→ 拿到jobIdschedule.job.depends.save(至少一条依赖,否则上线后不会产生 instance)- (可选)
schedule.job.plugins.save→ 绑 webhook 等插件- 通过平台 UI 上线作业(state: 0 → 1)→ 让 broker 把它纳入触发域
Ops 流程几乎总是先 schedule.instance.list(或 schedule.job.by_process → schedule.instance.list)拿到目标 jobTriggerId,再按下面这张表选动作:
场景推荐 skill备注失败了想重跑一次schedule.instance.redo保留依赖链;默认 opType="3",带依赖重跑运行中但想先停住等数据就绪schedule.instance.hold不杀进程,可 schedule.instance.resume 恢复等待太久想插队schedule.instance.reset_priority只对"在队列等待"的实例有效查上下游会被哪些 job 影响schedule.job.lineage在平台 UI 操作前先看一下上下游影响已知 process 名找对应 jobCodeschedule.job.by_process常用于从 Process 页面反向调 ops排查"作业没有 instance"schedule.broker.list → 看 workgroup 有没有 broker; schedule.job.lineage → 看 depend 是否还卡着第二常见的"不跑"陷阱排查"在跑但很慢 / 积压"schedule.broker.latency看 stalled / 队列长度;若是 broker 瓶颈就不是 job 的问题想看这次跑得怎么样schedule.instance.status.get(单点)或 schedule.instance.log.get(看日志)轮询建议用 status.get,日志用 log.get
skillIdmethod功能风险datasource.listGET列出数据源。 不在默认 read-data 预设里——默认预设建的 token 调会 403,需在创建 token 时手动勾选这个 scope,或重新签发。 🟢datasource.getGET获取数据源详情。 不在默认 read-data 预设里,同上,需手动勾选或重新签发。 🟢datasource.list.activeGET列出活跃数据源。 不在默认 read-data 预设里,同上,需手动勾选或重新签发。 🟢datasource.connection.testPOST测试数据源连接。 不在默认 read-data 预设里,同上,需手动勾选或重新签发。 🟡datasource.updatePUT全量替换 数据源(含连接配置 dsConf)。 **想只改描述或显示标签?用 datasource.patch,避免 dsConf/dsAuth 被覆盖。**🟡datasource.patchPATCH部分更新数据源元数据( PATCH /api/datasources/{dsId})。 字段掩码语义:仅 dsDescr(描述)/ dsLabel(显示标签)两个安全字段可被更新;缺失字段、显式 null、 以及空字符串 "" 都视为"跳过"( 不 清空)。 严格拒绝: dsConf(更改连接目标会破坏运行中的 ETL)/ dsType(标识符)/ dsAuth / dsId / dsName / teamName 出现在 body 即返回 HTTP 200 success:false, code:"FIELD_NOT_PATCHABLE"。 此 skill 需要 datasource.patch scope(独立于 datasource.update),现有 token 须重新签发方可使用。🟢dataasset.listGET列出数据资产🟢dataasset.getGET获取资产详情🟢dataasset.schema.getGET获取资产 schema(列名/类型);支持 ?refresh=true 绕过缓存实时重新采集🟢dataasset.metadata.getGET获取资产富元数据:20 个字段,包含 lastUpdated(最近刷新时间)、 expectedUpdateCadence(调度批次类型,来自关联 Job)、 cronExpression(cron 表达式)、 sourceDescription(数据源描述)。Bug #50 后续,用于程序化判断资产新鲜度。 2026-09 起已在默认 read-data 预设里,用默认预设建的 token 直接调即可。🟢dataasset.data.getGET查询资产数据(盈亏、行情等,全量历史);支持 filter_op 过滤运算符(见下方详情)。MC 数据源资产速度较慢(2-5s)但包含完整历史🟢dataasset.data.getRealtimeGET查询资产的 实时镜像数据源(PG 热窗口,最近 365 天);低延迟,适合仪表盘 / 实时 P&L。资产未配置 realtimeDataSource 时返回 {success:false, message:"This asset has no realtime mirror"},不抛异常。路径: GET /api/data-assets/{id}/data-realtime🟢dataasset.history.field-as-ofGET点状时间(PIT)股票状态字段查询: is_st(ST/暂停挂牌)、 market_board(板块)、 delisted(退市)、 industry(申万行业分类)。返回指定股票在某日期上的历史状态快照。依赖 PR-B ETL 完成首次回填后方可返回实际数据;回填未完成前返回 data:null。 这个 scope 不在默认 read-data 预设里——用默认预设建的 token 调会 403,需要在创建 token 时手动多勾这一项,或重新签发。详见下方「dataasset.history.field-as-of」节。 🟢dataasset.updatePUT全量替换 数据资产元数据( PUT /api/data-assets/{id})。 **想只改 description/tags/allowSubscription?用 dataasset.patch PATCH,避免误改 sensitivityLevel 等不可改字段。**🟢dataasset.patchPATCH部分更新数据资产元数据( PATCH /api/data-assets/{id})。 字段掩码语义:仅 description / tags / allowSubscription 三个安全字段可被更新;缺失字段、显式 null、 以及空字符串 "" 都视为"跳过"。 严格拒绝: sensitivityLevel(含别名 sensitivity_level / securityLevel / level)— 敏感级是 MONOTONIC,调整必须走专用 sensitivity-change 路径; assetName / assetType / tableName / teamName 等标识符也拒绝。 allowSubscription=true 在 INTERNAL 资产上要求 tags 包含 permission_field:<col>,否则返回 code:"PERMISSION_FIELD_REQUIRED"(INLINE 复刻 PUT 路径的 publish-validation guard)。 **此 skill 需要 dataasset.patch scope(独立于 dataasset.update),现有 token 须重新签发方可使用。**🟢
dataasset.schema.get — ?refresh 参数路径: GET /api/data-assets/{id}/schema
参数必填默认说明refresh否false``false:返回注册时缓存的 schemaInfo JSON(快速,无 I/O)。 true:绕过缓存,对底层表做实时列采集,结果持久化回 schemaInfo 并写 last_schema_sync_at。
refresh=true 时响应的 meta 字段会包含 lastSchemaSyncAt(ISO-8601 UTC 时间戳),标示本次同步完成时间。
示例( refresh=true):
json
{
"success": true,
"data": {
"columns": [
{"name": "stock_num", "type": "varchar"},
{"name": "day_id", "type": "int4"},
{"name": "close_price","type": "numeric"},
"..."
]
},
"message": "success",
"meta": {
"lastSchemaSyncAt": "2026-06-17T03:12:45Z"
}
}
业务错误(底层数据源不可达 / 序列化失败 / 保存失败):返回 HTTP 200, {"success": false, "message": "Schema refresh failed: <原因>"} —— 符合平台 {success, data, message, meta?} 合约,不抛 HTTP 500。
dataasset.metadata.get — 资产富元数据路径: GET /api/data-assets/{id}/metadata
参数必填说明id是数据资产 PK(Long)
无分页,无请求体。返回 data 对象包含 20 个字段:
字段类型来源备注assetIdLong直接资产 PKassetNameString直接资产名称assetTypeString直接如 "table"``assetCategoryString直接如 "market-data"``descriptionString直接可为 nulltagsString直接CSV blob,调用方自行拆分sensitivityLevelString直接PUBLIC / INTERNAL / CONFIDENTIAL / RESTRICTED``allowSubscriptionBoolean直接是否可订阅teamNameString直接所属团队createdDateLocalDateTime直接创建时间createdByString直接创建人recordCountLong直接行数(去规范化,可能滞后)sizeBytesLong直接字节数(去规范化)lastUpdatedLocalDateTime直接最近数据刷新时间(与 dataasset.list 命名一致)lastSchemaSyncAtOffsetDateTime直接最近 schema 同步时间(UTC)jobCodeString直接关联 Job 代码(标量,可为 null)expectedUpdateCadenceString派生来自 DataflowJob.batchType;多 Job 时取第一个有 cronExp 的 Job;无匹配 → nullcronExpressionString派生来自 DataflowJob.cronExp;同上选取逻辑dataSourceString直接数据源名称(文本)sourceDescriptionString派生来自 DataSource.dsDescr;team-scoped join,订阅方通常为 null
响应示例(成功):
json
{
"success": true,
"data": {
"assetId": 46,
"assetName": "stock_day",
"assetType": "table",
"assetCategory": "market-data",
"description": "A-share daily OHLCV bars",
"tags": "PUBLIC,equity,daily",
"sensitivityLevel": "PUBLIC",
"allowSubscription": true,
"teamName": "tenant_lg_data",
"createdDate": "2025-12-01T08:00:00",
"createdBy": "admin",
"recordCount": 5234567,
"sizeBytes": 8923847234,
"lastUpdated": "2026-06-17T15:42:18",
"lastSchemaSyncAt": "2026-06-16T03:00:00Z",
"jobCode": "stock_day_sync",
"expectedUpdateCadence": "daily",
"cronExpression": "0 0 18 * * ?",
"dataSource": "lg_data_pro_pg",
"sourceDescription": "Primary LG Postgres lakehouse (read-only mirror)"
},
"message": "OK"
}
业务错误(资产不存在 / 跨团队访问):返回 HTTP 200, {"success": false, "code": "ASSET_NOT_FOUND", "message": "<原因>"} —— 符合平台 {success, data, message} 合约,不抛 HTTP 4xx。
dataasset.data.get — filter_op 支持的运算符集合路径: GET /api/data-assets/{id}/data
snake_case 别名是 agent-skill gateway 的属性,不是这个 HTTP 端点本身的属性。 通过本技能( dataasset.data.get / lg.get_asset_data)调用时,gateway 会把 snake_case 参数名( filter_column / filter_value / filter_op / filter_column_2 / filter_value_2 / filter_op_2 / order_by / order_direction)翻译成 camelCase 再转发给后端( lib/skill-catalog.js 的 paramAliases,经 lib/param-aliases.js 施加,仅在 skill-dispatch 路径生效)。 直接对底层 HTTP 端点发起裸请求(不经过 gateway)必须使用 camelCase——snake_case 参数名会被 Spring 静默忽略(未声明的 @RequestParam 不报错也不生效),导致过滤器悄悄失效而不是报错。2026-07-30 起,四个资产数据 GET 端点(本端点、 dataasset.data.getRealtime、以及两个 /api/internal/** 内部端点)新增了未识别 query 参数的 fail-loud 校验——裸 HTTP 调用如果误用 snake_case 名,会收到 {"success":false,"message":"Validation Failed: Unrecognized query parameter(s): ..."}(HTTP 200),而不是从前那样被静默丢弃、无任何信号。
省略 filter_op 时的默认值:只传 filter_column/ filter_value、不传 filter_op( filterOp)时,默认运算符是 contains( LIKE '%value%'), 不是精确匹配—— filter_column=fund_code filter_value=510300(没有 filter_op)会匹配任何 包含 510300 子串的代码,不止 510300 本身。想要精确匹配请显式传 filter_op=eq。
filter_op / filterOp 完整合法值:
运算符SQL 语义示例eq``= 'val'``filterOp=eq&filterValue=600519``ne``<> 'val'``filterOp=ne&filterValue=600519``gt``> 'val'``filterOp=gt&filterValue=10.5``gte``>= 'val'``filterOp=gte&filterValue=10.5``lt``< 'val'``filterOp=lt&filterValue=10.5``lte``<= 'val'``filterOp=lte&filterValue=10.5``like``LIKE '%val%'(平台自动加两侧 %,与 contains 等价)filterOp=like&filterValue=浦发``contains``LIKE '%val%'(平台自动加两侧 %,与 like 等价)filterOp=contains&filterValue=浦发``in``IN ('a','b','c')``filterOp=in&filterValue=a,b,c``not_in``NOT IN ('a','b','c')``filterOp=not_in&filterValue=a,b,c``between ✨ 2026-07-28BETWEEN 'start' AND 'end'``filterOp=between&filterValue=20140101,20141231
多值运算符( in / not_in): filter_value 用英文逗号分隔,如 filter_value=600519,000001,601398 → 生成 IN ('600519','000001','601398')。
like 和 contains 行为相同:平台都生成 LIKE '%val%' 子串匹配。如需自定义 LIKE 模式(前缀 / 后缀 / 通配符位置),目前需在调用方 SQL 端处理或走 POST /api/data-assets/{id}/query。
between(issue #68,2026-07-28): filter_value 必须是 恰好 2 个 用英文逗号分隔的值( start,end),生成 field BETWEEN 'start' AND 'end'。 Fail-loud,无静默降级——以下任一情况直接返回 HTTP 200 {"success": false, "message": "Validation Failed: ..."},绝不会静默退化成单值过滤或悄悄改用最新分区:
start > end(范围反了)。未知运算符拒绝(不静默降为 eq):传入不在上表中的值(如 filterOp=regex)→ HTTP 200 {"success": false, "message": "Validation Failed: Unsupported filter operator: regex"}。
order_direction(snake_case;camelCase 写法为 orderDirection)接受 asc / desc(大小写不敏感),默认 asc。
分页上限与截断信号(feedback #69,2026-07-29,v2 修正见下): dataasset.data.get 对 MaxCompute-backed 资产( fund_day、 stock_day_v2 等历史全量资产)的 每页行数硬上限是 1000 行。PG-backed 资产( dataasset.data.getRealtime 的 365 天热窗口镜像)没有这个上限。
size 超过 1000 时必须同时传 order_by,否则直接报错(不看 page):MaxCompute 的分布式扫描在没有 order_by 时不保证行序一致——不仅是"翻页时前后两页可能对不上",更关键的是 同一个 page=1,size=10000 请求重复调用两次,拿到的可能是完全不同的一批行(这正是 2026-07 一起线上事故的根因:某每日选股流程按 size=10000 无排序键拉全市场数据,30 天内每天实际拿到的股票代码集合都不一样,导致移动均线永远算不出来,且流程本身"成功退出"、日志正常,是一个完全静默的失败——事故的行为特征更像是"同一个请求形状每次返回任意子集",而不仅仅是"第二页跟第一页对不上")。因此判定条件是 size > 1000 且未传 order_by—— 第一次调用( page=1)就会报错,不会等到翻页才发现: {"success": false, "message": "Validation Failed: ... requires an 'orderBy' column ..."}(HTTP 200)。
size <= 1000 永远不受此限制,无论 page 多大、总行数多少——marketplace 匿名预览固定 page=1&size=25, 25 <= 1000 恒成立,天然、永久豁免,不需要任何特殊处理;普通小分页翻页(如 size=25&page=2)也不受影响,行为与本次修复前一致(这不是本次修复的范围)。
当 size 超过 1000 且 提供了 order_by(因此调用成功)时,响应里会带两个额外信号字段: truncated: true 和 effectivePageSize: 1000(实际生效的每页行数)。不超限时这两个 key 不存在(不是 false/ null,而是整个 key 缺失——沿用 totalCountApproximate/ totalCountTruncated 的既有 additive-flag 惯例)。 pageSize / totalPages 字段始终反映实际生效值,不会再出现"回显了 size=10000 但只拿到 1000 行"的自相矛盾。 换句话说, truncated:true 只会出现在你自己提供了排序键的成功响应里——没有排序键的超限请求根本走不到返回结果那一步,直接报错。
scripts/lg_agent_exec.sh dataasset.data.get id=76 filter_column=fund_code filter_value=510300 filter_op=eq size=10000 order_by=day_id。fund_day.adj_nav 缺失信号(feedback #64 / #73,2026-08-18,LA REVISE #2 订正措辞): fund_day 有 unit_nav(单位净值)和 adj_nav(复权净值)两列。 unit_nav 会被基金份额拆分 / 分红再投污染——一旦发生拆分, unit_nav 会在拆分当天出现一个和真实收益完全无关的价格台阶(实例:513100 于 20220113 发生 5:1 拆分, unit_nav 从 5.189 跌到 1.009,凭空产生 -80.56% 的"暴跌"; adj_nav 同期从 5.189 平滑过渡到 5.045)。 adj_nav 才是唯一能正确反映基金全收益(含拆分/分红调整)的字段,长期回测 / 算收益率 / 算相关性一律应该用 adj_nav,不能用 unit_nav。
adj_nav 缺失是两条独立通道、两种不同性质的缺口,覆盖比例悬殊——不要把其中一条通道的情况当成全貌:
dataasset.data.get / lg.get_asset_data(MC 主数据源):缺口是 极少数(2026-08-18 审计:最新一天 12,482 行里仅 5 行 adj_nav IS NULL AND unit_nav IS NOT NULL,且全部来自 source='tushare_fund_nav')。dataasset.data.getRealtime / lg.get_asset_data_realtime(PG 实时镜像):缺口是 主体(同一天 24,606 行里 12,138 行满足同一条件,除 5 行外全部来自 source='akshare_em')。根因是 proc 3401( fund_day_backfill,写 MC)的依赖图里没有 proc 3540( fund_day_backup_akshare,写 PG)——MC 分区在 akshare 数据写完 PG 之前就被 tushare 数据覆盖,且从不回访旧分区,所以 akshare 来源的行 永久不会进入 MC。这是 覆盖缺口(行根本不存在),不是"存在但为 NULL"的缺口——对该类基金, dataasset.data.get 要么直接返回 0 行,要么返回一段完全没有触发下方 NULL 检查机会的历史。fund_day 是全平台仅有的两个配置了 realtimeDataSource 的资产之一,dashboard widget 带 __dashboardId 时 无条件 走 PG 镜像——这不是边缘路径,是这类资产的主要读路径之一。adjNavIncomplete 信号目前只到达 直接调用 REST / get_asset_data* 的调用方(agent 脚本、 curl 等);dashboard 大盘的 table 类 widget 渲染管道( DashboardDataService.fetchWidgetData)在把结果交给前端之前会丢弃除 data 行以外的所有字段, 尚未把此信号透出到浏览器 UI —— 这是一个更早、更大的既有缺口,不在本次改动范围内,已登记为独立 follow-up(issue #864)。因此 两条通道的响应体都会带同一个额外的 additive-only 信号字段 adjNavIncomplete:当返回的某一页里出现"某行 adj_nav 为 NULL 但 unit_nav 非 NULL"(即确认是真实交易日,只是复权净值缺失,不是非交易日占位行)时,该通道的响应体里会出现 adjNavIncomplete: true;否则该 key 不存在(同 truncated/ effectivePageSize 的 additive-flag 惯例)。 但要记住上面的覆盖比例差异:MC 通道触发这个信号的概率极低,PG 实时通道才是这个信号真正生效的地方。
lg_utils.backtest_examples.fund_day.run_fund_day_backtest(见下方 Python 工具库表格)内置的 fail-loud 检查只保护 MC 通道(它调用 get_asset_data,从不调用 get_asset_data_realtime)——对 MC 里那极少数的 5 行是真保护,但对 PG 通道的 12,138 行 没有任何保护,因为直接调用 dataasset.data.getRealtime / lg.get_asset_data_realtime 的代码根本不经过这个回测封装。 不要在看到 adj_nav 为 NULL 时静默退回 unit_nav 使用——反馈#64 记录过真实事故:某次静默回退把 YTD 算成 -47.06%(真实约 +7~10%),与基准相关性 0.2956(真实应 >0.85);直接读 dataasset.data.getRealtime 的调用方必须自己检查 adjNavIncomplete,不能假设有回测封装帮你挡住。
这个设计是 故意的 fail-loud,不同于 stock_day_v2.adj_factor 的 optional pass-through(PR #678 独立裁定过)——两者面对的是不同的数据缺口性质,选择不同的处理方式是设计决策,不是标准漂移。本次改动 不做任何推导/回填(不会用 unit_nav 的日收益率累乘出一条近似 adj_nav),这类链式推导方案已被 LA 明确驳回(先例: stock_day 的 adj_factor 拒绝"算个替代值",只接受权威真值 + provenance 标注)。
dataasset.data.get — 复合过滤(业务列 + 分区列范围,issue #68,2026-07-28)背景: dataasset.data.get 底层对 MC(MaxCompute)分区表有一个守卫——只要请求里没有显式出现分区列(如 fund_day 的 day_id),后端会强行拼一个"最新分区"等值过滤,避免 ODPS 全表扫描报错。这个守卫本身不在本次改动范围内;本次新增的是 第二个过滤槽位,让你能在一次调用里同时表达"业务列过滤 + 分区列范围",从根源避开这个守卫(守卫只看过滤条件里有没有出现分区列名,不关心运算符——你自己带了范围过滤,守卫就不会再覆盖它)。
第二过滤槽位参数: filter_column_2 / filter_value_2 / filter_op_2(camelCase: filterColumn2 / filterValue2 / filterOp2),与主槽位 filter_column / filter_value / filter_op 参数形态完全一致、独立解析、用 AND 拼接。 不传时行为与之前完全一样(不影响任何现有调用)。
示例 —— 取基金 510300 在 2014 全年的历史净值序列(原始 bug #68 场景):
bash
scripts/lg_agent_exec.sh dataasset.data.get id=76 \
filter_column=fund_code filter_op=eq filter_value=510300 \
filter_column_2=day_id filter_op_2=between filter_value_2=20140101,20141231 \
size=500
不需要业务列过滤、只想拉某个分区范围内全部记录时, between 也可以只用主槽位、不需要第二槽位:
bash
scripts/lg_agent_exec.sh dataasset.data.get id=76 \
filter_column=day_id filter_op=between filter_value=20140101,20141231 size=500
回测按年分块的使用指引(issue #68):对 MC 数据源支撑的分区资产(如 fund_day,assetId 76)做多年历史回测时, 不要 按天分块调用——十年历史按天拉需要约 41 小时(3650+ 次调用 × ~15-20s/次)。改为 按自然年分块,用 filter_op_2=between 一年一次调用: day_id BETWEEN 20140101 AND 20141231,然后 20150101 AND 20151231,……。十年历史大约 10-12 次调用,每次 ~15-20s,总耗时约 3-4 分钟——比按天拉快两个数量级。客户端把每年返回的分页结果拼接起来即可得到完整序列。
dataasset.data.getRealtime 支持完全相同的第二过滤槽位 + between 运算符(走 PG 热窗口镜像,仅覆盖最近 365 天——超出该窗口的历史仍需用 dataasset.data.get)。
dataasset.data.get vs dataasset.data.getRealtime — 路由选择指南用哪个?
dataasset.data.get/lg.get_asset_data— 分析型 / 历史全量 / 完整数据。底层存储是资产的主数据源(Step 4 切换后为 MC;速度较慢 ~2-5s,但包含完整历史和全列)。dataasset.data.getRealtime/lg.get_asset_data_realtime— 实时型 / 低延迟 / 热窗口(最近 365 天)。底层存储是资产的realtimeDataSource(PG 镜像)。对于未配置realtimeDataSource的资产返回{success:false, message:"This asset has no realtime mirror"}— 这是正常业务返回,不是异常,调用方应处理而非视为错误。
两个函数接受完全相同的参数( asset_identifier, page, size, order_by, filter_column, filter_value, filter_operator),返回 shape 相同,差异仅在于:
/data vs /data-realtime)。get_asset_data_realtime 永不抛出异常;所有错误(无镜像、类型不符、连接失败、认证失败)均以 {success:false, message:...} 返回,调用方 必须 检查 result["success"]。Prerequisite:
.getRealtimerequires the asset to haverealtimeDataSourcepopulated (viaPUT /api/data-assets/{id}, an admin API). If not configured,.getRealtimereturns{success:false, message:"This asset has no realtime mirror"}. Currentlystock_dayandfund_dayare the only assets with realtime mirrors configured; other assets will return the "no mirror" response until an admin populates theirrealtimeDataSource.
Note for existing bearer token holders: if you receive
403 Forbidden: missing required scope 'dataasset.data.getRealtime'when calling this skill, your token was minted before this skill was added. Go to lg-data.cc → Token Management, edit your token, and add thedataasset.data.getRealtimescope. New tokens minted after 2026-07-02 automatically include all currently-registered scopes.
fund_quote_day + fund_kline两张表都是 PG 原生表( 不是 MaxCompute/ODPS),走 dataasset.data.get 的常规 PG 读路径——不受本节上方 MC-only 的「最新分区自动注入」与「 size>1000 必须带 order_by」两条守卫约束(两条守卫的判定条件都显式检查 connectionInfo.databaseType == odps)。 当前 allowSubscription=false——本节只描述数据契约,不代表其它团队今天就能订阅这两张表。
fund_quote_day(场内基金日线,PG 单表不分区):
market 列取值 {ETF, LOF}—— 与 fund_day.market / fund_codes.market 的 {E,O} 是不同词表,跨表 join 一律用 fund_code,不要用 market 做 join key。turnover 是 成交额(元),不是换手率。source 区分两种写入来源: akshare_hist_em(官方历史收盘价, is_final=true / calibration_status='confirmed')与 fund_realtime_t0(当日 T+0 推导值, is_final=false / calibration_status='pending',最终会被官方值校准或在 5 次校准失败后标记 abandoned)。回测/长期分析优先用 is_final=true 的行。(fund_code, day_id)。fund_kline(场内基金分钟 K 线,PG 原生 RANGE (day_id) 分区表):
interval_type ∈ {1m, 5m, 15m, 30m, 60m}—— 没有 1d,日线请用 fund_quote_day。
bar_time 是 VARCHAR(19)(形如 '2026-08-18 09:30:00'), 不是 timestamp 类型——不要对它调用 SQL 的 to_char/日期函数,会报类型错误;需要日期运算时先在客户端解析。
设计保留期为 3 天(PG RANGE (day_id) 分区按日创建,由 proc 3389 Step 5d 按日龄丢弃超期分区, FUND_KLINE_CUTOFF_DAYS=3)。 该清理尚未真正执行过一次——表 2026-08-18 才建立第一个分区,08-17 那次运行的 Step 5d 摘要是 Age-eligible candidates: 0 / Dropped: 0;且清理门控要求 MC 侧 privora.fund_kline 镜像存在才会真删(Phase 6 offload 未做,proc 3859 线上仅三步无 offload),门是 fail-safe 的,因此当前会一直 SKIP。 契约按 3 天规划,不要假设能稳定拉到更早的历史;但也不要假设旧分区此刻真的已被清理。
每日典型 bar 数(ETF 众数,实测):
interval_type每日典型 bar 数备注1m237(分布 232–239)不是 240——源表并非每分钟都有 tick,缺口是正常现象不是缺陷5m48—15m16—30m8—60m5(hour-floor:09:00/10:00/11:00/13:00/14:00)与 stock_kline 一致, 不是 券商常见的 4 根 session-aligned 聚合
收盘打点已钳制:不存在 bar_time 落在 09:25–09:29 / 11:30 / 15:00 的桶。
tick_count 列量化该分钟内的真实采样笔数,用于判断稀疏度: LOF 采样普遍稀疏(覆盖率约 40%), tick_count=1 的 bar 必然 open=high=low=close——这代表"该分钟只采到一笔样本", 不代表"该分钟无波动",不要把它当异常值剔除。
is_final=false 是盘中快照(当日可被覆盖), is_final=true 是 T+1 fund_kline_daily_sync 定稿后的 bar(该 proc 会用 fund_quote_day 的收盘价回校当日最后一根 bar 的 close/high/low/volume/turnover)。 注意 is_final=true 目前只保证"来自已收盘并做过全天重算的源快照",不保证"已与官方日线对账"——对账要求 join 的 fund_quote_day.is_final=true(即官方值已校准),而截至目前该双源对账周期从未完整走通过(例如 2026-08-18 的全天重建摘要 unreconciled_fund_codes: 1947,覆盖当天全部场内基金,因上游校准源不可用导致 fund_quote_day 一直停在 calibration_status='pending')。
取数标准配方: dataasset.data.get 只有两个过滤槽位(主槽位 + filter_column_2/ filter_value_2/ filter_op_2 第二槽位),无法同时对 fund_code + interval_type + day_id 三个谓词都做等值过滤。按需要哪两个维度精确定位来分配槽位:
某只基金某个周期的近几天全部 K 线(不过滤 day_id——3 天保留期本身就是天然小结果集, 1m 最多 ~3×237≈711 行,不会碰到 1000 行分页上限):
bash
scripts/lg_agent_exec.sh dataasset.data.get id=<fund_kline的assetId> \
filter_column=fund_code filter_op=eq filter_value=510300 \
filter_column_2=interval_type filter_op_2=eq filter_value_2=1m \
order_by=bar_time order_direction=desc size=800
上例不带 day_id/ is_final 过滤,会把当天尚未定稿的盘中数据与此前的历史 bar 一并按时间倒序捞出——需要严格区分「已定稿历史」与「当日快照」时,请在结果里按 is_final 或 day_id 分组,不要假设越靠前的行就是最终值。
某一天某个周期的全市场快照(不过滤 fund_code——覆盖全市场 ~1950 只场内基金, 1m 单日全量约 1950×237≈462,000 行, 必须 显式传 order_by 并分页拉取,一次 size 请求不到全量):
bash
scripts/lg_agent_exec.sh dataasset.data.get id=<fund_kline的assetId> \
filter_column=day_id filter_op=eq filter_value=20260818 \
filter_column_2=interval_type filter_op_2=eq filter_value_2=1m \
order_by=fund_code size=1000 page=1
两张表尚未固定公开 assetId(当前 allowSubscription=false,尚未走公开发布流程)——与「快速接入」一节 Step 1 的惯例一致: 先 dataasset.list 按 assetName=fund_kline / fund_quote_day 查数字 id,不要硬编码猜测。
dataasset.history.field-as-of — 股票 PIT 状态字段查询 (#13 PR-A/C)路径: GET /api/data-assets/history/field-as-of
Auth: 标准 Spring Security session 或 Bearer token( lgatk_…)。GET 请求无需 CSRF token。Bearer token 需要 scope dataasset.history.field-as-of(PR-A 新增;现有 token 须重新签发方可使用)。
依赖: 底层表( stock_st_history / stock_board_history / stock_delist_history / stock_industry_history)由 PR-B ETL 进程回填。ETL 未运行前所有查询均返回 data:null,不会报错。
参数类型必填说明fieldString是字段名,固定四选一: is_st / market_board / delisted / industry``stockNumString是股票代码,如 600519.SH / 00700.HK``asOfDateString是查询日期,ISO-8601 格式 YYYY-MM-DD``swVintageString否仅 field=industry 时有效: SW2014 或 SW2021。缺省时自动按 asOfDate 选择( asOfDate < 2021-10-01 → SW2014;否则 SW2021)
field 语义说明:
field返回类型说明is_stBooleantrue = ST/特别处理/暂停挂牌; false = 正常; null = 无历史记录(ETL 未回填或日期早于数据)market_boardString主板 / 创业板 / 科创板 / 北交所 / 中小板(v1 每日刷新,非事件驱动,精度至天)delistedBooleantrue = delist_date <= asOfDate; false = 未退市或无记录(默认 False,不返回 null)industryObject申万行业分类对象(见下方结构), null = 无记录
industry 值对象结构:
json
{
"l1_code": "110000",
"l1_name": "食品饮料",
"l2_code": "110201",
"l2_name": "白酒",
"sw_vintage": "SW2014"
}
响应示例(field=is_st,股票在 ST 状态):
json
{
"success": true,
"data": {
"field": "is_st",
"stockNum": "000666.SZ",
"asOfDate": "2022-06-01",
"value": true,
"validFrom": "2021-03-15",
"validTo": null,
"source": "namechange",
"changeReason": "特别处理"
},
"message": "OK"
}
响应示例(field=industry,自动选择 SW2014):
json
{
"success": true,
"data": {
"field": "industry",
"stockNum": "600519.SH",
"asOfDate": "2020-06-15",
"value": {
"l1_code": "110000",
"l1_name": "食品饮料",
"l2_code": "110201",
"l2_name": "白酒",
"sw_vintage": "SW2014"
},
"validFrom": "2014-01-01",
"validTo": null,
"source": "index-member"
},
"message": "OK"
}
无历史记录(ETL 未回填 / 日期早于数据):
json
{
"success": true,
"data": null,
"message": "No history record found for stockNum=600519.SH, field=is_st, asOfDate=2020-06-15. ETL may not yet have populated this field."
}
业务错误(非法 field / 非法日期格式):
json
{
"success": false,
"message": "Unsupported history field: 'foo'. Supported: [is_st, market_board, delisted, industry]"
}
Python 快捷用法(via lg_utils.history):
python
from datetime import date
from lg_utils.history import is_st_at, market_board_at, is_delisted_at, industry_at, load_st_history
# 单日点查
st = is_st_at("000666.SZ", date(2022, 6, 1)) # True / False / None
board = market_board_at("688001.SH", date(2022, 6, 1)) # "科创板" / None
delist = is_delisted_at("000001.SZ", date(2023, 1, 1)) # True / False (never None)
ind = industry_at("600519.SH", date(2020, 6, 15)) # dict with 5 keys / None
# 获取某股票全部 ST 历史事件列表(按 valid_from 升序)
events = load_st_history("000666.SZ")
# [{'valid_from': '2001-06-04', 'valid_to': '2001-09-18', 'is_st_value': True, ...}, ...]
lg_utils.history 模块通过 executor 内部 HTTP 调用同一个 REST endpoint,fail-soft 语义与 REST API 一致(无记录返回 None/ [],不抛异常)。
skillIdmethod功能风险dashboard.listGET列出看板🟢dashboard.getGET获取看板详情🟢dashboard.data.getGET一次拿看板所有组件的数据(支持 maxRows,默认 100,上限 500)🟢dashboard.updatePUT全量替换 看板(含 widgets/variables)。 **想只改名称或描述?用 dashboard.patch,避免 widgetsJson 被覆盖。**🟡dashboard.patchPATCH部分更新看板元数据( PATCH /api/dashboards/{id})。 字段掩码语义:仅 name / description 两个安全字段可被更新;缺失字段、显式 null、 以及空字符串 "" 都视为"跳过"( 不 清空)。 严格拒绝: widgetsJson / variablesJson / widgets / teamName / dashboardId / allowSubscription 出现在 body 即返回 HTTP 200 success:false, code:"FIELD_NOT_PATCHABLE"。 **此 skill 需要 dashboard.patch scope(独立于 dashboard.update),现有 token 须重新签发方可使用。**🟢
skillIdmethod功能风险subscription.token.listGET列出订阅 token。 不在默认 read-data 预设里,预设场景按钮里也都没有它——创建 token 时必须手动勾选这个 scope,或重新签发。 🟢marketplace.item.listGET列出可订阅的看板/资产🟢marketplace.item.subscribePOST订阅市场条目—— 幂等( ON CONFLICT(item_code, team_name) DO UPDATE,已订阅过再调一次同样成功)。响应体按 itemId 前缀带回订阅方 自己团队 的克隆 id: asset-* → clonedAssetId, process-* → clonedProcessId, dashboard-* → clonedDashboardId(见下方 场景 5)。这是拿"我自己团队里这份资产的数字 id 是多少"的最短路径,比 dataasset.list 扫 tags 少一次往返,见 Quick Start §0。 不在默认 read-data 预设里——预设场景按钮里只有 subscribe-and-read 带它,创建 token 时请选这个预设(或手动勾选这个 scope)。 🟡marketplace.item.unsubscribePOST取消订阅。 不在默认 read-data 预设里——预设场景按钮里只有 subscribe-and-read 带它,创建 token 时请选这个预设(或手动勾选这个 scope)。 🟡
skillIdmethod功能风险metric.alert.listGET按 dashboardId 列出告警规则🟢metric.alert.getGET按 ruleCode 获取规则🟢metric.alert.createPOST创建告警规则🟡metric.alert.updatePUT全量替换 告警规则。 **想只改阈值/webhook/消息模板?用 metric.alert.patch。**🟡metric.alert.togglePUT启用/停用规则🟡metric.alert.testPOST仅测试(无副作用)🟡metric.alert.evaluatePOST执行评估并按规则触发 webhook🟡metric.alert.patchPATCH部分更新告警规则元数据( PATCH /api/metric-alerts/{ruleCode})。 字段掩码语义: threshold(阈值字符串)/ webhookDsName(webhook 数据源名)/ messageTemplate(消息模板)/ templateEngine(模板引擎, 'legacy' 或 'freemarker')四个安全字段可被更新;缺失字段、显式 null、 以及空字符串 "" 都视为"跳过"( 不 清空)。 严格拒绝: enabled(用 metric.alert.toggle)/ ruleCode(标识符)/ metricKey(更改后测量对象变化 — 破坏性)/ teamName 出现在 body 即返回 HTTP 200 success:false, code:"FIELD_NOT_PATCHABLE"。 templateEngine 传入非 legacy/ freemarker 值返回 HTTP 200 success:false, code:"INVALID_TEMPLATE_ENGINE"。 **此 skill 需要 metric.alert.patch scope(独立于 metric.alert.update),现有 token 须重新签发方可使用。**🟢metric.alert.snoozePUT暂停指定 alert rule 直到 until (ISO 8601 本地时间戳);body: {"until": "2026-06-22T18:00:00"}. Snooze 优先级高于 silence + rate-limit;snooze 到期后自动恢复正常 evaluate()。 until 缺失或非未来时间 → HTTP 200 success:false code:INVALID_UNTIL。🟢metric.alert.unsnoozePUT取消活跃的 snooze ( snoozed_until → null);与 snooze 是 独立 scope — 设置 snooze 的 agent 不会自动获得清除权限。幂等,已无 snooze 时也返回 success。🟢metric.alert.acknowledgePUT标记 alert 已知 ( acknowledged_at = now());v1 仅 info-only — 不影响下次 evaluate()(UI 显示"已知"状态);v2 可能扩展为"clears next trigger window"。🟢
metric.alert.evaluate — 数据新鲜度门控(freshness gate,2026-06-22 重写)freshnessConfig.sourceType = 'asset' 模式下,freshness gate 不再执行 SELECT MAX(field) 实时查询,而是直接读取 data_asset.last_data_refresh_at 列——该列由 registerAsset executor step 在每次 ETL 成功执行后自动写入。行为变化:
field 字段已废弃但向下兼容:现有配置中的 field 会被忽略(不校验、不查询),无需修改已有 freshnessConfig。publisherTeam != null):subscriber 侧直接通过 getAssetByIdInTeam 读取 publisher 资产的 last_data_refresh_at,无需 publisher 数据源凭证;跨租户 freshness gate 现已生效(此前 custom-sql 跨租户路径仍受 MVP 限制,保持 fail-open)。registerAsset step), last_data_refresh_at 为 NULL → probe 返回 Optional.empty() → 评估照常进行,不触发 gate。sourceType = 'custom-sql':行为不变(own-team 执行自定义 SQL;跨租户仍为 MVP 限制)。已知限制:ETL job 执行成功但写入 0 行时, RegisterAssetStep 仍然会 bump last_data_refresh_at,导致 gate 不会跳过该次评估。若 webhook 频繁误触发,建议使用 metric.alert.snooze 或调高阈值;v2 会在 rowsWritten > 0 条件下才 bump(目前为规划中的 follow-up)。
metric.alert.snooze — evaluate() 跳过优先级(Triage #11)evaluate() 内部跳过检查的优先级( 先匹配先返回):
snoozed_until > now() → SKIPPED "snoozed until "lastTriggeredAt + silenceMinutes > now() → SKIPPED "in silence period"firesToday >= maxFiresPerDay → SKIPPED "daily limit reached"Snooze 到期后( snoozed_until < now())自动恢复,无需再调 unsnooze。
metric.alert.patch — templateEngine 字段(Triage #10)templateEngine 控制 messageTemplate 的渲染方式:
值行为适用场景"legacy"(默认)${varName} 正则替换,与历史版本完全兼容现有规则无需改动"freemarker"FreeMarker 模板引擎:支持条件指令( <#if var??>...</#if>)、 <#list> 迭代、null 安全访问( ${var!})、rowFields 嵌套访问( ${rowFields.stock_num!})需要条件内容或丰富格式的飞书/微信消息
freemarker 模式下可用变量:
变量类型说明${ruleName}String规则名称${value}String当前触发值${threshold}String阈值${fieldName}String监控字段名${assetName}String资产名称${operator}String运算符(GT / LT / EQ 等)${tenantName}String告警规则所属团队${publisherTeam}String订阅资产发布方团队(自有资产为 null)${timestamp}StringISO-8601 UTC 触发时间( freemarker 独有,legacy 不含)${rowFields.col_name!}StringGROUP BY 分组字段值(如 ${rowFields.stock_num!}, ! 表示缺失时默认为空字符串)${col_name}StringrowFields 键同时提升为顶级变量,方便直接访问
freemarker 模板示例(飞书 Markdown):
text
<#if value??>
**告警 ${ruleName}** 触发
- 字段:`${fieldName}`
- 当前值:${value}
- 阈值:${threshold}(${operator})
<#if rowFields.stock_num??>- 股票:${rowFields.stock_num!}</#if>
<#if rowFields.region??>- 区域:${rowFields.region!}</#if>
触发时间:${timestamp}
</#if>
<#if value??>...</#if> 是 FreeMarker 空值检查指令:当 value 非 null 时渲染,等价于 if value is not None:。 ${rowFields.stock_num!} 中的 ! 是默认值操作符 — 缺失时渲染为空字符串,避免 undefined variable 错误。 <#if rowFields.stock_num??> 同理 — 仅当 GROUP BY 包含该列时渲染该行,避免多余的空行。
向下兼容说明: FreeMarker 的 ${var} 语法与 legacy 正则语法完全相同 — 将 templateEngine 从 "legacy" 切换为 "freemarker" 后,现有的 ${ruleName} / ${value} 等模板无需任何改动即可继续工作。 "freemarker" 模式额外支持 <#if> / <#list> 等高级指令。
切换引擎示例:
json
PATCH /api/metric-alerts/{ruleCode}
{
"templateEngine": "freemarker",
"messageTemplate": "${ruleName}: ${fieldName}=${value} > ${threshold}"
}
成功响应: {"success":true,"message":"Alert rule patched","ruleCode":"<ruleCode>"}
skillIdmethod功能风险plugin.webhook.sendPOST通过数据源发送 webhook🟡
skillIdmethod功能风险auth.user.registerPOST注册新账号( teamName 自动生成为 tenant_${username})🟢auth.token.introspectGET查询当前 Bearer token 的身份、scope、状态与过期时间(无参数,仅限自身 token)🟢feedback.submitPOST提交反馈/Bug/需求🟢feedback.listGET查看历史反馈与官方回复🟢
feedback.submit — 提交反馈 / Bug 报告 / 功能需求路径: POST /api/feedback
必填字段:
字段类型说明categorystring反馈分类,严格枚举,见下方subjectstring主题(最多 256 字符)messagestring详细描述
category 合法值( 严格匹配,不接受缩写):
text
bug_report | feature_request | general | other
注意:
"bug"、"feature"等缩写形式均为非法 category,接口将返回 HTTP 200 +success:false。
可选字段:
字段类型说明contactEmailstring联系邮箱(可选,最多 180 字符)
请求示例:
json
{
"category": "bug_report",
"subject": "数据资产列表加载失败",
"message": "在工作区 A 下点击「数据资产」后页面空白,控制台显示 JSON.parse 报错。浏览器 Chrome 125,复现率 100%。",
"contactEmail": "user@example.com"
}
成功响应(HTTP 200):
json
{
"success": true,
"message": "Feedback submitted"
}
业务错误响应(HTTP 200 + success:false,category 非法示例):
json
{
"success": false,
"message": "Invalid category"
}
认证错误响应(HTTP 401,未登录):
json
{
"success": false,
"message": "Authentication required"
}
auth.token.introspect — 查询当前 Bearer token 的身份 + scope + 生命周期路径: GET /api/public/agent/token-introspect
调用此接口可在不触发任何业务 401/403 的前提下,查看当前 Bearer token 的身份、所属团队、token id、用户 id、有效 scope 列表,以及 token 名称 / 状态 / 过期时间。 自省范围限定为当前 token——接口不接受 tokenId 参数,永远只描述请求 header 中提供的那一个 Bearer。
输入:无 query 参数。 Authorization: Bearer lgatk_... header 是唯一身份来源。
响应示例(200):
json
{
"success": true,
"username": "alice",
"teamName": "team-alpha",
"userId": 7,
"tokenId": 42,
"tokenName": "alice-clawhub-readonly",
"status": "active",
"expiresAt": "2026-09-17T10:30:45Z",
"scopes": [
"dataasset.list",
"dataasset.get",
"dataasset.schema.get",
"dataasset.data.get",
"process.ingestion.list"
]
}
expiresAt 在 token 无过期时为字面量 null(key 始终存在)。
响应示例(401,missing/invalid token):
json
{
"success": false,
"message": "Invalid token"
}
典型用法:在调用任何 /api/** 业务接口前先 introspect 一次,把当前 token 的 scopes 与计划调用的 skill 所需 scope 做集合对比;缺失则在客户端给出友好提示,避免逐个 skill 403 试探。
stock_day 日线回测请使用 run_stock_day_backtest(单股)或 run_stock_day_portfolio_backtest(多股组合),均在 lg_utils.backtest_examples.stock_day 模块。 基金 / 黄金 / 港股日线回测 也已 ship — lg_utils.backtest_examples.{fund_day,metal_day,hk_day}.run_*_backtest,shape 与 stock_day 一致(详见下方 Python 工具库表格)。完整参数说明见 场景 4 示例。
结果可通过 investment.stock.backtest.* REST skill 检索: list(摘要)、 get(全量 JSON)、 compare(两次 metrics diff)。REST skill 当前以 investment.stock.backtest.* 命名但是 asset-class-agnostic — 通过 Python 调用 lg_utils.backtest_examples.fund_day / metal_day / hk_day 持久化的结果同样可由这 3 个 REST skill 检索( persist(name=...) 调用统一走 process_backtest_result 表,按 name 索引)。专用的 investment.fund.backtest.* / investment.gold.backtest.* REST skill 命名空间是 v2 follow-up(目前 stock-class skill 已能 cover 所有资产类的回测结果检索)。
investment_studio 解决方案权限)#Phase 4 重命名说明:skill id 前缀由
stockstudio.*改为investment.stock.*(同时新增investment.fund.*/investment.gold.*)。旧stockstudio.*id 通过 Express alias 表自动转发,新代码优先使用investment.stock.*。
skillIdmethod功能风险investment.stock.portfolio.listGET查股票持仓(query 传 asset_class=stock)。每行附带 recommendation 字段:最新一条 per-stock 推荐;完整历史走 /api/profile/portfolio-positions/recommendations?stock_num=...。新增 ?accountType=real|paper|all 参数,默认 real;显式传 paper 或 all 才会包含模拟交易数据🟢investment.stock.portfolio.createPOST新增股票持仓条目(body 含 asset_class: "stock")🟡investment.stock.portfolio.updatePUT更新股票持仓🟡investment.stock.trading.listGET查股票交易记录(query 传 asset_class=stock)🟢investment.stock.trading.createPOST录入新股票交易(自动更新持仓;body 含 asset_class: "stock")🟡investment.stock.backtest.listGET列当前团队的回测结果,支持 ?name= / ?tag= / ?programGroup= 过滤。 Summary 视图——返回 totalReturn/sharpe/maxDrawdown/... 数值列 + tags / programGroup 元数据,不含大 JSON🟢investment.stock.backtest.getGET单条回测结果详情(按 id),包含 paramsJson/metricsJson/tradesJson/equityCurveJson 等全量 JSON 负载🟢investment.stock.backtest.compareGET两次回测结果 metrics 逐项 diff( totalReturn/sharpe/maxDrawdown/...)🟢investment.stock.portfolio.nav_history.getGET组合 NAV 日历史 + 日度时间加权收益率(TWR) + 基准对比 α;详见下方 「理财组合 NAV 历史」🟢investment.stock.portfolio.nav_history.benchmarks.listGET返回平台支持的基准指数目录(4 条),附带每个指数最新可用日期与 active/stale 状态;可用于 UI 下拉选择器🟢investment.stock.portfolio.attribution.getGET组合 α/β v1 归因:一次调用返回 alpha(日度 + 年化)、beta、R²、波动率、跟踪误差、信息比率;详见下方 「理财组合归因」🟢
investment.stock.backtest.list — 过滤参数 (#15)路径: GET /api/profile/backtest-results
参数必填默认说明name否—精确匹配 name 字段tag否—子串包含匹配:返回 tags 数组包含此 tag 的所有结果programGroup否—精确匹配 program_group 字段
多个过滤条件 AND 组合。无过滤参数时返回当前团队最近 50 条。
响应示例(截取):
json
{
"success": true,
"total": 2,
"items": [
{
"id": 123,
"name": "dual_ma_5_20",
"totalReturn": 0.1842,
"sharpe": 1.34,
"tags": ["live-mock-2026Q3", "tutorial-walkthrough"],
"programGroup": "dual-ma-series"
},
{
"id": 122,
"name": "dual_ma_5_30",
"totalReturn": 0.1751,
"sharpe": 1.21,
"tags": [],
"programGroup": "dual-ma-series"
}
]
}
tags 是 JSON 数组(无 tag 时为 [],不是 null 也不省略 key)。 programGroup 是字符串(未设置时为 null,但 key 始终存在)。
写入 tags / programGroup:调用 POST /api/internal/backtest-results 时在 body 里加 "tags": ["...","..."] 和 "programGroup": "..." 字段。约束:tag 单条最长 64 字符;最多 20 条;空白 tag 自动剔除;programGroup 最长 128 字符。
skillId:
investment.stock.portfolio.nav_history.get路径:
GET /api/profile/nav-history权限: 需开通
investment_studio解决方案权限(与持仓 / 交易系列相同)
查询参数:
参数必填默认说明start否组合最早快照日期起始日期,格式 YYYY-MM-DD``end否今天结束日期,格式 YYYY-MM-DD``asset_class否stock持仓资产类别( stock / fund / gold)benchmark否TeamVariable benchmark_index → 默认 1B0300基准指数代码(见下方指数目录)。优先级:请求参数 > 团队变量 > 默认 1B0300accountType否real账户类型筛选: real(仅真实账户,默认)/ paper(仅模拟账户)/ all(全部)。默认 real;显式传 paper 或 all 才会包含模拟交易数据。
基准指数解析优先级:
?benchmark=<code> 请求参数(最高优先级)benchmark_index(团队管理员在 TeamVariable 里配置的默认基准)1B0300(沪深300)支持的基准指数(stock_day 内的 legacy 编码):
代码名称市场1B0300沪深300(默认)SH1A0001上证综指SH1B0510中证A500SH399001深证成指(数据停更于 2026-03-17,请求时会降级并返回 meta.benchmarkWarning)SZ
注意:这些是平台
stock_day表里使用的 legacy 编码,与第三方常见的xxxxxx.SH/xxxxxx.SZ格式不同,不要混用。
EOD 快照与 accountType(v1.0.30 修复):
每日 EOD 快照作业( daily_pnl_snapshot)写入 daily_position_pnl 时,从 portfolio_positions.account_type 读取 accountType 并原样写入快照行。v1.0.30 之前,快照行始终以实体默认值 REAL 写入,导致模拟账户头寸被误标记为 REAL,使真实账户 NAV 曲线出现虚高。v1.0.30 通过同步写路径修复了此问题,并通过 Flyway 迁移回填了历史数据。 如在 v1.0.30 发布前存入的快照行出现异常 NAV 尖峰,请在后端重启后检查 daily_position_pnl 中 account_id LIKE 'paper-%' 的行是否均为 account_type = 'PAPER'。
收益率计算方法(日度时间加权收益率 TWR):
平台以每日 SUM(market_value) 作为组合净值 V_t,从 trading_records 提取当日净流量 F_t = Σ(BUY 金额) − Σ(SELL 金额),按 r_t = (V_t − F_t) / V_{t−1} − 1 计算日收益率,再链乘为累积收益率 R_t = Π(1 + r_i) − 1。通过减去当日净流入,"增仓 / 减仓"带来的资金变动不计入收益,只保留纯粹的价格涨跌信号。完整数学推导见设计文档 docs/plans/2026-06-16-portfolio-nav-history-api-design.md §3a。
响应示例(含基准):
json
{
"success": true,
"data": {
"dates": ["2026-01-02", "2026-01-03", "2026-01-06"],
"portfolio_nav": [100000.00, 102000.00, 101500.00],
"portfolio_cumulative_return": [0.00000000, 0.02000000, 0.01500000],
"benchmark_code": "1B0300",
"benchmark_cumulative_return": [0.00000000, 0.01000000, 0.00800000],
"alpha_spread": [0.00000000, 0.01000000, 0.00700000]
},
"message": "ok",
"meta": {
"filledDates": [],
"benchmarkSource": "default",
"benchmarkWarning": null,
"benchmarkStaleAt": null
}
}
meta 字段说明:
字段说明filledDates因 market_value=null(当日无行情)而向前填充的日期列表;不影响收益率计算,仅作信息提示benchmarkSource``"request" / "team_variable" / "default" — 说明基准来源benchmarkWarning基准数据停更时的告警文案(如"Benchmark 399001 series stale; last available 2026-03-17");为 null 时省略benchmarkStaleAt基准最后有效日期(ISO);基准截断时此字段告知截断位置, alpha_spread 此后各元素为 null
业务错误合约:
?benchmark=BADCODE → HTTP 200, {"success": false, "message": "Unknown benchmark index: BADCODE (...)"}(不抛 500,也不返 HTML)399001)→ 仍返回完整 NAV, meta.benchmarkWarning 描述停更, alpha_spread 截断到最后有效日;不失败整个调用{"success": true, "data": {"dates": [], ...}, "message": "No snapshot history found for this portfolio"}skillId:
investment.stock.portfolio.attribution.get路径:
GET /api/profile/attribution权限: 需开通
investment_studio解决方案权限(与持仓 / 交易系列相同)
一次调用即可得到"我的策略跑赢市场了吗?"的核心数字: 年化 alpha(超额收益)、 beta(市场敏感度)、 R²(拟合度)、波动率、跟踪误差和信息比率。底层数据来自已有 nav_history.get 的日度 TWR 累积收益序列,无额外 DB 查询。
查询参数:
参数必填默认说明start否组合最早快照日期起始日期,格式 YYYY-MM-DD``end否今天结束日期,格式 YYYY-MM-DD``asset_class否stock持仓资产类别( stock / fund / gold)benchmark否TeamVariable benchmark_index → 默认 1B0300基准指数代码,优先级与 nav_history.get 相同accountType否real``real(仅真实账户)/ paper(仅模拟)/ all(全部)
计算方法(α/β v1):
由 nav_history.get 返回的日度累积收益序列,经链式反推 R_p[i] = (1+CR[i])/(1+CR[i-1]) - 1 得到日度收益率序列,再以 OLS 回归计算:
指标公式beta``Cov(R_p, R_b) / Var(R_b)(样本方差,N-1 分母)alpha``mean(R_p) - beta × mean(R_b)(日度)alphaAnnualized``alpha × 252``rSquared``Cov(R_p, R_b)² / (Var(R_p) × Var(R_b))``portfolioVolatility``stddev(R_p) × √252(年化)benchmarkVolatility``stddev(R_b) × √252(年化)trackingError``stddev(R_p − R_b) × √252(年化主动风险)informationRatio``(mean(R_p) − mean(R_b)) × 252 / trackingError
所有数值字段精度为 4 位小数( BigDecimal scale 4)。
响应示例(含基准,合成数字):
json
{
"success": true,
"data": {
"start": "2026-01-01",
"end": "2026-06-17",
"tradingDays": 115,
"benchmarkCode": "1B0300",
"benchmarkSource": "team_variable",
"portfolioCumulativeReturn": "-0.1319",
"benchmarkCumulativeReturn": "0.0528",
"alpha": "-0.1847",
"alphaAnnualized": "-0.4123",
"beta": "0.8742",
"rSquared": "0.6418",
"portfolioVolatility": "0.2156",
"benchmarkVolatility": "0.1834",
"trackingError": "0.0941",
"informationRatio": "-1.9628"
},
"meta": {
"benchmarkWarning": null,
"benchmarkStaleAt": null,
"sampleSize": 115,
"minSampleWarning": null,
"computationWarning": null
},
"message": "OK"
}
22 个字段总览(D12 文档锁):
15 data 字段: start, end, tradingDays, benchmarkCode, benchmarkSource, portfolioCumulativeReturn, benchmarkCumulativeReturn, alpha, alphaAnnualized, beta, rSquared, portfolioVolatility, benchmarkVolatility, trackingError, informationRatio.
5 meta 字段: benchmarkWarning, benchmarkStaleAt, sampleSize, minSampleWarning, computationWarning.
2 envelope 字段: success, message.
meta 字段说明:
字段说明benchmarkWarning基准缺失 / 对齐窗口过短时的告警文案;null 表示正常benchmarkStaleAt基准最后有效日期(ISO);基准截断时告知截断位置sampleSize对齐后的原始序列天数(等于 tradingDays)minSampleWarning对齐后日度收益点数 < 5 时的告警 (OLS 最小有效样本)(含实际点数);null 表示正常;fires when aligned daily returns < 5 (OLS minimum)computationWarning基准方差接近零(β 无法定义)时的告警;null 表示正常
边界行为合约:
beta/rSquared/trackingError/informationRatio 全为 null, meta.benchmarkWarning = "Benchmark series too short for α/β computation after alignment"tradingDays 太短)→ 同样置 null, meta.minSampleWarning 含实际点数meta.computationWarning = "Benchmark variance near zero; β undefined"nav_history.get 行为一致,返回空序列 + success:true?benchmark=BADCODE → HTTP 200 {"success": false, "message": "..."}lg_utils.backtest_examples.fund_day Python 入口,REST skill investment.stock.backtest.* 可检索结果)skillIdmethod功能风险investment.fund.portfolio.listGET查基金持仓(query 传 asset_class=fund; market 取值 OF/ETF/LOF)🟢investment.fund.portfolio.createPOST新增基金持仓(body 含 asset_class: "fund")🟡investment.fund.portfolio.updatePUT更新基金持仓🟡investment.fund.trading.listGET查基金交易记录(query 传 asset_class=fund)🟢investment.fund.trading.createPOST录入新基金交易(body 含 asset_class: "fund")🟡
lg_utils.backtest_examples.metal_day Python 入口,REST skill investment.stock.backtest.* 可检索结果)skillIdmethod功能风险investment.gold.portfolio.listGET查黄金持仓(query 传 asset_class=gold; market 取值 SGE/BANK; stock_num 取值为 SGE 规范码,2026-08-25 起共 10 个: Au99.99 / Au99.95 / Au100g / Au(T+D) / mAu(T+D) / iAu99.99 / iAu100g / iAu99.5 / Ag99.99 / Ag(T+D)(即 metal_codes ∩ metal_day,全部 market=SGE;旧写法 AuTD 现规范化为 Au(T+D) 而非 mAu(T+D)))🟢investment.gold.portfolio.createPOST新增黄金持仓(body 含 asset_class: "gold")🟡investment.gold.portfolio.updatePUT更新黄金持仓🟡investment.gold.trading.listGET查黄金交易记录(query 传 asset_class=gold)🟢investment.gold.trading.createPOST录入新黄金交易(body 含 asset_class: "gold")🟡
⚠️ Paper-trading tokens are platform-issued, NOT self-mintable. The
paper.*scope namespace is reserved — callingPOST /api/subscription/tokenswithscopes: "paper.account.read"(etc.) returns HTTP 400 +{"code":"RESERVED_SCOPE"}. Paper-execution tokens are auto-minted by the platform when a strategy is bound to a paper account via the UI (PaperExecutionTokenService.mintForExecution). If you want to drive paper trading from an external agent, mint the token via the strategy-binding flow first, then call the endpoints below with that token.
新 v1.1:单户模拟盘,行情打通
stock_day实时价;MARKET 同步成交,LIMIT 走每 60s 撮合 + 15:00 自动 EXPIRED;T+1、一手 100 股、A 股标准手续费全部强制。 与真金账户完全隔离——真金交易记录走investment.stock.trading.*,二者数据库行级互不影响。
skillIdmethod功能风险investment.paper.account.getGET查模拟账户当前状态:现金 / 初始资金 / 累计重置次数 / 总市值。首次调用 lazy-create 默认账户。 多账户(2026-08-18):可选 query account_id= 指定具体账户(含已归档),省略则解析为调用方的 DEFAULT 账户;不属于调用方的 account_id 一律 404 "Account not found"(不区分「存在但不是你的」与「根本不存在」)🟢investment.paper.accounts.listGET新增(2026-08-18):列出调用方名下全部账户(含已归档),DEFAULT 账户排最前。用于发现 account.get / positions.list / orders.list 可传的 account_id 值——绑定到具名(非默认)账户的策略只能靠这个接口拿到自己的 accountId。复用 paper.account.read scope,能读默认账户的 token 无需额外授权即可调用🟢investment.paper.orders.submitPOST下单: {stock_num, market, side: BUY/SELL, order_type: MARKET/LIMIT, qty, limit_price?, client_order_id, account_id?}。 client_order_id 必传(同 user + 同 client_order_id 幂等)。MARKET 同步返回成交价;LIMIT 返回 SUBMITTED,撮合后由 scheduler 推到 FILLED/EXPIRED。 account_id(2026-08-18,实际受限):只有携带 X-Agent-Mode:true 但 不带 X-Process-Execution-Id 的调用(即非 Process 来源, source=API/UI)才能真正指向具名账户;只要请求同时带了这两个 header( source=PROCESS—— lg.paper.* Python SDK 的每一次调用都属于这一类,因为 _EXEC_ID 来自调度器注入的 execution_id 作业变量),后端会把 account_id 强制钉死在 DEFAULT 账户,传其它值一律 HTTP 400 "PROCESS-sourced orders must target the default account"——即使那个账户确实是调用方自己的( PaperTradingController.placeOrder 的 LA REVISE v2 BLOCKER 3)。换言之: Process Python 节点里的 lg.paper.submit_order(account_id=...) 今天对非默认账户必然失败,该参数只对直接持 PAT 调 HTTP 接口(不带 execution-id header)的外部调用方有意义🟡investment.paper.orders.listGET查订单(支持 status= / from= / to= / account_id= 过滤, account_id 省略同样解析为 DEFAULT 账户,无 PROCESS 来源限制——与 orders.submit 不同);返回字段含 status / source / filledPrice / filledQty / fees / rejectReason 等🟢investment.paper.positions.listGET查模拟持仓(只返 account_id LIKE 'paper-%' 的行,不会和真金混在一起)。可选 query account_id= 指定具体账户,省略则为 DEFAULT 账户,无 PROCESS 来源限制🟢
Scope 集(process-execution 默认): paper.orders.write paper.account.read dataasset.read。这是后端在 process 起跑时自动注入的 scope-limited 短期 Bearer。 investment.paper.accounts.list 复用 paper.account.read,因此默认 process-execution token 已经具备列账户的能力,无需额外配置。
Scope guard 边界:带 paper scope 的 Bearer 只能 访问 /api/wealth/paper/**、 /api/data-assets/**、 /api/auth/current、 /api/public/agent/token-introspect;其他 namespace(如 /api/trading-records 真金路径)一律 403 scope_insufficient。 paper.* 不可自助签发—— POST /api/subscription/tokens 传 paper.* scope 一律 400 RESERVED_SCOPE,个人设置 → Token 管理页面同理拒绝;外部 Agent 想拿到带 paper scope 的 Bearer,只能走上方「场景 6:模拟交易」一节描述的两条路(Process python_script 节点自动注入的短期 Bearer,或管理员按策略绑定流程手动铸的 process-execution token), 不存在自助签发这条路。
典型用法(在 Process Python 节点里跑量化策略):
python
import lg
execution_id = lg.get_variable("execution_id")
quote_resp = lg.get_asset_data_realtime("stock_day",
filter_column="stock_num", filter_value="600000",
filter_operator="eq", size=1)
if not quote_resp["success"]:
print(quote_resp["message"])
return
quote_rows = quote_resp["data"]
positions = {p["stockNum"] for p in lg.paper.get_positions()["items"]}
if quote_rows and quote_rows[0]["close_price"] < 9.50 and "600000" not in positions:
# Preferred form: bare stock code + explicit market.
# The dotted form ("600000.SH") is also accepted — the SDK and backend both
# auto-strip it via PaperOrderService.normalizeStockNum (Bug #51-A).
lg.paper.submit_order(
symbol="600000", market="SH", side="BUY", qty=100, order_type="MARKET",
client_order_id=f"{execution_id}-pufa-entry", # 用 execution_id 做幂等键
)
资源市场已上线 starter_paper_trade_strategy 模板,「免费接入」一键复制到你的 tenant 即可改写。
实时热窗口查询( lg.get_asset_data_realtime):
python
# Real-time hot-window query (last 365 days, PG mirror, low-latency)
result = lg.get_asset_data_realtime(
"stock_day",
page=1, size=50,
filter_column="day_id",
filter_value="20260701",
filter_op="eq",
order_by="day_id",
order_direction="desc",
)
if not result.get("success"):
print(f"Realtime query failed: {result.get('message')}")
# Common message: "This asset has no realtime mirror"
# → Ask an admin to populate `realtimeDataSource` on the asset via
# PUT /api/data-assets/{id}
else:
rows = result["data"]
total = result["totalElements"]
print(f"Got {len(rows)} rows of {total} total")
回测模式(Ship 5-H,同一份脚本两种跑法):在 Process 执行环境上额外设置 LG_PAPER_MODE=backtest + LG_PAPER_BACKTEST_FROM=YYYY-MM-DD + LG_PAPER_BACKTEST_TO=YYYY-MM-DD, 同一脚本不改一行代码 即对历史 stock_day 回放。此模式下:
submit_order 等调用走 lg_utils.paper_sim 进程内模拟器;后端 /api/wealth/paper/** 不会收到任何请求; paper_order 表不会写入新行。 PaperScopeGuard / process_execution_secret / __LG_PAPER_TOKEN__ 全部不被消耗(token 若存在则 DEBUG 日志一行后忽略)。FeeCalculator / PriceLimitValidator / PaperOrderService (T+1 + 一手) 为规则正本;Python 模拟器有 fixture 平价测试 ( paper_sim_fixtures.yml + PaperSimRuleParityTest) 守护,任何规则方法 PR 必须同步重新生成 fixture + 更新 Python,否则 CI 红。paper.orders.write 等 scope 在 backtest 模式下完全用不到;agent / PAT / process-execution token 的 scope 集合不需要任何调整。lg.paper.persist_backtest(name=...)(无 atexit 隐式持久化;防止脚本异常中断时写入「半拉子结果」造成数据污染)。结果写入 process_backtest_result,UI 走 /profile/backtest-results。dacp_suspended_stocks 是前向运维表,回测刻意不查;这是文档化的 KNOWN_DIVERGENCE,不是 bug。资源市场已上线 starter_paper_trade_strategy_backtest 模板。设计文档: docs/plans/2026-06-11-paper-trading-ship-5-h.md(LA-APPROVED v3)。
lg.paper 完整 API 速查表(live + backtest 两种模式共用同一函数签名;运行时由 LG_PAPER_MODE env var 决定走 HTTP 还是走进程内模拟器):
函数模式签名返回备注lg.paper.submit_order(symbol, side, qty, order_type="MARKET", limit_price=None, client_order_id=None, market=None, asset_class=None, account_id=None)live + backtest见左dict。 两端共有: id( 主键 — 不是 orderId,两端一致)、 clientOrderId、 stockNum、 market、 side、 qty、 orderType、 limitPrice、 assetClass、 status、 rejectReason、 fees、 filledQty。 已成交价键名两端不一致:backtest 用 fillPrice,live 用 filledPrice(pre-existing 命名分歧;agent 代码需做 r.get("fillPrice") or r.get("filledPrice") 兼容写法)。 时间戳键两端不同:backtest 仅返回 tradeDate(推进到的模拟交易日);live 额外返回 submittedAt/ filledAt(PaperOrder 实体时间戳)但 不返回 tradeDate。 live 独有键: accountId、 submittedAt、 filledAt、 source — backtest 路径无此四键。symbol 推荐裸代码 + 显式 market="SH"/"SZ"/"BJ"/"HK";带 .SH/.SZ/.BJ/.HK 后缀也接受(SDK 客户端 + 后端 PaperOrderService.normalizeStockNum 都会自动剥离 — Bug #51-A)。 client_order_id 用作幂等键。 order_type="LIMIT" 时 limit_price 必填。 account_id(2026-08-18,live 模式实际受限):省略/None ⇒ DEFAULT 账户(与旧行为字节级一致)。因为这个 SDK 恒定跑在 Process 执行里( _EXEC_ID 来自调度器注入的 execution_id 作业变量),后端恒把请求判为 source=PROCESS,而 PROCESS 来源的订单被硬性钉死在 DEFAULT 账户——传任何非默认 account_id 都会得到 HTTP 400 "PROCESS-sourced orders must target the default account",哪怕那个账户确实归调用者所有。backtest 模式忽略该参数(单账户沙盘)。lg.paper.get_account(account_id=None)live + backtest见左dict。 回测: {"initialCapital", "cashBalance", "positionValue", "totalEquity", "unrealizedPnl"}。 live: {"id", "accountId", "displayName", "isDefault", "initialCapital", "cashBalance", "baseCurrency", "status", ...} — 注意 live 路径 不返回 positionValue / totalEquity / unrealizedPnl(这些是模拟器进程内汇总,不是后端持久化字段)。回测模式下来自模拟器持仓估值;live 模式查询后端 /api/wealth/paper/account → PaperTradingController.toAccountMap()。 account_id(2026-08-18 新增):省略/None ⇒ DEFAULT 账户;不属于调用方的 id 一律 404 "Account not found"。backtest 模式忽略该参数(单账户)。此函数 没有 submit_order 那条 PROCESS-来源限制——任何来源都能按