RISE Framework Specification
Spec ID: 73 Version: 1.1 Document ID: caishen-rise-pdsp-events-v1.1 Last Updated: 2026-08-01 Depends on: 70 (architecture), 72 (incremental update) Realized by: 77 (message bus)
What The Event System Enables:
Desired Outcomes:
The original carries two parallel mechanisms. Both are specified because they serve genuinely different purposes.
| In-process events | Out-of-process hooks | |
|---|---|---|
| Mechanism | Delegate/observer subscription | Executable script invoked with arguments |
| Coupling | Compile-time; subscriber is in the same process | None; subscriber is a separate program |
| Payload | Full typed object graph | Positional string arguments |
| Failure | Propagates unless caught | Isolated by process boundary |
| Extensibility | Requires rebuild | Drop a file in a directory |
| Use for | Intra-component coordination, UI binding | Cross-service chaining, operator extension |
Requirement: preserve both, but unify them behind a single event definition. In the original an event and its hook are declared in different places with different names and can drift. Define the event once; the in-process dispatch and the out-of-process invocation are two transports for the same event.
| Event | Fired when | Payload |
|---|---|---|
PriceUpdateStarting |
A refresh run begins | list of series about to be refreshed |
PriceUpdateCompleted |
A refresh run ends, with data | collection of per-series results |
PriceUpdateCompletedNoData |
A refresh run ends, nothing changed | run identity only |
SeriesUpdateStarting |
One series’ refresh begins | (instrument, timeframe) |
SeriesUpdateCompleted |
One series committed successfully | (instrument, timeframe, barsAdded, barsUpdated, lastBar) |
SeriesUpdateFailed |
One series’ refresh failed | (instrument, timeframe, error) |
PerspectiveUpdateStarting |
All timeframes of one instrument begin | instrument |
PerspectiveUpdateCompleted |
All timeframes of one instrument done | (instrument, results[]) |
SeriesExported |
An export artifact was written | (instrument, timeframe, path) |
A perspective is one instrument across all its timeframes — the unit a chart UI or a multi-timeframe strategy consumes. It is a distinct event because consumers frequently need “this instrument is fully current”, not “one of its timeframes changed”.
| Event | Owner | Fired when |
|---|---|---|
ChartDataUpdateCompleted |
CDS (spec 04) | Chaos/chart data rebuilt for a series |
IndicatorUpdateCompleted |
IDS (spec 05) | Indicators computed for a series |
StrategyRefreshStarting / StrategyRefreshCompleted |
SDS (spec 02) | Strategy evaluation cycle |
StrategyBreakoutOccurred |
SDS (spec 02) | A breakout price was reached |
Emitted by the price component itself (spec 70): Initialized,
PriceLoadingStarted, PriceLoadingCompleted, PriceLoadingError,
StateChanging, StateChanged. These are in-process only — they are too
fine-grained for cross-process propagation.
Interface EventSource:
Subscribe(eventName, handler) -> subscription
Unsubscribe(subscription)
Interface EventPayload:
occurredAt : instant
instrument : string | null
timeframe : string | null
correlationId : identifier
body : typed object
Requirements:
Fire after commit. An event announcing “series updated” must not be
observable before the data it announces is durable. The original fires
SeriesUpdateCompleted after SaveChanges — preserve this ordering strictly.
Never let a handler fail the producer. Each handler is invoked in isolation; a throwing handler is logged and the remaining handlers still run. The original wraps hook invocation in a bare catch that discards the exception entirely — preserve the isolation, add the logging.
Carry a correlation identifier. A refresh run, its exports, and every downstream reaction should share one identifier so a chain can be traced end to end. The original has no such identifier; adding one is the single highest -value improvement to the event system.
Payload is a value, not a live reference. The original passes live ORM entities to handlers, so a handler can mutate the producer’s state. Pass immutable snapshots.
<hookDirectory>/<hook-name><ext> <args...>
The argument vector differs by runner — see Standard arguments below.
hookDirectory — a configured directory, resolved relative to the running
service. The original hardcodes it, and the two runners disagree: the shared
runner uses <cwd>/../hooks/, the engine-private runner uses <cwd>/hooks/.hook-name — the event’s kebab-case name (see Hook registry below).<ext> — platform script extension. The original uses .bat; the target must
support the host’s native form.Hooks fired through the shared runner receive a name blob in position 1,
prepended by the runner itself, and a minutes token appended last
(PS.Common.Hooks/HookTools.cs:20-21):
args = "\"{name:<hook-name>}\" " + <caller args> + " " + <minutes>
So the full positional contract is:
| Position | Value | Example |
|---|---|---|
| 1 | name blob, prepended by the runner | {name:pds-pov-update-completed} |
| 2 | POV — the series identifier | EUR/USD_H4 |
| 3 | instrument | EUR/USD |
| 4 | timeframe | H4 |
| 5+ | event-specific extras | 1.09241 1.09228 |
| last | minutes token, appended by the runner | 47 |
The hook scripts confirm it — pds-pov-update-completed.bat and
cds-pov-update-completed.bat both begin:
set jsondata=%1
set pov=%2
set instrument=%3
set timeframe=%4
Run-level hooks shift accordingly: pds-update-starting.bat reads
set jsondata=%1 / set povs=%2.
Requirements:
{name:<hook>}, an
unquoted pseudo-object no script can parse. Either pass real JSON or drop it;
the hook already knows its own name from its filename.| Aspect | Original | Required |
|---|---|---|
| Concurrency | Launched on a background task, then immediately awaited — i.e. effectively synchronous with extra machinery | Explicitly asynchronous fire-and-forget, or explicitly synchronous with a timeout. Choose one and document it per hook. |
| Timeout | None. A hanging hook hangs the refresh. | Mandatory timeout; on expiry, terminate and record |
| Exit code | Ignored | Recorded; non-zero logged, does not fail the producer |
| Output | Discarded | Captured to the run log |
| Window | Hidden, no shell | Keep |
| Working directory | The hook directory | Keep |
| Errors | Swallowed entirely | Logged with hook name and arguments |
Security requirement. Hook arguments are built by string concatenation in the
original and passed to a shell-adjacent launcher. Instrument symbols contain /,
and any operator-supplied path could contain shell metacharacters. Pass arguments
as a vector, never as a concatenated command line, and never through a shell.
Names observed in the original, with their firing points:
| Hook file | Fired by | Arguments |
|---|---|---|
pds-update-starting |
Refresh run begins | comma-separated POV list |
pds-update-completed |
Refresh run ends | timestamp + POV list |
pds-pov-update-completed |
One series committed | pov instrument timeframe |
pds-pov-update-failed |
One series failed | pov instrument timeframe |
pds-perspective-update-completed |
Instrument fully refreshed | instrument |
perspective-update-starting |
Instrument refresh begins | instrument |
pov-update-starting |
One series begins | pov instrument timeframe |
pov-post-update |
After a series commits (engine-local) | pov instrument timeframe askClose bidClose |
export-pov-update |
After an export artifact is written | pov instrument timeframe exportedFilePath |
cds-pov-update-completed |
Chart data rebuilt | jsonData pov instrument timeframe tlid |
ids-pov-update-completed |
Indicators computed | pov instrument timeframe |
ids-update-completed |
Indicator run complete | — |
strategy-refresh-started / strategy-refresh-ended |
Strategy cycle | — |
strategy-bdbo-broke-thru |
Breakout price reached | jsonData pov idug message |
common-env |
Sourced by other hooks for shared configuration | — |
update-starting |
Whole-platform update begins | POV list |
Inconsistency to resolve — two runners with different argument shapes.
Shared runner (HookTools.RunHookBatch) |
Engine-private runner (Program.cs:runBatchProcess) |
|
|---|---|---|
| Hooks | the pds-*, cds-*, ids-*, perspective-* families |
pov-post-update, export-pov-update |
| Position 1 | {name:<hook>} blob |
POV |
| Trailing token | minutes, appended | none |
| Hook directory | <cwd>/../hooks/ (PS.Common.Framework/App/DTS.cs:1472-1474) |
<cwd>/hooks/ |
So a script written for one runner cannot be moved to the other, and the two disagree on where hooks even live. Define one argument convention and one directory resolution rule, and apply both to every hook.
The hooks compose a pipeline across independent programs. Reconstructed from the hook scripts:
[ Price Engine ]
│ commits series
▼
pds-pov-update-completed ──────────────▶ [ Indicator Console ]
│ computes indicators
▼
ids-pov-update-completed
│
▼
[ Chart Data Service ]
│ builds chart data
▼
cds-pov-update-completed
│
▼
[ Strategy Runner ]
│ evaluates strategies
▼
strategy-bdbo-broke-thru
│
▼
[ Ordering ]
Each arrow is a hook script invoking the next program’s CLI with the series
identity. pds-pov-update-completed launches the indicator console with the
instrument and timeframe; cds-pov-update-completed launches the strategy
runner with instrument, timeframe, and a bar identifier.
(instrument, timeframe), so the chain parallelizes across series trivially..bat files, several of which are disabled with goto skip labels around
their real content.Keep the decoupling; replace the transport.
Price Engine ──publish──▶ [ durable event log / message broker ] ──▶ subscribers
│
└─▶ hook adapter ──▶ operator scripts
Requirements:
R1 Durable publication. Events are appended to a durable log as part of,
or immediately after, the same commit that produced them. A subscriber that was
down catches up on restart.R2 At-least-once delivery with idempotent consumers. Every consumer keys
its work on (instrument, timeframe, barKey) — the deterministic identity from
spec 70 makes reprocessing harmless.R3 Correlation. Every event carries the originating run’s identifier;
every derived event inherits it.R4 Bounded concurrency. A configurable limit on in-flight downstream work
per stage.R5 Hook compatibility adapter. Retain the file-drop hook mechanism as a
subscriber to the event log, not as the primary transport. It is genuinely
valuable for operator extension and must not be lost.R6 Observability. The chain’s shape is derivable from the event log at
runtime, not only from reading scripts.The original attempted a third mechanism: a database trigger on the price table that, on update, ran Python inside the database engine and shelled out to notify downstream systems.
It was never completed — the instrument is a hardcoded literal and the notification call is commented out. See spec 71, Triggers.
Two separate mechanisms in the shipped export path do reach outward from inside
the database: sp_csv_avg_exporter__220624__noreturn writes a CSV and then
invokes a shell script hook-avg-updated.sh via subprocess.call, and
sp_ic_csv_pov_exporter does the same for the broker-format export. Those are
in-database hooks in production use. See spec 75.
Requirement: do not carry this pattern forward. A storage engine shelling out
to the host on data change is a large, hard-to-observe blast radius and it
couples the schema to the host filesystem layout. If the target store offers a
native change feed or logical replication, use it to feed the event log (R1).
Otherwise publish from the application, after commit.
The durable transport that requirements R1–R6 describe was built. It is
specified in full in 77 — Message Bus.
Summary of the relationship:
BusPublisher reference and the generated
transition code already contains the publish dispatch — but nothing assigns
a publisher in the price path, so no PDSP event ever reaches the bus.WDS.WriterGuide.StoreServiceApp).Connecting the trading chain to it is therefore assignment and configuration,
not construction. Spec 77 specifies the envelope, the publisher/subscriber
contracts, the topology, the fifteen defects to fix, and a six-step migration
path that begins with the SeriesUpdateCompleted event defined above.
| Setting | Purpose | Original |
|---|---|---|
hooks.enabled |
Master switch for out-of-process hooks | hookFeatureActivated, DTSApp.FireHooksBatch |
hooks.directory |
Where hook scripts are found | hardcoded ./hooks |
hooks.timeout |
Per-hook execution limit | absent |
hooks.cds.enabled |
Chart-data hooks specifically | DTSApp.FireHooksCDS |
events.enabled |
In-process event dispatch | DTSConf.EnableObserver |
events.correlationId |
Run identifier | absent |
The original spreads these across a static application-configuration object, an
app config file, and a .env file read at startup. Consolidate.
| # | Defect | Location | Fix |
|---|---|---|---|
| 1 | Infinite recursion. The (string, GetPriceHistoryResponseCollection) overload of FirePDSPerspectiveUpdateCompleted builds a perspectives collection, discards it, and calls itself with the same argument types. Stack overflow if ever invoked. |
PDS.Hooks/PDSHooks.cs:237-245 |
Call the (string, GetPriceHistoryResponseDtoCollection) overload with the collection it just built |
| 2 | Two runners, two conventions — different position-1 semantics, different trailing token, different hook directory. | HookTools.RunHookBatch, PDSEngine2203/Program.cs:runBatchProcess |
One convention, one directory rule |
| 3 | The {name:…} blob is not JSON despite every script naming the variable jsondata. |
HookTools.cs:20 |
Real structured payload, or drop it |
| 4 | Undocumented trailing minutes token appended to every shared-runner hook, read by none. | HookTools.cs:21 |
Remove or define |
| 5 | Arguments concatenated into a command line. Instrument symbols contain /; operator paths can contain metacharacters. |
both runners | Argument vector, never a shell |
| 6 | Hook failures swallowed entirely — bare catch, no logging, no exit-code check, no timeout. | runBatchProcess, HookTools |
Record outcome; mandatory timeout |
| 7 | Several shipped hooks are disabled in place with goto skip / goto notmuch labels wrapping their real bodies, so the chain’s actual shape is not what the files appear to say. |
Common/PS.Common.Hooks/pds-pov-update-completed.bat, cds-pov-update-completed.bat |
Delete dead branches; the chain must be readable |
| # | Scenario | Expected |
|---|---|---|
| T1 | Series commits successfully | SeriesUpdateCompleted fires exactly once, after data is durable |
| T2 | Subscriber throws | Producer completes; error recorded; other subscribers still run |
| T3 | Hook script absent | Silent no-op, no error |
| T4 | Hook script hangs | Terminated at timeout; recorded; refresh completes |
| T5 | Hook exits non-zero | Recorded; refresh result unaffected |
| T6 | Subscriber offline during a run, restarts | Missed events delivered on restart (R1) |
| T7 | Same event delivered twice | Consumer produces identical result (R2) |
| T8 | 40-series refresh | Downstream concurrency stays within the configured bound (R4) |
| T9 | Order placed at end of chain | Traceable to the originating refresh via correlation id (R3) |
| T10 | Instrument symbol containing shell metacharacters | Passed safely; no shell interpretation |
| Concept | Original artifact |
|---|---|
| In-process event firing | src/Caishen/PDS/PDS.Hooks/PDSHooks.cs |
| Hook name constants | src/Caishen/Common/PS.Common.Hooks/HookConstants.cs |
| Shared hook runner (arg prepend/append) | src/Caishen/Common/PS.Common.Hooks/HookTools.cs:14-23, Lib/HookBase.cs:19 |
| Hook directory resolution | src/Caishen/Common/PS.Common.Framework/App/DTS.cs:1472-1474 (RunHook) |
| Hook scripts | src/Caishen/Common/PS.Common.Hooks/*.bat, src/Caishen/PDSP/PDSEngine2203/hooks/*.bat |
| Engine-local hook runner | PDSEngine2203/Program.cs → runBatchProcess, runHook_export_pov_update |
| Firing point after commit | PDSEngine2203/Program.cs, after Context.SaveChanges() |
| Component lifecycle events | PDSP.Business/PDSPPriceComponentFsm.cs |
| Bus publisher scaffolding | Common/PS.Common.StateTransitionMachineries.Std/StateForge.StateMachine/CreasTypes/IBusPublisher.cs |
| In-database outward calls | gia-mssql/sp_csv_avg_exporter__220624__noreturn.*.sql (hook-avg-updated.sh) |
| Abandoned trigger | gia-mssql/PDSPPrices.Trigger-ON-lastUpdated__designing220919{,.altering}.sql |