Add a reader for per-symbol geometry sidecars (<SYMBOL>.meta.json) returning neutral InstrumentGeometry #3
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The export pipeline that produces this server's
SYMBOL_YYYY_MM.m1/.tickfiles now also writes one small per-symbol JSON sidecar next to them in the data
directory:
<SYMBOL>.meta.json. Verified: 30 such files in/mnt/tickdata/Pepperstone, allschemaVersion2, mtimes 2026-06-25 13:26–13:32.They carry the price-interpretation geometry the bars themselves lack — how many
decimals a price has, what a pip is, what one lot is worth, which currency the
price is quoted in.
This server already owns that directory, the symbol index over it, and the
binary price format. The sidecar is the price stream's own units/geometry, so it
belongs behind the same reader rather than being parsed by each downstream
consumer.
What
Add a reader (e.g.
symbol_meta(symbol) -> Option<InstrumentGeometry>) that loads<SYMBOL>.meta.jsonand returns neutral geometry — nothing provider-specificescapes the API. Pair it with the existing
has_symbol/symbolssurface so aconsumer can ask one server for a symbol's bars and its geometry.
Sidecar schema (verified, schemaVersion 2)
A flat JSON object, one per symbol. Provider quirks the reader must absorb so they
never reach a consumer:
1E-05); valid JSON, parses as a number.null.schemaVersiongates compatibility (currently2); an unknown version is areader-side decision, not a consumer's.
baseAssetis sometimes a provider-specific code, not clean ISO:GER40 → DE40,FRA40 → F40,NAS100 → USTEC,JPN225 → JP225.quoteAssetis a realcurrency across the 30 samples (EUR/USD/JPY/HKD/GBP).
Suggested neutral output
A flat geometry struct:
digits,pip_size,tick_size,lot_size,base,quote. A consumer derives the rest, e.g.pip_value_per_lot = lot_size * pip_size(inquote): EURUSD100000 * 0.0001 = 10USD, USDJPY100000 * 0.01 = 1000JPY, XAUUSD100 * 0.1 = 10USD — all verified against the samples.Scope boundary — geometry only
The sidecar deliberately carries only facts intrinsic to the historical price
series and effectively constant over its multi-year span. It excludes
time-varying / account-dependent conditions (commission, swap, deposit-currency
pip value, volume limits, trading status). Those are a separate, time-indexed
concern and are not this reader's job.
Why here, not in the consumer
Keeping the sidecar's filename, schema version, and provider quirks (the
DE40/USTECnaming, the1E-05formatting,null-for-NaN) inside this broker-facinglibrary is what lets the downstream consumer stay broker-agnostic. Splitting it —
bars read here, geometry read by the consumer — would put two readers with two
independent path assumptions on one directory, free to drift.
Dependency note
Parsing the flat object adds a JSON dependency (
serde_json) or a smallhand-rolled parse. Minor;
serde_jsonis the straightforward call.Consumer counterpart
The aura-side consumer of this neutral geometry is
http://192.168.178.103:3000/Brummel/Aura/issues/143. This server is theupstream provider; that issue slots the neutral geometry into aura's
instrument-reference resolution hierarchy (the recorded-metadata tier of
http://192.168.178.103:3000/Brummel/Aura/issues/124), cross-checks it againstthe hand-authored vetted table, and never reads the raw JSON itself.
Design reconciliation (specify)
Spec:
docs/specs/0001-symbol-meta-geometry-reader.md. The forks below arestill listed open on this issue; this records their resolution. All are
derived orchestrator decisions (autonomous
/bossrun), not minuted userdecisions — each carries its rationale and is vetoable.
Fork: base/quote — normalize provider codes vs pass through → pass the
raw
baseAsset/quoteAssetstrings through unchanged.Basis: derived. The 30 live sidecars show
baseAssetis wildlyheterogeneous — clean ISO (
EUR,USD), index codes (DE40,USTEC,JP225,US500,VIX), descriptive names (Bitcoin,Copper,Soybeans), and stock tickers (AAPL.US,GOOG.US). There is nowell-defined "neutral" target for an index/stock/commodity base, so
normalization is not even definable here, and no mapping table exists. The
consumer (Aura #143) explicitly cross-checks the value against a
hand-authored vetted table and never reads the raw JSON — resolution is the
consumer's job by design. "Neutral" is therefore read as: the struct's
shape and semantics are broker-agnostic (a plain Rust type, parsed numbers,
no
serde_json::Value, no raw JSON, no provider filename leaking), not thatevery string is canonicalized.
Fork: unknown
schemaVersion→ returnNone(and log a one-line stderrwarning). Basis: derived. The API is already
Option; the issue states theversion decision is "reader-side, not a consumer's". House style degrades
failures to
None/empty with aneprintlnwarning (see lib.rs loadhelpers). A missing sidecar is normal (a symbol may have bars but no
sidecar) → silent
None; a present but unsupported/unparseable sidecar →None+ warning.Fork: null / non-finite numeric fields → any
nullor non-finite corenumeric field makes the geometry incomplete →
symbol_metareturnsNone(never a holed struct). Basis: derived. The issue frames a flat geometry
struct (not per-field options) and the consumer derives arithmetic
(
pip_value_per_lot = lot_size * pip_size) needing every field. "Absorbnull-for-NaN so it never reaches a consumer" is honored most literally by
emitting a complete geometry or
None. (No nulls exist in the current 30samples; this is a defensive policy with no present observable effect.)
Fork: field types →
InstrumentGeometry { digits: u32, pip_size: f64, tick_size: f64, lot_size: f64, base: String, quote: String }. Basis:derived.
digitsis a small precision count (samples 1/2/5);lot_sizeisf64because the consumer multiplies it bypip_sizeandf64representsthe integer sizes (1/100/100000) exactly. The six fields are exactly the
issue's "suggested neutral output";
symbol/description/generator/generatedAtUtcare deliberately excluded (the most provider-specificstrings, and
symbolis already known to the caller).Fork: JSON parsing — serde_json vs hand-rolled → add
serde_json(only;no serde-derive), parse via
serde_json::Valuefield extraction. Basis:derived + issue-endorsed. The issue calls serde_json "the straightforward
call"; scientific notation (
1E-05, present in 7 files), string escaping,and robust number parsing are exactly what a hand-rolled parser botches.
Valueextraction keeps direct deps minimal (one new direct dependency, noproc-macro). This expands the README's "depends only on chrono, regex, zip"
boast — a deliberate, issue-sanctioned change; the README dependency line is
updated as part of the work.
Fork: caching vs read-on-demand → read + parse the file fresh on each
symbol_metacall (no cache). Basis: derived. Geometry files are tiny andread rarely (typically once per symbol); the issue requests no caching;
adding a cache map + invalidation is complexity for no stated need. Contrast
the bar
FileCache, which exists for large, repeatedly-streamed files.Fork: receiver & index coupling →
DataServer::symbol_meta(&self, symbol: &str) -> Option<InstrumentGeometry>; does not require the symbol to bein the bar index. Basis: derived. No prefetch/streaming → no
Arc<Self>needed. The file is read directly at
<base_path>/<symbol>.meta.json(symbols with dots like
AAPL.USwork — the files are literally named thatway); a missing file →
Nonecovers both "unknown symbol" and "known symbolwithout sidecar". Requiring index membership would add coupling the issue
does not ask for.
New module
src/meta.rsholdsInstrumentGeometry+ the parser; thesymbol_metamethod lives onDataServerin lib.rs. Public pathdata_server::meta::InstrumentGeometry, consistent withdata_server::records::*.