A dashboard says WAIT. Was the spread check false, or did the engine never receive a spread observation?
Those are different failures to investigate. A trace that turns both into an empty cell loses the distinction before debugging starts.
A current discussion about trading-engine decision traces asks which evidence belongs beside a score and decision. My answer starts with a small requirement: preserve the inputs that actually existed at decision time, including explicit false values.
Give missing data its own meaning
For this example, the input contract has two boolean observations: sessionOpen and spreadAllowed. It is deliberately narrower than a real engine.
-
falsemeans the check ran and the condition was not met. - A missing key means the observation was not recorded.
-
nullis invalid for this particular contract. A system that permits unavailable observations should give them an explicit status and reason.
Do not validate a boolean with if (!value). That rejects a perfectly legitimate false result. Check presence and type separately.
A runnable boundary test
Save the following as trace.test.mjs and run node --test trace.test.mjs. It uses Node's built-in test runner, with no packages or broker connection.
import test from 'node:test';
import assert from 'node:assert/strict';
function requireSnapshot(trace) {
for (const key of ['decisionId', 'sourceEventId', 'codeVersion', 'configVersion']) {
if (typeof trace[key] !== 'string' || trace[key].trim() === '') {
throw new Error('Missing ' + key);
}
}
if (!trace.features || typeof trace.features !== 'object') {
throw new Error('Missing features');
}
for (const key of ['sessionOpen', 'spreadAllowed']) {
if (!Object.hasOwn(trace.features, key) || typeof trace.features[key] !== 'boolean') {
throw new Error('Invalid feature ' + key);
}
}
return trace;
}
const fixture = () => ({
decisionId: 'decision-example', sourceEventId: 'event-example',
codeVersion: 'example-build', configVersion: 'example-config',
features: { sessionOpen: true, spreadAllowed: false }
});
test('false is recorded evidence, not missing data', () => {
const trace = fixture();
assert.equal(requireSnapshot(trace), trace);
});
test('an omitted feature is rejected', () => {
const trace = fixture();
delete trace.features.spreadAllowed;
assert.throws(() => requireSnapshot(trace), /Invalid feature spreadAllowed/);
});
test('null is not a boolean observation', () => {
const trace = fixture();
trace.features.spreadAllowed = null;
assert.throws(() => requireSnapshot(trace), /Invalid feature spreadAllowed/);
});
test('a missing configuration version is rejected', () => {
const trace = fixture();
trace.configVersion = '';
assert.throws(() => requireSnapshot(trace), /Missing configVersion/);
});
The four tests passed locally. That result covers this small validation function only. It does not establish the correctness of a trading strategy, broker integration or production trace pipeline.
What the version fields do, and do not do
The example also requires non-empty code and configuration version strings. An absent configuration identifier makes it hard to explain why the same event behaved differently after an input change.
But a string is only a reference. This validator does not prove that the referenced build exists, that its contents match the identifier, or that the configuration was the one actually running. Production evidence needs those links to be checked and retained.
I would store the input snapshot when the decision is made. Rebuilding it later from the latest chart or a revised data feed can silently describe a different decision.
Keep the decision separate from its consequences too. A BUY decision, a submitted order request and an observed fill are three different facts. Join their records with identifiers; do not overwrite one status field until the earlier facts disappear.
Try the failure path
Start with one saved test trace. Remove a required feature and confirm the reader rejects it. Restore the feature as false and confirm it survives unchanged. Then change the configuration reference and check that your investigation tools display the different version.
This is a narrow test of evidence quality. A populated table is useful only if the values still mean what the engine meant when it wrote them.
Reference: Node.js test runner.
Top comments (1)
@stratcorealpha, preserving
falseas recorded evidence instead of collapsing it into absence is a small schema choice with a large debugging payoff. I’d extend the production form to an observation envelope such asobserved | unavailable | invalid, with source event ID and observation time, because a missing stored key could mean producer absence or ingestion loss, and a correctly typed boolean can still be stale. Keeping the decision, order request, and fill as separate joined facts is equally important. Would you require a reason code whenever an observation is unavailable so that state never falls back to ambiguousnull?