Skip to content

feat(core): 指标定义编程式注册(registerIndicatorDefinition)+ 外部指标 ui 通道 - #273

Open
EliteOtaku wants to merge 118 commits into
363045841:mainfrom
EliteOtaku:pr/indicator-definition-registration
Open

EliteOtaku wants to merge 118 commits into
363045841:mainfrom
EliteOtaku:pr/indicator-definition-registration

Conversation

@EliteOtaku

Copy link
Copy Markdown
Contributor

依赖:基于 #266(外部渲染器加载点)与 #272(Agent 工具宿主注册)——三者共同构成"外部宿主/插件 bundle 接入 KCQ 生态"的注册机制族。建议在 #266/#272 之后评审;分支含少量相邻行改动。

动机

指标目录的注册只有 @Indicator 类装饰器(依赖模块加载时机)。外部宿主/插件 bundle 与 Core 不共享模块实例时,类装饰器不可达——外部指标定义(自定义指标库等)无法进入指标目录,指标选择器/实例管理/参数系统等既有基础设施对外部指标全部不可用。

方案(与 #272 的 registerChartTool 同族)

1. 编程式注册入口

registerIndicatorDefinition(
  {
    name: 'my_overlay_indicator',      // 放宽为 string:契约 union 仅约束内置编译期拼写
    displayName: 'My Overlay',
    kind: IndicatorKind.Indicator,
    category: 'main',
    indicatorType: 'trend',
    defaultPaneId: 'main',
    mainPane: { rendererName: 'my_overlay_renderer' },
    ui: { params: [...] },              // 新增:外部指标的 UI 元数据随定义携带
  },
  rendererFactory,
)
  • 内部抽取 defineIndicator 共享组装(装饰器与编程式同一条写入路径,语义一致);
  • 同名重复注册抛错(与装饰器一致)。

2. 外部指标 UI 元数据通道

内置指标的 params/description 来自静态 uiMeta 表;外部定义无法进入该表。IndicatorDefinitionConfig / IndicatorMetadata 增加可选 ui?: { name?, description?, params? },catalog rebuild 映射改为 uiMeta[key] ?? def.ui——内置路径零改动,外部指标获得同等的参数面板能力。

验证

  • 新增 registerIndicatorDefinition.test.ts 4 用例:编程式注册进目录(catalog id=displayName)/ 同名重复注册拒绝 / ui.params 通道 / 装饰器与编程式同表;
  • 全量:252 文件 / 2680 用例全绿;tsc build 绿。

兼容性

纯增量:未注册外部定义时行为与之前完全一致;ui 为可选字段,内置路径 uiMeta[key] 优先。

Add './dist/data/provider/sources/*.js' subpath (types + import) to
package exports so hosts can deep-import builtin market-data source
registrations. Without it, hosts bootstrapping the provider registry
outside the aggregation UI get an empty registry and no routable
providers.
The bare web-component entry gets its side-effect source registrations
tree-shaken away (package sideEffects:false), leaving hosts with an
empty provider registry. New entry force-imports the five builtin
market-data sources (baostock/finshare/gotdx/mock/tradingview) and
re-exports the element so the side-effect chain survives bundling.

It also declares static capabilities for host-wired sources: the
shell's SourceRouter filters providers by source.capabilities, which
the stock flow only populates via probe() (aggregation-UI-only path),
so headless bootstrap was deadlocked with no routable provider even
after registration. Static declarations unblock routing on the first
bars request (idempotent: only fills undefined capabilities).
Add '4h' to KLinePeriod union, KLINE_PERIODS set, market data policy
initial days (90), agent tool whitelist, semantic DataConfig period
union + validator max range (180 days), and the level dropdown entry.
Hosts mapping broker resolutions (e.g. H4=240) currently have no
in-library period to land on.
applyToolSession resets the active tool via setDrawingToolId('cursor'),
which also clears the current selection. Emitting onDrawingCreated
before that reset meant a host selecting the new drawing in the
callback (TV-style floating style toolbar) had its selection wiped
immediately. Swap the order: reset the tool first, then notify.
Selected-drawing floating toolbar gains a template dropdown (filtered
by the current drawing kind) plus a 'save as template' dialog:

- KLineChart accepts an optional drawingTemplateStore prop; a
  localStorage-backed default store is used when not injected, so the
  mechanism stays host-agnostic (hosts can route to their own backend)
- apply merges the template style into the selected drawings (may add
  keys updateBatch would reject via its field-intersection guard)
- save captures the primary selected drawing's style under a name

Depends on the selection-timing fix so the toolbar actually appears
right after finishing a drawing.
Add packages/nexus-shell: a generic, business-decoupled TradingView-style
host shell (React + Vite, private workspace package, never published):

- top bar (period presets incl. 4h, theme toggle), left drawing toolbar,
  indicator panel, template panel skeleton, chart stage mounting the
  engine via the react adapter
- theme tokens as CSS variables (light/dark), all colors centralized in
  tokens.css per upstream convention
- UI strings centralized in labels.ts; template storage behind a
  DrawingTemplateStore port so hosts can inject their own backend

Engine wiring (datafeed, tool selection, indicator registry, template
store default impl) is intentionally left as documented next milestones:
this scaffold ships chrome + ports only, no product coupling.
Add a fork section (via a docs fragment so generated READMEs stay in
sync with pnpm docs:generate) describing the fork's purpose: a generic
TV-style host shell plus engine enhancements offered back upstream in
small batches. Add NOTICE with upstream attribution; LICENSE is
retained unchanged from upstream.
- ambient module stub for the web component entry (same approach as
  packages/react) instead of following into .vue resolution
- drop vite.config.ts from tsconfig include (repo convention excludes
  config files from type-check)
- type DEFAULT_INDICATORS as ReadonlyArray to avoid literal-type trap
The exports wildcard for data provider sources has no single source
file to map, so createCoreSourceAliases aborted every vitest/vite
config that consumes it (ai-runtime tests failed at startup). Wildcard
entries are runtime deep-import patterns for built output; source-alias
contexts do not need them.
The vue package source (pulled in via the web component alias) imports
~icons/tabler/* virtual modules; without the Icons plugin the chart
mount fails to resolve them and the dynamic import of the web component
entry throws.
- ChartStage hosts core createChartController directly: the react WC
  adapter exposes no controller channel and KLineChart.vue embeds its
  own toolbar chrome, so the shell now owns all chrome itself (G-10)
- left toolbar: grouped flyouts with last-used memory, favorites,
  magnet tri-state (shell-side OHLC snapping), stay-in-drawing-mode,
  measure and eraser pseudo-tools, zoom buttons
- selection style flybar: color/width/linetype/fillOpacity with
  mixed-state handling, lock toggle, template dropdown, delete
- drawing templates on nexus.* localStorage: save/apply/rename/delete
  plus auto-apply to newly created drawings (toggleable)
- pointer bridge: Shift 45-degree angle lock, Shift-click multi-select
  normalization (engine only supports Ctrl), Ctrl drag-copy via
  restore-in-place strategy, Esc/Delete keyboard flow
- deterministic seeded mock data source covering symbols and periods;
  dev server pinned to port 5273
- E2E acceptance probe scripts/probe-drawing.mjs (39 assertions, all
  green) and docs/action-checklist.md tracking TV-parity actions
…batch 2)

- SymbolPicker: dropdown with keyword filter over the mock catalog,
  Enter-to-select and recents persisted on nexus.shell.recent-symbols
- TopBar rebuilt on shell context: grouped period select (minutes /
  hours / day-week-month) re-feeds mock data on change; theme toggle
- probe-topbar.mjs acceptance probe (12 assertions) including canvas
  legend rendering verification; drawing probe regression 39/39
Add magnetSnapper pure module (weak: 8px to high/low, strong: 15px to
OHLC, X snaps to bar center) mirroring the verified nexus-shell
ChartPointerBridge.applyMagnet semantics. resolveDrawingPointer gains an
optional magnet config applied before screenToAnchor so cursor hit,
marquee and label paths stay untouched. DrawingInteractionController
owns the session-level magnet mode (off/weak/strong, Ctrl/Meta
temporarily upgrades to strong including off) and passes it only along
the drawing anchor and preview paths.
A locked drawing was still clickable, marquee-selectable and dragged
along with the selection group. Exclude locked drawings from the hit
candidates fed to HitTester, from marquee commit candidates, and from
the connected drag group so a locked drawing can no longer be selected,
boxed or moved by pointer interaction.
Shift+click in cursor mode now toggles drawing selection and preserves
the selection when clicking blank space, matching Ctrl semantics. The
shell previously normalized Shift into a synthetic Ctrl event; this
makes the engine accept both modifiers natively.
Host UIs previously had to duplicate the anchor-count table to render
step hints. Export getAnchorCountForTool plus the SINGLE/DOUBLE/TRIPLE
anchor tool lists from the drawing module and re-export them through
the controllers facade.
Hosts implementing eraser or object-tree hover needed the same filtered
hit candidates as cursor clicks but had no access to HitTester. Extract
the candidate filtering shared with findDrawingHit and expose
hitTestAt(x, y) returning the drawing under container-local
coordinates, honoring pane offset and the locked exclusion.
Channel kinds render an area primitive whose fill falls back to stroke
when unset, so fill is a kind-inherent capability rather than an
explicitly-stored style key. getBatchStyleKeys now includes 'fill' when
every target is a channel kind, letting hosts batch-edit the fill color
without changing the default style (unset fill keeps following stroke,
so no visual regression). Mixed channel+line selections keep the
intersection guard rejecting fill.
vue-tsc resolves @363045841yyt/klinechart-agent-runtime (and its
/contracts/ui subpath) through package exports, which point at a dist
that is never built in dev clones; the vite dev/build path already
aliases the same specifier to agent-runtime source. Mirror that mapping
in tsconfig.app.json paths so the whole features/agent type chain
resolves (337 type errors drop to 56 pre-existing test-file debts
outside the agent chain; use-agent-workspace implicit-any errors were
downstream of the broken imports and disappear with them).
Document magnet tier semantics (radii, candidate order, Ctrl override,
why X snaps to bar center), the resolveDrawingPointer opt-in contract,
why magnet mode lives on the controller instead of StateKernel, and the
locked-drawing interaction semantics plus the channel fill key
decision.
…xports, hitTestAt, channel fill, vue type chain)
Shift 角度锁(宿主壳层实现)会先改写指针坐标,引擎磁吸若再吸附
会造成双重改写;此前壳侧以分支互斥规避,迁移到引擎 setMagnetMode
前必须先在 resolveMagnetOptions 恢复该互斥语义。Shift 优先级高于
Ctrl/Meta 的 strong 升级,单锚点工具按 Shift 亦不吸附。

补 interaction.magnet.test.ts 用例(Shift 不吸附、Shift+Ctrl 抑制
升级),设计文档修饰键条目同步更新。
引擎自 fb5392d 起原生提供 OHLC 磁吸(档位/半径/X 吸附/Ctrl 升级
与壳侧基准逐点一致),壳侧删除重复实现:applyMagnet、其按下/移动
两处调用分支与 MAGNET_RADIUS_* 常量。偏好读取保留为
BridgeStateAccessors.getMagnet,新增桥方法 syncMagnet 由壳在桥挂载
与偏好变化时调用 dic.setMagnetMode 同步档位;持久化键不变。
…i-selects natively

引擎光标模式自 fb5392d 起原生支持 Shift 多选(ctrlKey || shiftKey
同语义:toggle 且空白不清空),壳侧删除 cursor 模式的
shiftKey→ctrlKey 事件归一化分支;clonePointerEvent 的 shiftKey
覆写字段随之失去调用方,一并移除。
橡皮擦从"点选→读选中→删"三步组合改为 hitTestAt 公开命中查询
(与点选同口径:locked 排除、pane/工作区过滤)直接删除;不再借用
光标点选语义,消除误开拖拽会话与空白点击清空选中的副作用。
磁吸/Shift 多选/橡皮擦已切换引擎原生实现(G-01/G-06/G-03),
locked 强制、锚点数表导出、通道 fill 键、vue 类型链均随 fb5392d
关闭;缺口表同步标注,仅 G-02(测量)/G-05(FVG)/G-10(WC 通道)
保持登记。
…tMagnetMode wiring, native shift-select, hitTestAt eraser)
TV 官方 Magnet Mode 文档定义 Ctrl/Command 为磁吸临时取反:off 时
临时开启、开启时临时关闭。此前引擎沿壳侧旧基准"一律强制 strong",
后半段与官方相反。临时开启强度取 strong(TV 对磁吸开的描述即吸附
OHLC 四值)。测试改写为取反三态用例,设计文档修饰键条目同步。
可见区间不含任何真实 bar 时(range.start >= data.length),Pane.updateRange 早退保留最近一次有效 priceRange 与基准价,消除拖入未来区时价格轴跳变到 {100,0} 兜底;空数据冷启动仍走原兜底。
…ading calendar

collectFutureTimeBoundaries 纯函数按外推时间 key 变化检测未来槽位边界(经 RenderContext.getTimestampAtLogicalIndex 复用 Task 5 外推 SSOT,渲染器不二次推导 session/周期);时间轴未来刻度以 text.tertiary 降级渲染,纵向网格同帧合并。
…olation

MT5 连接器 symbol-catalog 规格约定 sessionId=MT5(对应前端注册的会话),但内建注册表只有 CN/HK/KR/US,resolveSymbolMarketSession 对 MT5 品种 throw 使 futureSession 恒 null,未来区时间外推/十字签/网格在 MT5 链路整体失效。注册 MT5 -> FOREX_MARKET_SESSION(24/5 主场景),crypto 等全周品种的周末预测由索引制轴自愈兜底。
# Conflicts:
#	packages/core/src/engine/controller/__tests__/interaction.future.test.ts
#	packages/core/src/engine/data/__tests__/contentGeometry.parity.test.ts
#	packages/core/src/engine/data/chartDataManager.ts
#	packages/core/src/engine/market/cryptoMarketSession.ts
#	packages/core/src/engine/market/forexMarketSession.ts
#	packages/core/src/engine/market/marketSessionRegistry.ts
#	packages/core/src/engine/render/chartRenderer.ts
#	packages/core/src/engine/renderers/__tests__/gridLines.mode.test.ts
#	packages/core/src/engine/renderers/__tests__/helpers/futureAxisTestKit.ts
#	packages/core/src/engine/renderers/__tests__/timeAxis.crosshairFuture.test.ts
#	packages/core/src/engine/renderers/__tests__/timeAxis.future.test.ts
#	packages/core/src/engine/renderers/timeAxis.ts
#	packages/core/src/engine/utils/__tests__/chartZoomController.future.test.ts
#	packages/core/src/engine/viewport/__tests__/visibleRange.clamp.test.ts
#	packages/core/src/foundation/plugin/types.ts
#	packages/core/src/foundation/utils/sessionTimeLabels.ts
#	packages/vue/src/components/KLineChart.vue
…/getRenderer)

宿主业务 overlay(指标扩展/业务标记层)此前只能经 dev-only 的 window.__chart
自挂实例触达 useRenderer;vue/agent 层 ChartController 无注册入口。

- ChartController 新增 useRenderer/removeRenderer/getRenderer,透传核心 Chart
  实例(幂等语义一致:按 plugin.name 保留首注册实例)
- 根导出补充 RendererPlugin/RendererPluginWithHost 类型
- 新增 createChartController.renderers.test.ts 3 用例(注册/幂等/移除)
预览工作台启动(controller-ready)时加载宿主声明的渲染器插件模块:
- 声明通道:localStorage['kcq_external_renderers'](JSON URL 数组)或
  ?externalRenderers=url1,url2
- 模块契约:default/renderers/renderer 导出 RendererPlugin 或其数组
- 相对路径归一为绝对 URL 后动态 import(dev 管线只放行外部协议的动态
  import,相对路径会被重写 ?import 进模块图致 public 资产 404——实测坑)
- 单模块失败仅 console 告警,插件间与工作台互相隔离
- demo:preview/public/external-demo-renderer.js(主图中部虚线参考线)
useRenderer/removeRenderer/getRenderer 之外补 scheduleDraw(level?):外部渲染器
插件经轮询/WS 取到新数据后需要显式触发重绘(引擎仅在交互/数据变更时自绘)。
委托核心 Chart.scheduleDraw,缺省 UpdateLevel.All;测试补 1 用例(注册前后
调用均不抛)。
插件可经 host.controller 读取品种(controller.symbols 信号)与触发重绘
(controller.scheduleDraw),与 core API 面配套。
…20260929

# Conflicts:
#	packages/vue/src/components/DrawingStyleToolbar.vue
#	packages/vue/src/components/KLineChart.vue
#	packages/vue/src/composables/chart/useDrawingTemplates.ts
#	packages/vue/src/web-component-with-sources.ts
#	scripts/core-source-aliases.mjs
数据根数随周期变化,切换后保持 scrollLeft 会让视口漂进未来区
(上游 363045841#261 右缘空白放开后 scroll 不再钳制到数据区),极端时整个
可视区都是未来网格、图元创建全部落空。rAF 双帧等待数据注入后的
视口布局,再 scrollToRight 对齐最新——TV 同款切周期看最新行为。
preview dev server 增加三个本地代理:
- /api/option_levels、/api/options_gamma → 127.0.0.1:8888(cloudtrade
  后端期权插件试点数据源,同机 FastAPI)
- /api/v1 → 127.0.0.1:8090(KCQ market-data 聚合服务,MT5 XAUUSD 等
  品种 K 线),preview 直连连接器取真实行情
…ain docs)

- AGENTS.md 追加 Agent skills 段(issue tracker / triage labels / domain docs)
- docs/agents/issue-tracker.md:GitHub Issues 为唯一 triage 通道(gh CLI 约定,
  PRs 不作为请求面,wayfinder 地图/子票/阻塞依赖操作)
- docs/agents/triage-labels.md:五角色同名标签词表
- docs/agents/domain.md:单上下文消费规则(根 CONTEXT.md + docs/adr/,
  惰性创建,术语表词汇约束,ADR 冲突显式标记)
…egisterToolHost)

Tool 装饰器协议允许任意宿主把领域方法暴露为 Agent 工具,但 toolHosts
硬编码(仅内置对比品种宿主),外部宿主的 @tool 方法无法被归属解析。

- ChartController 新增 registerToolHost/unregisterToolHost(幂等去重);
  ChartAgentControllerDependencies 加可选 extraToolHosts()
- toolHosts getter 合并内置+动态宿主;browser tool registry 的 owns()
  解析消费方零改动
- 根导出补通用工具协议:Tool 装饰器 + ChartToolConfig/
  ChartToolExecutionContext/ChartToolSafety/RegisteredChartTool 类型
- 新增 toolHosts 测试 3 用例;core 251 文件 2676 用例全绿
@tool 装饰器依赖跨 bundle 不可达(外部插件与 core 不共享模块实例),
bridge 工具目录又只在构造时静态快照——外部宿主的 Agent 工具无法进入目录。

- core:chartToolRegistry 增 registerChartTool/unregisterChartTool(编程式
  写入唯一真源,冲突语义与装饰器一致);根导出同步
- vue:BrowserAgentBridgeOptions 增 extraChartTools?: () => RegisteredChartTool[]
  (惰性求值一次),BrowserToolRegistry.registerTools 合并内置+外部源
- 与 363045841#272(registerToolHost 方法归属解析)组成外部 Agent 工具完整链:
  宿主持有 @tool 类实例 → registerToolHost 注册归属 → bridge extraChartTools
  声明工具目录
插件模块可带 chartTools: RegisteredChartTool[] 命名导出;loader 收集进
externalChartTools 引用(bridge extraChartTools getter 先构造后填充)。
与 363045841#272(工具宿主注册)同族的注册表编程入口:指标目录的注册只有类装饰器
(依赖模块加载),外部宿主 bundle 与 Core 不共享模块实例时不可达。

- indicatorDefinitionRegistry:抽共享组装 defineIndicator,新增
  registerIndicatorDefinition(config, rendererFactory)(同名重复注册抛错,
  与装饰器语义一致);name 放宽为 string(契约 union 仅约束内置编译期拼写)
- IndicatorDefinitionConfig/IndicatorMetadata 增可选 ui 字段(name/description/
  params):内置指标 UI 元数据来自静态 uiMeta 表,外部指标随定义携带——
  indicatorCatalog rebuild 映射 uiMeta ?? def.ui
- 测试 +4(编程式注册进目录/重复注册拒绝/params 通道/装饰器与编程式同表),
  全量 252 文件 2680 用例全绿
- toolHosts 测试:hostInputSchema 改 Type.Object 构造(Static<TSchema> 宽泛基类
  取不出具体类型致 input: unknown);Type import 修正
- 指标注册测试:删装饰器样例(@Indicator 的 name 受契约 union 约束是上游有意
  的类型语义,外部指标走编程式,测试不以 as never 硬绕);kind/category 去
  as never 改结构推断字面量;const config 补 as const 防字面量拓宽
EliteOtaku added a commit to EliteOtaku/KCQ-NexusAI that referenced this pull request Sep 30, 2026
@EliteOtaku

Copy link
Copy Markdown
Contributor Author

真实用例验证 + 两个缺口反馈(一目均衡表套件指标化 spike)

我们在闭源侧做了一目均衡表套件的外部渲染层插件(多周期切片/延迟线/ghost 带),但外部渲染层存在宿主竞态窗口内的错帧问题(周期切换/数据更新时 RenderContext 新旧混杂,视觉表现为抖动;原生指标管线无此问题),因此评估走 #273 的编程式注册路线做指标化迁移。本周在 PR 分支 worktree 上做了端到端 spike,验证通过:

  • registerIndicatorDefinition 注册新名字 → 指标选择器列表可见、可激活 ✓
  • worker 计算(CALCULATOR_MAP key)→ visibleState.compose → rendererFactory → indicatorStateReader 全链路打通 ✓
  • 主图线/云渲染贴蜡烛,切周期(1d↔1h)600ms 内干净重渲染,拖动压测零错位伪影 ✓

缺口 ①:worker 路径无法使用外部内联 compute(阻塞闭源侧接入)

外部定义的 runtime.compute 函数无法序列化进 worker;worker 路径只认 CALCULATOR_MAP[computeKey]。外部定义不带包内 calculator key 时静默失败:[IndicatorRuntime] Unknown computeKey: undefined + 恒空结果(instanceCalculationRuntime.ts 的 inline 路径 definition.compute 本身是支持的,只是调度器没有回退)。

建议二选一:

  • computeKey 缺失时回退主线程 runtime.compute(inline 路径已具备);
  • 或提供外部 calculator 注册通道(与 registerIndicatorDefinition 同族)。

缺口 ②:visibleState.compose 的 entry 取用路径缺文档

compute 结果在 compose 收到的 entry 里是双层包裹(entry.series.<stateKey>),无类型指引,只能读源码摸索。建议在 IndicatorDefinitionConfig 文档/runtime 描述符上注明取用路径。

交付形态确认请求

外部插件 bundle 需拿到 registerIndicatorDefinition 函数本体才能注册(闭源侧不能 import 包内模块)。建议 loader 的工厂宿主参数带上该函数(与 #272 registerToolHost 通道同族),或在 ChartController 上暴露。


spike 细节:XAUUSD 1d/1h,worker compute(包内临时 calculator 打通渲染链路验证)+ 自定义 rendererFactory 画三线/云/延迟虚线;错帧对照样本与取证见我们侧记录(外部渲染层同场景存在贯通色墙伪影,原生管线同场景干净)。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant