Story: Model every output ORE writes

Table of Contents

This page documents a story in Sprint 29. It captures the goal, current status, acceptance criteria, and the tasks that compose it.

1. Goal

Every output family ORE writes is named in one catalogue, read by ores.ore from the samples checked in, and stored by the component that owns the data.

2. Status

Field Value
State DONE
Parent sprint Sprint 29
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-11-03

3. Acceptance

  • A catalogue names every output family, the file parameter that names it, its key dimensions and its columns, and agrees with the samples checked in.
  • ores.ore reads every results family and holds no store: no session, no table, and no mapping to another component's entity.
  • Every run-result table in ores.reporting is fed by the post-processor, which calls the readers and does not parse a file of its own.
  • Every market-state output is fed by ores.marketdata's ingest, which calls the readers.
  • Every output family is parsed into rows in a table; no family is stored only as a blob.
  • A report definition states which families its runs store as rows, and a new definition stores every family.
  • A test reads every checked-in expected output and lands it in the model, or states why the file is out of scope.
  • A run's outputs load in blocks, in one transaction for each run, so a refusal leaves nothing behind.
  • One knowledge page states every family's file, key and columns with their types and units, generated from the catalogue so the page and the readers cannot drift.

4. Tasks

Task State Start End Description
Name every output ORE writes DONE 2026-10-30 2026-10-31 ORE publishes no list of what its outputs contain. The truth is in four places: our file_type seed names 55 output parameters, the manual describes some reports, the writers declare every column, and 473 checked-in expected outputs show the real shapes. Build one catalogue in ores.ore, as document_kind is a catalogue: one row per output family with the file parameter that names it, the analytic codes that write it, its key dimensions and its columns. Derive it from the writers and confirm it against the samples.
Give every family its column roles DONE 2026-10-31 2026-10-31 The catalogue names every column and its kind but not its job. Give each family three roles: the keys that identify a row, the qualifiers that state the context of a figure, and the measures that are the figures, and say whether its measure set is open, which is how a configuration-driven family gains a column per curve, factor or quantile.
Read the metric outputs DONE 2026-10-31 2026-10-31 One declaration-driven reader serves every family whose row is keys, qualifiers and figures: the trade figures, the observation series, the pillar files and the configuration-driven ones. Each measure column becomes one metric named by its heading, so a curve or a risk factor needs no reader of its own.
Read the child-row outputs DONE 2026-10-31 2026-10-31 Three families have a row that is a child of a trade rather than a bag of figures: flows, stresstest_flows and todaysmarketcalibration_cashflows. Their meaning is the relation between columns, so each gets a typed row and the one reader the metric reader cannot serve.
Give reporting the run-result tables DONE 2026-10-31 2026-10-31 Reporting owns what a run produced, in three shapes rather than one per family: a metric hypertable, a cashflow table, and a view that serves the run, level and compare reads so no screen learns a family. The post-processor calls the readers and writes each family in one bulk insert.
Give marketdata the market-state tables DONE 2026-10-31 2026-11-01 The scenario cubes and today's market are market states, not results: marketdata stores the wide cube as one compressed payload whose index names the dates, scenarios and factors, and the families the store already shapes as curve points and fixings through the readers.
Gate the corpus DONE 2026-10-31 2026-10-31 One test walks every checked-in expected output, asks the catalogue which family it is, reads it and requires the read to succeed or the file to be listed out of scope. A sample may omit an open measure or state an extra one; the required columns are the test, not an exact header.
Document every output file format DONE 2026-11-02 2026-11-02 The last thing the story owes, and the thing a person actually reads: one page under doc/knowledge/external that states, for every family, the file name and the parameter that names it, the analytic that writes it, its key, every column with its type, the header variants the samples show, and one sentence for what each figure means and its unit. Generate the column tables from the catalogue so the page cannot drift from the reader, write the prose beside them, and cite a checked-in sample file for every family. Link the page from the ORE knowledge page and from the journey that reads a run's output. This is the list that does not exist upstream.
Cover the no-sample families with synthetic data DONE 2026-11-02 2026-11-02 The seed declares 55 output parameters and the samples ship the file of 17, so 38 outputs have no shape in the corpus: jacobi, jacobi_inverse and crossgamma, productclass_mapping, calibration with its pca and meanReversion outputs, creditcurves, crif, marketObjects and riskFactors, the scenario dumps and cubes, timeaveragedexposure, dim_evolution, dim_regression, additionalResults and sacva's net sensitivity file. Read their columns from the writers and state one synthetic fixture for each, marked as such in the catalogue, so the reader and the gate prove them without pretending ORE shipped them.
Fan a run's output out at the node DONE 2026-11-01 2026-11-01 The node uploads each output file the engine wrote as its own compressed object, with a manifest, so reporting and marketdata each fetch the few files they model. Today both download the whole output archive and unpack it, twice per run, which does not hold at grid volumes.
Store a run's figures in blocks DONE 2026-11-02 2026-11-02 The post-processor reads a family whole and inserts it in one statement, so a large run holds every figure in memory before the first row is written. Read a block of rows, write it, read the next, so memory per run is bounded whatever the family states.
Keep the market state in the object store ABANDONED     A cube is tens to hundreds of megabytes. Held as base64 text in a row it adds a third on top of gzip and puts the payload in the database, which is the wrong place for grid throughput. The row keeps the index and a storage key; the compressed file lives in the object store and is fetched only when a reader needs the values.
Document how a report run is processed DONE 2026-11-02 2026-11-02 One page states the flow of a run end to end and what post-processing does with its output: the phases the workflow invokes, what each hands the next, how the archive becomes rows in the two stores, which component owns which format, and why the shapes are what they are. The existing pipeline page predates the implementation and says only that post-processing records rows.
Land the market inputs in the market data store DONE 2026-11-02 2026-11-02 A run states the market it read in marketdata.csv, fixings.csv and dividends.csv, and the catalogue names none of them, so no reader serves them and the store keeps nothing. Catalogue the three from the writers, read them with the declaration-driven reader, and land them in the shapes ores.marketdata already holds: market observations for the quotes and market fixings for the fixings and dividends. Also decide whether todaysmarketcalibration_cashflows is a run result or market state, and record the decision in the story.
Parse the market state into timeseries tables DONE 2026-11-01 2026-11-02 Every market-state file a run writes is parsed into rows in a hypertable, so a user can query what the computation did. The NPV cubes are values of trades, so they are run results and go to ores.reporting. The simulated factor values, the stress shifts and the calibrated market go to ores.marketdata. The payload table and its index are removed, and scenario.csv moves to the run results, because it holds sensitivity figures.
Choose per report definition what a run stores DONE 2026-11-02 2026-11-02 A report definition states which output families its runs store as rows. The default stores every family, so we can see that the parsers work. When we trust them, a definition can turn off the large intermediate families, such as the NPV cubes and the scenario dumps. The raw files stay in the object store, so a run that did not store a family can be parsed later.
Store the text-valued outputs DONE 2026-11-02 2026-11-02 Four outputs state text rather than figures: additional_results, marketObjects, riskFactors and the calibrated model's XML. Each gets a row shape so no output is left unread.
Verify the stored outputs end to end DONE 2026-11-02 2026-11-03 Run reports for Acme and check, through the shell and the web, that every output a run writes lands as rows where the story says, starting with one report and a few families.

