Task: Import a pricing engine configuration as ORE XML
Table of Contents
This page documents a task in the Make every configuration screen follow one standard story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
A person brings an ORE pricing engine document into a pricing engine configuration, through the standard five-step import, with the server doing the parse and the review naming every addition, change, removal, unknown type and dropped element before anything is written.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Make every configuration screen follow one standard |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-28 |
3. Acceptance
- The server parses the document and answers what it holds, what the vocabulary does not know and what the target does not hold.
- The preview states added, changed and removed rows, with Keep it as the default for a removal.
- The import writes the configuration through the same save path the screen uses, and the run is the person's, with a reason and a commentary.
- The screen draws Import on the list header and the shell exposes the same two operations.
4. Plan
4.1. The pair, and where each part lives
| Part | Where | Why there |
|---|---|---|
| The two operations | ores.ore.modeling, generated to C++ and TypeScript |
RS-ORE-06: every ore.v1.ops.* operation lives in the ORE component |
| The parser, the document and the difference | ores.ore.core, domain/pricing_engines_import |
The one place that knows how PricingEngines maps onto the analytics rows, beside pricing_engine_projection which maps them the other way |
| The handler | ores.ore.service, pricing_engines_handler |
It reads and writes the configuration through analytics' own messages under the caller's delegated session |
| The routes | the BFF's pricing-engines.ts |
POST /api/reporting/pricing-engines/ore-import/preview and /ore-import, carrying the file's text and the choices |
| The steps | the web's shared OreExchange module |
RS-ORE-07: one module carries the buttons, the Export dialog and the Import steps |
| The command | ores.shell, pricing_engines import |
The same two operations, with --dry-run as the preview |
4.2. The protocol
Two requests and two responses, in the shape the currencies' pair established:
preview_pricing_engines_import_requestcarries the file's text and the target: the reporting configuration to compare against, or nothing for a configuration the file would create. The response states the root element it found, the products and settings the file holds, the names of the root-level settings, the difference in counts and in rows, and three lists of what the system does not know.import_pricing_engines_requestcarries the same file, the same target or a new configuration's name and variant, the keys of the removed rows the person kept, and the reason and commentary. The response states the configuration it wrote, what it added, changed, deleted and kept, and the rows it could not write.
A row of the difference is named by a key: a product by its type and which
product of that type it is (Swap#0), a setting by its product's key, its scope
and its name (Swap#0/engine/DiscountingCurve). The key is what a choice names,
and it is what makes two products of the same type, or a setting repeated twice
in one scope, tellable apart.
4.3. The parse
pricing_engines_import::parse reads the text with pugixml and answers the same
analytics pricing_engines_document the exporter writes: the products in their
order, each carrying Model, ModelParameters, Engine and EngineParameters, then
one GlobalParameters block, with a position per scope so the export comes back
in the order the file wrote. A text that is not well formed, or whose root
element is another document's, answers a refusal that names the root element it
found and writes nothing.
Three things the tables cannot hold are named rather than dropped:
| What | Kind | What happens to the row |
|---|---|---|
| an attribute or element with no column | dropped |
written without it |
| a product with no type, a setting with no name, a product with no model or engine | missing |
the product is left out, or the row is named and the save refuses it |
| a setting stated twice in one owner and scope | duplicate |
the first value stands, because analytics refuses a repeated setting |
| a product type the vocabulary does not hold | unknown_type |
written as the file states it, and named |
The vocabulary is analytics': the handler reads the pricing engine types and passes the codes to the difference, so the parser decides nothing about what a screen may offer.
4.4. The difference, and the person's choices
A product is matched by its type and by which product of that type it is; a setting by its product, its scope and its name, and then by which repeat of that name it is. Nothing is matched by identity, because the file states no identifiers.
Only removed rows ask a question, and the review starts every one of them at
Keep it: a file that omits a row usually omits it by accident.
pricing_engines_import::kept_by_default states that default once, so the
screen and the command cannot default differently, and the import request
carries the keys of the removed rows the person kept.
4.5. The write
The import re-reads the target and re-parses the file, so the write acts on the
configuration as it stands rather than on the preview's reading. It assembles the
document with pricing_engines_import::merge: the file's rows laid over the
target's, each row keeping the identity and the version of the row the target
already holds, so a write corrects a row rather than replacing it and the history
stays one line per setting. A kept removal is carried from the target with the
settings it owns; a removal the person dropped is absent from the document,
which is how the analytics save removes it.
It then saves through analytics.v1.pricing_engines_documents.put, the same
operation the configuration's own screen uses, so the validation, the history and
the party rules are the screen's, and the person's reason and commentary travel
on the rows, which is where that save keeps them.
4.6. A configuration the file creates
The screen's create writes the reporting configuration the record fills and then
the document, because analytics finds a document by that reporting configuration.
Both the browser and the shell import, so neither may carry that order: the ORE
service's handler registers the reporting configuration through reporting's own
put_configuration, saves the document, and removes the header again if the save
is refused, so a refused import leaves nothing behind.
4.7. The official configuration, and what is not built
The configuration screen standard makes a file put into an official configuration a proposal, with a sign-off. Sign-off is not built. The import writes directly, with the person's reason and commentary, and the review states that in as many words before the press. It is the one place this build does not reach the standard, and it is recorded as the known gap rather than dressed as an approval.
4.8. The screen
Import stands on the list header between Refresh and Workflows (RS-ORE-02), at
/reporting/pricing-engines/import, and follows the standard's five steps on the
steps UI: the file, what it holds, what the system does not know, the target
(new or existing), and the difference with Keep it as the default for a removed
row. The preview is read twice: once on leaving the file step, which states what
the file holds and what the system does not know without a target, and again on
leaving the target step, which states the difference against the configuration
the person chose.
The five steps are the shared module's, not the kind's: OreImportPage takes a
kind that declares its document, its target and its wording, so the currencies
and the pricing engines draw the same dialog. The kind's own file states what a
row is called and what the review offers for it, and nothing else.
4.9. The command
pricing_engines import <id or --new-name> --file <path> [--dry-run] sends the
same two operations, prints what the file holds, what the system does not know
and the difference, and then writes, keeping every removed row, because a command
line states no per-row choice. --dry-run stops after the preview.
4.10. The order**
- The protocol, generated to C++ and TypeScript.
- The parser and the difference in
ores.ore.core, with its tests. - The handler: the preview, then the import, with the reporting configuration's registration.
- The BFF's two routes, with their tests.
- The shared steps module, generalised for a kind with a target, and the pricing engines' Import page and list action, with their tests.
- The shell command and its tests.
- The build, the suites, the local checks, and the live round trip.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
5.1. The build
| What | Value | Command | Commit |
|---|---|---|---|
| The tree builds bare | ./compass.sh build passes |
./compass.sh build |
a74fd129ed |
| Import cases in the ORE core suite | 10 | grep -c 'TEST_CASE(' projects/ores.ore/core/tests/pricing_engines_import_tests.cpp |
a74fd129ed |
| Cases in the ORE core suite | 416 (475709 assertions) | publish/bin/ores.ore.core.tests |
a74fd129ed |
| Cases in the shell reporting suite | 577 (1599 assertions) | publish/bin/ores.shell.reporting.tests |
a74fd129ed |
| Pricing engines route tests in the BFF | 33 | grep -c 'it(' projects/ores.web/packages/bff/src/pricing-engines.test.ts |
a74fd129ed |
| Import step tests in the web package | 11 | grep -c 'it(' projects/ores.web/packages/web/src/ore/OreExchange.test.tsx |
a74fd129ed |
5.2. The live round trip
The environment was recreated and provisioned with compass db recreate -y -k and
compass smoke ticks --leave-running, which left 24 services running, the
acme_corporation tenant provisioned and 44214 ticks on 10 routes.
| What | Value | Command | Commit |
|---|---|---|---|
| Pricing engine configurations the seed holds | 23 across 4 parties | select count(*) from ores_analytics_pricing_model_configs_tbl where valid_to > now() |
a74fd129ed |
| Products and settings the export wrote for the UK party's XVA Overnight | 8 products, 19 settings, 1 configuration setting | grep -c '<Product ' / '<Parameter ' tmp/live/xva-overnight.xml |
a74fd129ed |
| The exported document, previewed against the configuration it came from | 0 added, 0 changed, 0 removed, 27 held, no issues | POST /api/reporting/pricing-engines/ore-import/preview |
a74fd129ed |
| The same document previewed against Pre-Trade Quick Price | 17 added, 1 changed, 0 removed, 7 held | POST .../ore-import/preview |
a74fd129ed |
| A wrong root element | success: false, root_element: CurrencyConfig, and the sentence naming both |
POST .../ore-import/preview with a currency document |
a74fd129ed |
| Configurations in the table around a preview | 26 before, 26 after | select count(*) ... before and after |
a74fd129ed |
| The shell import into a new configuration | 27 added, 0 changed, 0 deleted, 0 kept | pricing_engines import --new-name ... --file ... |
a74fd129ed |
| The API import into a new configuration | 27 added, configuration 01a17fd2-6bc3-7310-9d13-b33fcf99f76e |
POST .../ore-import |
a74fd129ed |
| The document exported, imported and exported again | byte identical, twice (once by the shell, once by the API) | diff tmp/live/xva-overnight.xml tmp/live/imported2.xml |
a74fd129ed |
| The review of a file that omits rows | 2 removed rows first, 4 choice buttons, 2 checked — Keep ours on each | Playwright, tmp/live/shots/step8-removals-review.png |
a74fd129ed |
6. Notes
- The parse is pugixml's, and the root element is refused with the element it found named, as the currencies' import does. The difference from that import is that nothing here is matched by identity: the file states no identifiers, so a product is matched by its type and by which product of that type it is, and a setting within its product by its scope and its name.
- The position an import writes is the place a setting takes in its own scope. The live round trip is what found that the first cut numbered it by which repeat of its name it was, which put every first occurrence at zero and let the export break the ties by id: the values were right and the order was not. The round-trip case now carries three settings of one scope in an order no sort would produce.
- The vocabulary that decides whether a product type is known is analytics', and the service reads it: a type it does not hold is reported, and the row is still written, because the tables hold any type code.
- A target the caller names and analytics refuses is a refusal of the whole preview, in analytics' own words. One case follows from that: a configuration whose header exists but which holds no document cannot be a target, because analytics' read finds nothing. A person who wants to fill such a configuration has to start a new one, or fill it from the screen.
- A change reason is required, and the analytics save keeps the one a row states. The import states the person's reason on every row it writes, which is how the screen's own save carries one.
7. What the standard asks that this build cannot express
The configuration screen standard's Import section, step 4, says that a file put into an official configuration is a proposal, drawn for sign-off. Nothing in the system is a proposal and no sign-off is built, so the import writes directly, with the person's reason and commentary, and the target step says so before the last press rather than promising an approval it cannot make. Making it a real proposal needs the lifecycle the standard's own States section names for official configurations — a state, a reviewer and a transition — and that is a task of its own. This is the one place where the build does not reach the standard, and it is recorded as a gap rather than dressed as an approval.
Two smaller ones follow from the same shape:
- The standard says the review "claims the version read". The analytics save takes the document whole and stamps it, so the import carries the version of every row it touches and the save's own conflict rules apply; there is no separate claim the screen makes and the service checks.
- The standard's third step names three kinds of unknown. This document has a
fourth: a product with no model, with no engine, or with no type at all, which
analytics refuses as a whole document. The preview names each as
missingrather than leaving the person to meet the refusal after the press.
8. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
| Import a pricing engine configuration as ORE XML | PENDING | Runs on 21002 after provisioning. |
9. PRs
| PR | Title |
|---|---|
| #3999 | [ore] Import a pricing engine configuration as ORE XML |
10. Review
The review of #3999 found five
things. All five are taken, on
fix/import-pricing-engines-review, with the tests the review asked for.
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | A new configuration's header can be orphaned: it is registered before the vocabulary read, and the final catch never removes it | pricing_engines_handler.cpp |
Taken | The order and the rollback are now run_pricing_engines_import in pricing_engines_import_run.cpp. Everything that can refuse is read before anything is registered, the registration is the last step before the save, and the catch removes it however the run fails. The regression test makes the vocabulary read fail on a new-configuration import and asserts no configuration remains, and a second case makes the save refuse so the rollback itself is driven |
| 2 | read_target reads read->message before it tests !read |
pricing_engines_handler.cpp |
Taken | The missing reply is tested first and answers its own sentence; the service refusal is read only from a reply that came back |
| 3 | Merged rows may keep the file's random pricing_model_config_id |
pricing_engines_import.cpp |
Taken | Confirmed in the code: the analytics save writes the rows it is given as they stand (`pricing_engines_document_service::save` stamps the tenant, the party and the audit fields and nothing else), so a row would have been written under an id no configuration holds. merge now takes the configuration the document fills and stamps every row with it. The core case reads each row's id back for an existing target and for a new configuration, the run case does the same through the save, and the live run shows no product or parameter row pointing at a configuration that does not exist |
| 4 | No size cap on the request's content | pricing_engines_import.cpp, pricing-engines.ts |
Taken | pricing_engines_import::maximum_content is four megabytes, refused by parse with the size and the limit named, and the BFF states the same limit and holds its body to twice it (a body is the document and its escaping) |
| 5 | Log a failed rollback; build the type#n key once; share the removal walk between the difference and the merge |
pricing_engines_handler.cpp, pricing_engines_import.cpp |
Taken | The rollback that fails is a warning; pricing_engines_import::product_keys is the one builder of a product key, which the review's rows and a refused row's name both ask; the rows the file omits come from one walk that the difference states and the merge carries the kept ones from |
One suggestion did not fit the code as it stands: the review's regression test is asked for at the handler, and this component's handlers talk to analytics and reporting over subjects their own test server cannot stand in for without racing the running services. The orchestration was therefore made a function with each step handed in, so the test drives the order and the rollback with fakes instead of a fake broker — the property under test is the same one.
11. Result
11.1. The build
Committed on feature/import-a-pricing-engine-configuration-as-ore-xml:
| Commit | What | |
|---|---|---|
762199c75f |
The protocol: ore.v1.ops.preview_pricing_engines_import and ore.v1.ops.import_pricing_engines, generated to C++ and TypeScript |
|
05ed11f12d |
pricing_engines_import in ores.ore.core: the pugixml parse, the difference, the default for a removed row and the merge, with its tests |
|
706aaf3586 |
The handler in ores.ore.service, the reporting configuration it registers for a new one, and its registration |
|
fe2409e8c3 |
pricing_engines import <id or --new-name> --file <path> [--dry-run] in ores.shell |
|
be1c136a82 |
POST /api/reporting/pricing-engines/ore-import/preview and /ore-import, and their tests |
|
1518407e81 |
The shared steps generalised for a kind with a target, the pricing engines' Import page and list action, and the words in English, French and Portuguese | |
612c8892b6 |
The story's row and this task | |
55747a26a0 |
Current main merged in, which the bare build needed: the branch was cut before the market data work landed | |
b757d735a6 |
The equity underlyings regenerated from their model. Main's committed output no longer matched the generator and broke the bare build twice, in a unity collision and in a generated test | |
1e8b3e3b55 |
a74fd129ed |
Formatting, the regenerated list request descriptors, the ordering fix and the route walk |
cefca27ab4 |
The import's trail starts from the kind's own area, which the live run found reading "Reference data" over a Reporting screen |
11.2. What was proved
./compass.sh build(bare) passes, and passes again after every commit that followed it.- The ORE core suite passes (416 cases, 475709 assertions) and holds the 10 import cases; the shell reporting suite passes (577 cases); the analytics suites pass; the BFF suite passes (1100 tests in 81 files); the web suite passes (2232 tests in 179 files).
- The live round trip, on a database recreated with
compass db recreate -y -kand provisioned withcompass smoke ticks --leave-running(24 services, 44214 ticks on 10 routes):- The UK party's XVA Overnight exported to
tmp/live/xva-overnight.xml: 8 products, 19 settings, one configuration setting, well formed underxmllint --noout. - Previewed through the BFF against the configuration it came from: 0 added, 0 changed, 0 removed, 27 held, no issues — the export and the importer agree.
- Previewed against Pre-Trade Quick Price: 17 added, 1 changed, 0 removed, 7 held, the changed row stating "Model DiscountedCashflows → CrossAssetModel; Engine DiscountingSwapEngine → AMC".
- A currency document offered to the preview:
success: false, the root elementCurrencyConfignamed, and the sentence "This file is not a pricing engine configuration: its root element is CurrencyConfig. Choose a PricingEngines file." - The configurations in the table around a preview: 26 before, 26 after.
- The shell import into a new configuration: 27 added, and the configuration
01a17fd0-9cf9-77e2-9aec-e4c5311dd467exported again is the same document, byte for byte. - The API import into a new configuration: 27 added, the configuration
01a17fd2-6bc3-7310-9d13-b33fcf99f76e, and itsore-documentread back withcontent-type: application/xmlandcontent-disposition: attachment; filename"pricingengine.xml"= is the same document, byte for byte. - The reason and commentary the person states reach the rows: the configurations
made by the shell and the API carry
system.external_data_import, and the one made in the browser carries the reason the review started on. - Playwright drove the five steps on the running environment
(
tmp/live/drive_import.mjs, screenshots intmp/live/shots/): the counts and the product list, "everything in the file is known", the target step, the review, the reason and commentary last, and the press. A second run with a file holding one product against the seeded XVA Overnight drew the two removed rows first, each with four choice buttons of which the two Keep ours were checked, and the sentence "Writes over XVA Overnight, in place. Every row the file omits is kept unless you say otherwise, and the history records the change."
- The UK party's XVA Overnight exported to
- The live round trip is what found the one defect the unit tests could not: the ordering of the settings within a scope. It is fixed, and the round-trip case now carries three settings of one scope in an order no sort would produce.
11.3. The review branch
The five things the review of #3999 found are taken on
fix/import-pricing-engines-review (see * Review). What that branch proved
again, on a database recreated with compass db recreate -y -k and provisioned
with compass smoke ticks --leave-running:
- The UK party's XVA Overnight exported as 8 products and 19 settings.
- Imported through the shell into a configuration that exists — the case the review's third finding was about — 13 rows were added and the export of that configuration is the file's document, byte for byte. No product and no parameter row in the database points at a configuration that does not exist, which is what the finding would have caused.
- Imported through the API into a new configuration: 27 added, and the export is the file's document, byte for byte.
- A file whose product states no model is refused by the save, the refusal names
the row by its key (
Swap#0), and no configuration with that name is left behind — the rollback, live. - A body of four megabytes and one byte is refused over HTTP with "An ORE pricing engines document may be at most 4194304 bytes."
- The suites: ORE core 444 cases (478894 assertions), ORE service 6 cases (the
first this target has held), BFF 1139 tests in 83 files, and
./compass.sh buildpasses bare.
11.4. The checks
python3 build/scripts/local_checks.py --exclude-heavy runs 41 checks and passes
- The eight that fail carry main's inputs, not this branch's:
| Check | Why it fails on this tree |
|---|---|
web |
Prettier refuses eight files main merged unformatted (inbox/, marketdata/Published*). The typecheck passes, and this branch's files are formatted |
er-diagram |
The committed ER diagrams are stale against the equity underlyings main added |
money-decimal-columns |
ores.trading.equity_basket_constituent::initial_price, from main |
protocol-twins |
ores.reporting.report_result_row has no TypeScript twin, from main |
handler-permissions |
refdata::equity_underlyings:* is checked and never seeded, from main |
event-registrars |
The equity_underlying registrar is not composed, from main |
entity-writes |
A SQL function main added writes an entity table |
codegen-tests |
Two cases fail on the equity_underlyings permission seed, above |
format, bff-list-requests and unity-collisions were red when this task
started and are green now: the first two because of this branch's own files, the
third because the regeneration of the equity underlyings repaired main's
collision.