caishen

PDSP — Event-Driven Architecture & Hooks

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)


Creative Intent

What The Event System Enables:

Desired Outcomes:

  1. A completed price update triggers indicator computation automatically
  2. Completed indicators trigger strategy evaluation automatically
  3. A strategy reaching a decision point triggers ordering automatically
  4. Any stage can be replaced, disabled, or observed without touching the others
  5. A failing downstream stage never fails the upstream one

Two Propagation Mechanisms

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 Catalogue

Price domain

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”.

Downstream domains (chained, defined by their owning specs)

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

Component lifecycle events

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.


In-Process Event Contract

Interface EventSource:
    Subscribe(eventName, handler) -> subscription
    Unsubscribe(subscription)

Interface EventPayload:
    occurredAt      : instant
    instrument      : string | null
    timeframe       : string | null
    correlationId   : identifier
    body            : typed object

Requirements:

  1. 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.

  2. 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.

  3. 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.

  4. 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.


Out-of-Process Hook Contract

Invocation

<hookDirectory>/<hook-name><ext>   <args...>

The argument vector differs by runner — see Standard arguments below.

Standard arguments

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:

Execution semantics (original, with required changes)

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.

Hook registry

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 Processing Chain

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.

What is good about this design

What must change

  1. No delivery guarantee. If the indicator console is not running, or its invocation fails, the update is lost with no record and no retry. Nothing reconciles.
  2. No backpressure. A refresh covering 40 series fires 40 hooks, each spawning a process. Nothing bounds concurrency.
  3. Unbounded fan-out latency. Each stage spawns a process; process startup dominates the actual work for a single series.
  4. Loss of causality. By the time an order is placed, nothing connects it back to the price update that started the chain.
  5. The chain is invisible. Its shape exists only as the union of several .bat files, several of which are disabled with goto skip labels around their real content.
  6. Arguments are stringly-typed. A JSON payload is passed positionally as argument 1 in some hooks and absent in others.

Target architecture

Keep the decoupling; replace the transport.

Price Engine ──publish──▶ [ durable event log / message broker ] ──▶ subscribers
                                        │
                                        └─▶ hook adapter ──▶ operator scripts

Requirements:


In-Database Change Notification

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.


Message Bus — see spec 77

The durable transport that requirements R1–R6 describe was built. It is specified in full in 77 — Message Bus.

Summary of the relationship:

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.


Configuration

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.


Defects To Fix, Not Port

# 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

Verification

# 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

Traceability

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