5. Result

Every output ORE writes is catalogued, read into rows and stored by the component that owns it. The catalogue names 81 families, each proven by a sample ORE ships, a run here wrote, or a fixture written from its writer, and the generator refuses a seed output that is neither a family nor excused with its reason. Run results land in reporting's metric, cashflow and text value tables; the scenarios, calibration and market inputs a run used land in marketdata's tables; every row is keyed by the result, file and line that state it. A report definition chooses the families its runs skip, storing every family by default. A run's rows load a block at a time in one transaction, which cut peak memory on a 500000-row cube from 1992 MiB to 202 MiB. ORE output formats documents every family, generated from the catalogue, and Report post-processing states the flow as built. A defect found on the way is captured, Every report run fails on an orphan commodity future part: since the conventions split, no report run reaches post-processing. The block load's wall clock is set by the per-row insert trigger, a limit recorded on its task.

The end-to-end check closed the loop. With the orphan part fixed, runs reach post-processing, and the run page shows each run's results, its errors and its files named as the ORE user guide names them. The tester signed off the NPV: on a fresh environment the group values all 73 of its trades. A first pass over every group and Hong Kong report stored valuation, curves and market, portfolio details and run statistics; the analytics that store nothing yet wait on the trade XML gaps, now the story Prove generated trades value against the ORE samples, and on the market data work.

6. Decisions

  • 2026-11-01: Every output family is parsed into rows, and the payload store is removed. This supersedes the 2026-10-30 and 2026-10-31 decisions that kept a market-state cube as one compressed payload. The user's requirement is that the parsers put all of a run's data into timeseries tables, because the intermediate state is what explains what the computation did: the NPV cubes explain an exposure and let XVA be aggregated again without a new simulation, the simulated factor values validate the model, the stress shifts explain a stress P&L, and the calibrated market explains a price. The volume concern is real, so it becomes the user's choice and not ours: a report definition states which families its runs store, and the default stores every family until we trust the parsers. The raw files stay in the object store, so a family turned off can be parsed later.
  • 2026-11-01: The NPV cubes (rawcube, netcube) are run results, not market state, because each value is the NPV of a trade or a netting set. They go to ores.reporting. scenario.csv is sensitivity output (base NPV, scenario NPV, difference for each trade and shifted factor), so it is a run result too. The simulated factor values, the stress shifts and the calibrated market stay in ores.marketdata.
  • 2026-10-30: The layering. ores.ore holds the format, the read model and one reader for each family, and no store: no session, no table and no mapping to another component's entity. The component that stores the data owns its tables and the mapping from the read model: ores.reporting for run results, ores.marketdata for market state.
  • 2026-10-30: The post-processor lives in reporting and calls the readers directly. That gives reporting a core link to ores.ore.core, the same edge marketdata already holds, and the core-link baseline records it as a reviewed exception. The alternative — the compute node marshals every family — puts sixty-six formats in the node and leaves reporting unable to read what it stores.
  • 2026-10-31: Three storage shapes, not one table for each family. A family's columns fall into three jobs: keys that identify the row, qualifiers that state its context, and measures that are its figures. About fifty families share one shape once the roles are known, so one metric hypertable takes them with the family code as a column; the three child-row families keep a typed table because their meaning is a relation between columns; the wide cubes stay payloads in marketdata. Sixty tables would be forty identical shapes and a UNION for every cross-family read.
  • 2026-10-31: One reader driven by the declaration, not one reader for each family. ORE declares its columns imperatively and adds them in loops over the configuration — curves.csv adds a column for every curve the run built — so a family's shape is not a fixed list. The reader requires the keys, carries the qualifiers and turns each measure column into one metric named by its heading. A variant then costs nothing: an extra figure is another metric, an extra context column another qualifier, and a missing open measure is not an error.
  • 2026-10-31: The 76 header variants are three different things. Configuration columns (curves, dim regression, var quantiles, scenario factors), analytic augmentation (the sensitivity analytic prepends Type, IsPar, Factor_1 and the shift to an exposure file), and one stale expected file: the 25-column flows.csv in MinimalSetup is a subset of the writer's 27. Only the first two are shapes the engine can write; the third is a corpus artefact, and the required-subset rule accepts it without pretending it is a variant.
  • 2026-10-30: The screens keep one shape. A view over the family tables serves the run, level and compare reads, so no screen learns a family and the rollups are written once.
  • 2026-10-30: Wide market state is market data, not a result, so it lands in ores.marketdata. A scenario cube is kept as one compressed payload with an index of what it holds, because a production cube holds millions of values and exploding it into rows would cost more than it answers.
  • 2026-10-30: The node stops projecting npv.csv into canonical rows. The archive it already uploads is what the post-processor reads, so there is one writer of an ORE format and not two. The run's log rows keep their own file.
  • 2026-10-31: A cashflow family is read by the same declaration-driven reader, and the three typed readers it replaces are gone. A typed row could not carry every figure a family states — flows.csv states ten, and the stressed family states each of them three times — and the table stores them all, so the reader reads each heading the catalogue calls a measure. The stressed suffix _Base, _Scen and _Diff becomes the part of the figure rather than part of its name, so a reader compares the three without learning it.
  • 2026-10-31: The metric table carries the scenario and the risk factor as columns of their own, because the corpus states them by name and a screen filters on them, and it does not carry the shift: the sensitivity files state one shift column for each factor of the run, so the shift of a figure is not one field of its row. Every other qualifier travels as one JSON object.
  • 2026-10-31: The screen reads a read model, not a second table. ores.reporting.run_result is a view over the metric table that names the engine's headings once — NPV is npv — and reads the counterparty out of the qualifiers. The result-row table it replaces held the same figures under the same names, so keeping it would have been two stores of one fact.
  • 2026-10-31: A market-state cube is stored as the file the engine wrote, not as rows. stress_scenarios.csv states a column for every curve and tenor the run built — 829 in the family's union, 789 in the widest file — so a row per value would be millions of rows per run and a read that needs one factor would unpack all of them. The payload is one compressed blob with an index beside it: the headings, the row count, and the distinct values of each key the catalogue names, capped and flagged when a key states more.
  • 2026-10-31: The index is the distinct values of the family's key columns, not a list of known column names. The catalogue already says which columns are keys, so the dates, scenarios, factors and market objects an index names are the keys of the family, and a family whose keys change needs no change here.
  • 2026-10-31: The payload is held as base64 text rather than a binary column, because the store's columns are text and a binary column would be a second way to hold a blob. The compression happens first, so the cost is a third of the compressed size and not of the file.
  • 2026-10-30: The tests read the checked-in expected outputs; the sample suite does not run. At this commit the corpus is 473 files under 192 file names, which group into 66 family shapes by name, and the widest file states 789 columns. The count is a grouping of the corpus, not a list ORE publishes.

6.1. 2026-11-02 A figure is keyed by where the engine stated it

Every row of a run's figures and of its market state is keyed by the compute result, the file and the line, with the date and the heading. A sensitivity, a cube or a scenario file states many rows with the same subject, date and heading, so nothing in the figure itself tells them apart, and the old key refused the second row. The family, subject and currency stay as columns to query by.

6.2. 2026-11-02 A family whose figures the run decides requires none of them

The catalogue generator required every column all samples share, which made a scenario family require the sample's curves and refuse any other run. A family whose measures are open requires no measure column, and the scenario families are open because their columns are the factors the run simulated.

6.3. 2026-11-02 A definition names the families it skips

A report definition states the families its runs do not store, not the ones they do, so an empty list stores everything and a family the catalogue gains later is stored without anyone editing a definition. The list is copied into the run when it starts, so a run stores what its definition said then.

6.4. 2026-11-02 The market a run read is kept beside the store, not in it

A run's marketdata, fixings and dividends files are what the engine loaded from this store, written back. They land in a run-scoped table, not as observations and fixings: writing them as observations would duplicate the store's own data and replace its provenance with the run's.

6.5. 2026-11-02 todaysmarketcalibration_cashflows stays in reporting

It is a cashflow keyed by market object, pillar, leg and number. The reporting cashflow table and reader already hold it, and marketdata would need a second cashflow table of the same shape.

6.6. 2026-11-02 A seed output is a family or is excused by name

The generator refuses a seed output that no family reads and no stated reason excuses: a binary or compressed file the engine reads back, a parameter no writer reads at the vendored version, a stem the engine writes per currency, or an output a later task reads. A family's sample is ORE's when ORE ships one, ours otherwise, and the catalogue marks a family whose only sample is synthetic.

7. Out of scope

Emacs 29.3 (Org mode 9.6.15)