Asteria Investment Office — Institutional Portfolio-Risk Solver Skill
This skill helps solve portfolio-risk JSON tasks against the shared Asteria Investment
Office environment. The environment is the current book of record; local
input/payloads/ files are intake context that may be stale. Always reconcile to the
environment before answering. Return ONLY a JSON object matching the task's
answer_template.json (no narrative outside the JSON).
There are three recurring workflows. Identify yours from the prompt/portfolio id, then
follow the matching SOP. Cross-cutting conventions (precision, ordering, enums,
precedence) apply to all of them.
0. Environment access
Base URL: <remote-env-url> (GET only). Call with curl or python3 urllib.
Key endpoints and what they return:
| Endpoint |
Use |
GET /api/catalog |
All ids: portfolios, policies, indices, issuers, bonds, opportunity_sets |
GET /api/policies |
ALL policy blocks in one object: credit_default, credit_risk_reduction, correlation, allocation_mapping, multi_asset, multi_asset_risk. Also top-level as_of_date. |
GET /api/portfolios |
Portfolio summaries |
GET /api/portfolios/<portfolio_id> |
Objective, constraints (policy_id), holdings[] with quantity_usd_m/instrument_id/sleeve, market_value_usd_m, as_of_date |
GET /api/instruments/bonds |
Full bond universe. Filter ?candidate=true, ?rating_bucket=HY |
GET /api/issuers |
sector, subsector, rating_bucket, watchlist (bool), credit_outlook, research_tags |
GET /api/market/energy |
Energy commodity signals[] (score, direction, signal_id) and pitch_themes |
GET /api/indices |
Index metadata + level_start_date/level_end_date |
GET /api/index-levels |
Dict: index_id -> [ {date, level}, ... ] monthly. No precomputed correlations — compute yourself. |
GET /api/allocation/opportunity-sets |
Taxonomy: opportunity_set -> asset_class (Equities/Duration/Credit/Currency) + display_order |
GET /api/allocation/prior-views |
List of prior-quarter view records (see §2C) |
GET /api/macro-signals |
List of signal records: opportunity_set, quarter, score, rationale_code, drivers |
Filter style: append ?<field>=<value> matching the record field name
(e.g. ?quarter=Q2_2026, ?candidate=true, ?rating_bucket=HY).
Data-precedence rule (universal)
The environment is authoritative. When a local payload conflicts with the environment,
prefer the environment and set any precedence enum to current_environment_over_stale_payload.
Only choose no_conflict_found when the payload and environment genuinely agree; never
choose local_payload_over_current_environment for live portfolio/mark records. Common
conflicts to check: stale market_value_usd_m, stale HY %, stale holding quantities
(worksheet quantities often lag the reconciled service), stale as-of dates, and stale
local "desk notes" that pre-date the latest index levels.
as_of_date
Use the environment's current as_of_date (top of /api/policies, also on each
portfolio). It is the portfolio book-of-record date. Do not copy the local packet's
older request_date/memo_as_of_date/snapshot_date into the answer's as_of_date.
1. Workflow A — Energy / fixed-income trade strategy
Tasks: build a bond trade package under credit-risk constraints, then report post-trade
metrics, constraint checks, sales positioning, and a data-precedence verdict. Variants:
(1a) a BUY-only income package funded by a new sleeve allocation; (1b) a SELL+BUY
rotation that reduces HY / watchlist pressure while preserving duration.
A1. Inputs to fetch
GET /api/portfolios/<id> — holdings (instrument_id, quantity_usd_m), market_value_usd_m, constraints.policy_id.
GET /api/policies — the policy block matching the portfolio's constraints.policy_id
(e.g. POL_CREDIT_DEFAULT or POL_CREDIT_RISK_REDUCTION).
GET /api/instruments/bonds (+ ?candidate=true) — bond master: rating_bucket
(IG/HY), modified_duration_years, yield_to_maturity_pct, energy_linked,
candidate, sector, subsector, issuer_id, recommended_theme_tags.
GET /api/issuers — watchlist bool per issuer (join on issuer_id).
GET /api/market/energy (energy tasks) — directional signals + pitch themes.
- The local payload (
desk_request.json / risk_meeting_memo.json) — ticket count,
notional, allowed actions, candidate shortlist, stale snapshot, preferences. Reconcile
every quantity/mark to the environment.
A2. Policy constraints (from /api/policies credit blocks)
Both credit_default and credit_risk_reduction carry: duration_band_years:[3.0,5.0],
max_hy_allocation_pct:20.0, issuer_concentration_limit_pct:12.0,
subsector_min_count_for_diversified:2. credit_risk_reduction additionally has
target_hy_reduction_pct:4.0 (minimum HY reduction in percentage points). Read the
exact numbers from the policy file in the test environment — do not hard-code.
A3. Candidate filtering (apply in this order)
candidate == true (the bond is in the opportunity set).
energy_linked == true for energy-credit tasks (the prompt says "energy-linked").
- Issuer
watchlist == false — drop every watchlist issuer. Watchlist issuers are
the ones with watchlist:true in /api/issuers (typically the refiners, shale/E&P,
and telecom names flagged with downgrade/refinancing/margin risk).
- Modified duration within the policy band (e.g. 3.0–5.0). Drop duration-ineligible
distractors (very short <3.0 or long >5.0) — these are traps, not picks.
- Honor the desk's stated exposures/preferences and the
recommended_theme_tags, and
cross-check with /api/market/energy signals: pick bonds whose themes align with
positive commodity signals (e.g. LNG-export pull, gas-demand, renewables rate-relief),
avoid bonds whose commodity signal is negative (e.g. watchlisted refiners).
A4. Diversification checks on the SELECTED package
The *_diversification_pass checks apply to the selected trade package, not the whole
book (a 2-ticket package cannot repair a whole portfolio's concentration):
selected_issuer_diversification_pass: the selected buys are from >=2 distinct issuers.
selected_subsector_diversification_pass: the selected buys span >=2 distinct
subsectors (the policy's subsector_min_count_for_diversified, normally 2).
So do NOT pick two bonds from the same subsector (e.g. two Natural Gas/LNG names) — pair
complementary subsectors (e.g. LNG + Midstream, or LNG + Renewables).
A5. Post-trade metric formulas (market-value-weighted)
Let q_i = holding quantity (USD m), dur_i = modified duration, ytm_i = YTM, and
hy_i = 1 if bond rating_bucket == HY else 0. After applying the trade package (adds for
BUY, removes for SELL), with post-trade total market value MV = Σ q_i:
total_market_value_usd_m = MV (precision 2)
hy_allocation_pct = 100 * Σ(q_i * hy_i) / MV (precision 2)
weighted_modified_duration_years = Σ(q_i * dur_i) / MV (precision 2)
weighted_yield_to_maturity_pct = Σ(q_i * ytm_i) / MV (precision 2)
For a BUY-funded (new allocation) package, post-trade MV = pre-trade MV + sum of BUY
notionals. For a rotation (SELL then reinvest into BUY), post-trade MV = pre-trade MV
(SELL proceeds fund the BUYs). Use the environment's bond master for dur/ytm/rating
(noting duration is "modified duration" in years and YTM is in percent).
A6. Constraint pass/fail booleans
hy_cap_pass: post-trade hy_allocation_pct <= max_hy_allocation_pct.
duration_band_pass: post-trade weighted duration within [3.0, 5.0] (inclusive).
watchlist_avoidance_pass: no selected BUY's issuer is on the watchlist (AND, for
rotation tasks, all watchlist holdings were sold).
- Diversification passes as in A4.
For rotation tasks, the additional booleans are:
target_hy_reduction_met =
(pre-trade HY% − post-trade HY%) >= target_hy_reduction_pct; watchlist_exposure_cleared
= post-trade watchlist exposure == 0.
A7. Sales positioning (BUY-package tasks)
target_segment: pick from the enum to match the client context
(multi_asset_income for a "multi-asset income update"; insurance_general_account/
pension_liability_matching for long-duration IG ballast; endowment_opportunistic
for HY carry; private_bank_income for carry). The prompt's client_context is the
cue.
theme: pick the enum that best matches the headline selection AND the energy pitch
theme: lng_export_tailwind (LNG exporter headline, LNG-export pull signal),
midstream_stability (midstream fee-based), transition_bond_selectivity (renewables
with rate-relief), oil_oversupply_caution, avoid_watchlist_yield_trap (the desk
explicitly flags watchlist sensitivity / stale worksheet highlighted watchlist yield).
A8. Rotation-specific fields (SELL+BUY tasks)
rotation.trades: list with action BUY/SELL, instrument_id, quantity_usd_m
(precision 1). Ordering: SELL before BUY, then instrument_id ascending within each
action (note: not the same ordering as the BUY-only package).
risk_metrics: post_trade_hy_allocation_pct, post_trade_duration_years,
hy_reduction_pct_points (= pre-trade HY% − post-trade HY%, precision 2),
post_trade_watchlist_exposure_usd_m (precision 1).
watchlist_handling.watchlist_sell_ids: instrument ids sold that were watchlisted,
ascending instrument_id. buys_avoid_watchlist: bool (true iff no buy is watchlisted).
risk_note_code: pick the dominant risk theme: watchlist_concentration if clearing a
watchlist name was the driver; hy_cap_pressure if HY cap was the binding constraint;
duration_preservation if the package's main job was keeping duration inside the band;
carry_tradeoff if HY carry was sacrificed for IG quality; no_action only if no trade.
A9. Rotation sizing logic
The rotation must satisfy ALL of: clear watchlist (sell every watchlist holding), get
post-trade HY% <= cap, achieve the >= target HY pp reduction, keep duration in band, and
avoid buying watchlist. Because HY pp reduction and the HY cap are both binding, compute
the minimum HY notional to sell = max(0, pre_HY_mv − cap*MV_target, and ensure the pp
reduction threshold). With discrete holdings, sell whole watchlist + enough HY to get
under the cap, then reinvest the proceeds into duration-band-eligible IG candidates from
the shortlist (excluding any watchlisted shortlist name — check the issuer). Favor buys
that preserve carry (acceptable YTM) and keep the blended duration mid-band.
A10. Common pitfalls (Workflow A)
- Stale worksheet quantities (e.g. a holding shown as 10.0 locally but 12.0 in the
environment) — always use the environment quantity.
- Picking a high-YTM watchlist bond because it "improves carry" — watchlist avoidance
overrides carry.
- Two buys from the same subsector (fails subsector diversification) even if both fit
the desk's LNG preference.
- A duration-ineligible distractor (dur 2.3 or 6.7) that looks attractive on yield.
- Treating a HY cap of 20% as a target rather than a ceiling; or forgetting that the HY
reduction target is in percentage points, not percent of HY.
- Using
notional_usd_m (precision 1) where the task wants quantity_usd_m, or
forgetting the SELL-before-BUY ordering on rotation trades.
2. Workflow B — International equity correlation review
Tasks: given an index universe and a level window, compute pairwise Pearson correlations
of monthly simple returns, identify extreme pairs, flag China/Asia concentration, name
diversification candidates, and propose sleeve actions.
B1. Inputs to fetch
GET /api/index-levels — the dict of monthly levels per index_id.
GET /api/policies -> correlation block: correlation_high_threshold (0.8),
correlation_low_threshold (0.2), review_window_start, review_window_end.
GET /api/portfolios/<id> — the sleeve holdings (which sleeves/indices the book holds).
- The local
review_request.json — review_window (level_start_date/level_end_date),
index_universe, and the CIO memo.concern_codes.
The policy window and the request window normally coincide
(2025-05-30 .. 2026-04-30). Use the request window if it is given; fall back to the
policy window.
B2. Correlation computation
For each index in the universe:
- Take the monthly levels with
date in [level_start_date, level_end_date] inclusive,
sorted ascending. There are normally 12 levels (one per month-end) yielding 11
monthly simple returns r_t = level_t / level_{t-1} − 1.
- Compute Pearson correlation of the return series for each unordered pair.
Use sample statistics (n−1) — but with n identical across series the choice of n vs
n−1 cancels in the Pearson ratio; just be consistent.
cov/(sx*sy).
- Round each reported correlation to 3 decimals.
return_observations (integer) in the review_window object = the number of return
observations (11 for a 12-level window). level_start_date/level_end_date echo the
window bounds.
B3. Extreme pairs
highest_positive: the pair with the maximum correlation.
lowest: the pair with the minimum correlation (often negative).
Each pair_id is a list of exactly 2 index ids sorted ascending alphabetically.
correlation rounded to 3 decimals.
B4. Concentration flags
high_threshold_breached: true if ANY pair correlation >= correlation_high_threshold
(0.8).
china_asia_dependence_flag: true if the China index and/or the Asia-Pacific-ex-Japan
index correlate >= high threshold with the broad complex (EM, World, ACWI, EAFE) — i.e.
the portfolio's risk is concentrated in a China/Asia beta cluster.
primary_code:
CHINA_ASIA_DEPENDENCE when China/Asia high-correlation cluster dominates (concern
codes like CHINA_DEDICATED_SLEEVE, ASIA_BETA_OVERLAP).
GLOBAL_DEVELOPED_OVERLAP when the dominant high-correlation cluster is EAFE/World/
ACWI (developed overlap) without China/Asia being the headline.
NO_MATERIAL_CONCENTRATION only if no pair breaches the high threshold.
- The low-correlation diversifier (concern code
LOW_CORRELATION_DIVERSIFIER) is the
index whose correlations with the rest are all below the low threshold (often strongly
negative); Latam typically plays this role.
B5. Diversification candidates
diversification_candidates: list of index ids that reduce concentration, from the
template's allowed values, sorted ascending alphabetically. Typically the
EM-ex-China, India, and Latam ids (the non-China EM set that breaks the China/Asia
cluster).
B6. Sleeve actions
list[object] length per template, ordered ascending by sleeve name. Each row:
sleeve, action ∈ {trim, add, hold, hedge, monitor, rotate}, target_index_id.
Map from the concentration verdict: trim the concentrated sleeve (e.g. China),
add/rotate into the diversifier(s) (e.g. Latam, EM-ex-China). Typical 2-row set:
trim China (target IDX_CHINA) + add the low-correlation diversifier (target IDX_LATAM).
B7. Pitfalls (Workflow B)
- Using price levels instead of returns for correlation.
- Forgetting to sort pair ids alphabetically.
- Reporting
return_observations as the number of levels (12) instead of returns (11).
- Pre-computing correlations from a stale local worksheet that "did not include the final
month's level" — recompute from
/api/index-levels.
- Mis-reading the high threshold as 0.9 or low as 0.0 — read them from the policy block.
3. Workflow C — Cross-asset active allocation view updates
Tasks: produce per-opportunity-set active views (UW/N/OW) with change vs prior,
conviction, rationale code; plus a portfolio-level risk overlay and lineage. A combined
variant (PF-MA-HELIO) also bundles a small correlation summary + sleeve actions +
rebalance trigger + concentration flag + next step.
C1. Inputs to fetch
GET /api/allocation/opportunity-sets — maps each opportunity_set to an
asset_class (Equities / Duration / Credit / Currency) and display_order.
GET /api/allocation/prior-views — the prior-quarter view records.
GET /api/macro-signals — current signal score + rationale_code per
opportunity_set, per quarter.
GET /api/policies -> allocation_mapping block (view + conviction thresholds), and
the governing multi-asset policy block.
- The local
allocation_request.json / committee_request.json — focus_opportunity_sets
(the rows requested, in the order rows must be emitted), target_quarter,
prior_quarter, policy_id hints.
C2. Selecting the right records
- Macro signals: filter
/api/macro-signals to records whose quarter ==
target_quarter. Use that quarter's score and rationale_code for each requested
opportunity set.
- Prior view: from
/api/allocation/prior-views, pick the records whose
previous_quarter == the task's prior_quarter. (Each prior-views record carries both
quarter (the target it was the prior for) and previous_quarter (the stance's
quarter); match on previous_quarter == prior_quarter. The record's view
(UW/N/OW) is the prior view.) The prior conviction is NOT reused — only the prior
view, to compute change.
- Sanity check: the prior views should NOT already equal what the current-quarter macro
signals imply (if they did, you'd be transcribing not refreshing). If they match, you
likely selected the wrong quarter's signals.
C3. View, conviction, change from the allocation-mapping policy
From /api/policies allocation_mapping:
view_score_thresholds: OW_min (0.35), UW_max (−0.35), neutral_between
[−0.35, 0.35].
conviction_thresholds: HIGH_abs_min (0.7), MEDIUM_abs_min (0.35),
LOW_abs_below (0.35).
view_rank: OW=1, N=0, UW=−1.
Derivation (use the macro signal score s):
view: OW if s >= OW_min; UW if s <= UW_max; else N.
conviction: HIGH if |s| >= HIGH_abs_min; MEDIUM if MEDIUM_abs_min <= |s| < HIGH_abs_min; LOW if |s| < LOW_abs_below.
rationale_code: copy the macro-signal record's rationale_code (must be one of the
template's allowed enum values).
change vs prior view (compare view ranks, not scores):
UP if rank(new) > rank(prior); DOWN if rank(new) < rank(prior);
UNCHANGED if equal.
asset_class: from /api/allocation/opportunity-sets for that opportunity_set
(Equities / Duration / Credit / Currency).
Boundary rule: at exactly ±0.35 the view flips to OW/UW and conviction is MEDIUM (not
LOW). Read the exact thresholds from the policy file in the test environment.
C4. Row ordering
- Allocation-views list: sort rows in the request payload's
focus_opportunity_sets
order (the order the CIO listed them), not alphabetical and not display_order.
- Output exactly the requested number of rows (e.g. 8). One row per requested
opportunity set.
- For the combined committee variant:
target_sleeve_actions and allocation_views are
emitted in the template's stated item_order (e.g. Emerging Markets, India, Latin
America, USD).
C5. Risk overlay
risk_overlay (allocation-only tasks): overlay_code, primary_action,
rationale_codes[].
- Derive
overlay_code from the cluster of view directions + rationale codes:
CREDIT_RISK_REDUCTION / trim_credit_beta when Corporate HY is UW on
HY_VALUATION_RISK (credit beta to cut).
DURATION_QUALITY_TILT / tilt_to_duration_quality when U.S. Treasuries (or core
duration) is OW on DURATION_SUPPORT / RATE_CUT_SUPPORT (rotate into duration
quality).
CURRENCY_DEFENSIVE_HEDGE / add_currency_hedge when a safe-haven currency is OW
/ risk currency UW on DOLLAR_DEFENSIVE.
EQUITY_BETA_EXTENSION / add_cyclical_equity_beta when cyclical equities are OW on
GROWTH_IMPROVES/EUROPE_RECOVERY.
NO_OVERLAY / hold_policy_weights only if views are broadly neutral.
rationale_codes[]: the rationale codes driving the overlay, in business-priority
order, highest priority first (strongest signal / most actionable risk first; e.g.
lead with the binding risk like HY_VALUATION_RISK, then DURATION_SUPPORT,
RATE_CUT_SUPPORT). Each must be from the allowed enum; do not duplicate.
C6. Combined committee variant fields (PF-MA-HELIO style)
policy_id: the governing multi-asset policy id (e.g. POL_MULTI_ASSET_DEFAULT for a
balanced multi-asset model; POL_MULTI_ASSET_RISK if the prompt stresses risk-overlay/
escalation). The portfolio's constraints.policy_id is the source of truth.
correlation_summary: length-2 list [highest_concentration, best_diversifier]. Each
item: pair_role, pair (2 index ids sorted alphabetically), correlation
(Pearson of monthly simple returns over the level window, 3 decimals) — compute
exactly as in Workflow B but for the request's correlation_review.index_ids subset.
target_sleeve_actions: one row per item in the template's item_order, each with
opportunity_set + action. Map from the new views + correlation verdict:
UW/trim the concentrated sleeve, OW/add the diversifier, unchanged-OW/hold the
strong offset, downgrade/trim or hedge the stale-defensive currency.
allocation_views: one row per item in item_order, each with opportunity_set,
prior_view (from prior-views), signal_score (the macro signal score, 3
decimals), view, change, conviction, rationale_code — derived as in C3.
rebalance_trigger: correlation_cap_breach if any reviewed pair >= the correlation
high threshold; else hy_cap_pressure / duration_drift / watchlist_concentration
if those pressures are present in the book; else committee_review.
portfolio_risk_concentration_flag: true if a correlation cap is breached OR a China/
Asia dependence concentration is present.
next_step: pick from approve_rotation, approve_with_monitoring,
defer_pending_risk_review, reject_constraint_breach. The
multi_asset_risk.committee_escalation_threshold ("two_or_more_material_exceptions")
governs: with two or more material exceptions (e.g. correlation breach + a constraint
breach) -> defer_pending_risk_review; a single contained exception with a clear
remediation -> approve_with_monitoring; a clean rotation plan -> approve_rotation;
an unfixable breach -> reject_constraint_breach.
C7. Lineage fields
as_of_date: environment as_of_date.
target_quarter / prior_quarter: from the request (required values in the template).
policy_id: the governing policy id (see C6).
task_id: the template's required value (e.g. train_003) — echo it verbatim.
C8. Pitfalls (Workflow C)
- Using the wrong quarter's macro signals (Q3 signals for a Q2 target). Match
quarter
== target_quarter exactly.
- Matching prior-views on
quarter instead of previous_quarter — that gives the
already-decided view and makes change meaningless.
- Computing
change from score deltas instead of view-rank deltas (a score that moves
within the same band is UNCHANGED).
- Emitting rows alphabetically instead of in the request's
focus_opportunity_sets
order.
- Leaving
signal_score unrounded (3 decimals) or rounding it to 2.
- Forgetting
prior_view in the combined-variant rows, or reusing the prior conviction
as the new conviction (conviction comes from the current signal score).
- Trusting a stale local "desk note" that keeps an old overweight (e.g. USD OW) — refresh
from current signals; the downgrade is the point of the refresh.
4. Cross-cutting output conventions
- Precision: obey each field's
precision in answer_template.json. Common values:
notional_usd_m / quantity_usd_m = 1 decimal; post_trade_metrics /
risk_metrics percentages/durations = 2 decimals; correlations = 3 decimals;
signal_score = 3 decimals. Round (not truncate) at the end.
- Ordering:
trade_package (BUY-only) sorted ascending by instrument_id;
rotation.trades sorted SELL before BUY, then instrument_id ascending within each
action; pair ids and index lists ascending alphabetical; sleeve_actions ascending
by sleeve; allocation rows in request payload order; rationale_codes in
business-priority order.
- Enums: every enum field MUST be exactly one of the template's
allowed_values
(case-sensitive). Do not invent values.
- Required values: fields with
required_value (portfolio_id, task_id,
target_quarter, review_quarter, policy_id-where-fixed) must echo exactly.
- Booleans: pass/fail and flag fields are real JSON booleans, not strings.
- JSON only: emit a single JSON object. No prose, no markdown fences, no trailing
commas, no comments.
5. Universal execution checklist
- Read
prompt.txt, answer_template.json, and the local payload fully; note the
required top-level keys, required values, enums, precision, and ordering rules.
- Identify the workflow (A trade strategy / B correlation review / C allocation views,
possibly the combined C+B committee variant).
- Fetch
/api/policies first (gives as_of_date + all thresholds), then the portfolio,
then the workflow-specific endpoints. Cache responses to reuse across calculations.
- Reconcile every local quantity/mark/window to the environment; record the precedence
verdict where required.
- Compute with the environment's numbers; round only when emitting.
- Validate: every required key present, every enum legal, every ordering satisfied,
every boolean reflecting the actual computed constraint, precision correct.
- Emit only the JSON object.
1---2name: self-attempt-01-143description: Asteria Investment Office — Institutional Portfolio-Risk Solver Skill4---5# Asteria Investment Office — Institutional Portfolio-Risk Solver Skill67This skill helps solve portfolio-risk JSON tasks against the shared Asteria Investment8Office environment. The environment is the **current book of record**; local9`input/payloads/` files are intake context that may be stale. Always reconcile to the10environment before answering. Return ONLY a JSON object matching the task's11`answer_template.json` (no narrative outside the JSON).1213There are three recurring workflows. Identify yours from the prompt/portfolio id, then14follow the matching SOP. Cross-cutting conventions (precision, ordering, enums,15precedence) apply to all of them.1617---1819## 0. Environment access2021Base URL: `<remote-env-url>` (GET only). Call with `curl` or `python3 urllib`.2223Key endpoints and what they return:2425| Endpoint | Use |26|---|---|27| `GET /api/catalog` | All ids: portfolios, policies, indices, issuers, bonds, opportunity_sets |28| `GET /api/policies` | ALL policy blocks in one object: `credit_default`, `credit_risk_reduction`, `correlation`, `allocation_mapping`, `multi_asset`, `multi_asset_risk`. Also top-level `as_of_date`. |29| `GET /api/portfolios` | Portfolio summaries |30| `GET /api/portfolios/<portfolio_id>` | Objective, constraints (policy_id), `holdings[]` with `quantity_usd_m`/`instrument_id`/`sleeve`, `market_value_usd_m`, `as_of_date` |31| `GET /api/instruments/bonds` | Full bond universe. Filter `?candidate=true`, `?rating_bucket=HY` |32| `GET /api/issuers` | `sector`, `subsector`, `rating_bucket`, `watchlist` (bool), `credit_outlook`, `research_tags` |33| `GET /api/market/energy` | Energy commodity `signals[]` (score, direction, signal_id) and `pitch_themes` |34| `GET /api/indices` | Index metadata + `level_start_date`/`level_end_date` |35| `GET /api/index-levels` | Dict: `index_id -> [ {date, level}, ... ]` monthly. **No precomputed correlations** — compute yourself. |36| `GET /api/allocation/opportunity-sets` | Taxonomy: `opportunity_set` -> `asset_class` (Equities/Duration/Credit/Currency) + `display_order` |37| `GET /api/allocation/prior-views` | List of prior-quarter view records (see §2C) |38| `GET /api/macro-signals` | List of signal records: `opportunity_set`, `quarter`, `score`, `rationale_code`, `drivers` |3940Filter style: append `?<field>=<value>` matching the record field name41(e.g. `?quarter=Q2_2026`, `?candidate=true`, `?rating_bucket=HY`).4243### Data-precedence rule (universal)4445The environment is authoritative. When a local payload conflicts with the environment,46prefer the environment and set any precedence enum to `current_environment_over_stale_payload`.47Only choose `no_conflict_found` when the payload and environment genuinely agree; never48choose `local_payload_over_current_environment` for live portfolio/mark records. Common49conflicts to check: stale `market_value_usd_m`, stale HY %, stale holding quantities50(worksheet quantities often lag the reconciled service), stale as-of dates, and stale51local "desk notes" that pre-date the latest index levels.5253### as_of_date5455Use the environment's current `as_of_date` (top of `/api/policies`, also on each56portfolio). It is the portfolio book-of-record date. Do not copy the local packet's57older `request_date`/`memo_as_of_date`/`snapshot_date` into the answer's `as_of_date`.5859---6061## 1. Workflow A — Energy / fixed-income trade strategy6263Tasks: build a bond trade package under credit-risk constraints, then report post-trade64metrics, constraint checks, sales positioning, and a data-precedence verdict. Variants:65(1a) a BUY-only income package funded by a new sleeve allocation; (1b) a SELL+BUY66rotation that reduces HY / watchlist pressure while preserving duration.6768### A1. Inputs to fetch6970- `GET /api/portfolios/<id>` — holdings (instrument_id, quantity_usd_m), market_value_usd_m, constraints.policy_id.71- `GET /api/policies` — the policy block matching the portfolio's `constraints.policy_id`72 (e.g. `POL_CREDIT_DEFAULT` or `POL_CREDIT_RISK_REDUCTION`).73- `GET /api/instruments/bonds` (+ `?candidate=true`) — bond master: `rating_bucket`74 (IG/HY), `modified_duration_years`, `yield_to_maturity_pct`, `energy_linked`,75 `candidate`, `sector`, `subsector`, `issuer_id`, `recommended_theme_tags`.76- `GET /api/issuers` — `watchlist` bool per issuer (join on `issuer_id`).77- `GET /api/market/energy` (energy tasks) — directional signals + pitch themes.78- The local payload (`desk_request.json` / `risk_meeting_memo.json`) — ticket count,79 notional, allowed actions, candidate shortlist, stale snapshot, preferences. Reconcile80 every quantity/mark to the environment.8182### A2. Policy constraints (from `/api/policies` credit blocks)8384Both `credit_default` and `credit_risk_reduction` carry: `duration_band_years:[3.0,5.0]`,85`max_hy_allocation_pct:20.0`, `issuer_concentration_limit_pct:12.0`,86`subsector_min_count_for_diversified:2`. `credit_risk_reduction` additionally has87`target_hy_reduction_pct:4.0` (minimum HY reduction in percentage points). Read the88exact numbers from the policy file in the test environment — do not hard-code.8990### A3. Candidate filtering (apply in this order)91921. `candidate == true` (the bond is in the opportunity set).932. `energy_linked == true` for energy-credit tasks (the prompt says "energy-linked").943. Issuer `watchlist == false` — **drop every watchlist issuer**. Watchlist issuers are95 the ones with `watchlist:true` in `/api/issuers` (typically the refiners, shale/E&P,96 and telecom names flagged with downgrade/refinancing/margin risk).974. Modified duration within the policy band (e.g. 3.0–5.0). Drop duration-ineligible98 distractors (very short <3.0 or long >5.0) — these are traps, not picks.995. Honor the desk's stated exposures/preferences and the `recommended_theme_tags`, and100 cross-check with `/api/market/energy` signals: pick bonds whose themes align with101 positive commodity signals (e.g. LNG-export pull, gas-demand, renewables rate-relief),102 avoid bonds whose commodity signal is negative (e.g. watchlisted refiners).103104### A4. Diversification checks on the SELECTED package105106The `*_diversification_pass` checks apply to the **selected trade package**, not the whole107book (a 2-ticket package cannot repair a whole portfolio's concentration):108- `selected_issuer_diversification_pass`: the selected buys are from >=2 distinct issuers.109- `selected_subsector_diversification_pass`: the selected buys span >=2 distinct110 subsectors (the policy's `subsector_min_count_for_diversified`, normally 2).111So do NOT pick two bonds from the same subsector (e.g. two Natural Gas/LNG names) — pair112complementary subsectors (e.g. LNG + Midstream, or LNG + Renewables).113114### A5. Post-trade metric formulas (market-value-weighted)115116Let `q_i` = holding quantity (USD m), `dur_i` = modified duration, `ytm_i` = YTM, and117`hy_i` = 1 if bond rating_bucket == HY else 0. After applying the trade package (adds for118BUY, removes for SELL), with post-trade total market value `MV = Σ q_i`:119120- `total_market_value_usd_m = MV` (precision 2)121- `hy_allocation_pct = 100 * Σ(q_i * hy_i) / MV` (precision 2)122- `weighted_modified_duration_years = Σ(q_i * dur_i) / MV` (precision 2)123- `weighted_yield_to_maturity_pct = Σ(q_i * ytm_i) / MV` (precision 2)124125For a BUY-funded (new allocation) package, post-trade MV = pre-trade MV + sum of BUY126notionals. For a rotation (SELL then reinvest into BUY), post-trade MV = pre-trade MV127(SELL proceeds fund the BUYs). Use the environment's bond master for `dur`/`ytm`/`rating`128(noting duration is "modified duration" in years and YTM is in percent).129130### A6. Constraint pass/fail booleans131132- `hy_cap_pass`: post-trade `hy_allocation_pct <= max_hy_allocation_pct`.133- `duration_band_pass`: post-trade weighted duration within `[3.0, 5.0]` (inclusive).134- `watchlist_avoidance_pass`: no selected BUY's issuer is on the watchlist (AND, for135 rotation tasks, all watchlist holdings were sold).136- Diversification passes as in A4.137For rotation tasks, the additional booleans are: `target_hy_reduction_met` =138(pre-trade HY% − post-trade HY%) >= `target_hy_reduction_pct`; `watchlist_exposure_cleared`139= post-trade watchlist exposure == 0.140141### A7. Sales positioning (BUY-package tasks)142143- `target_segment`: pick from the enum to match the client context144 (`multi_asset_income` for a "multi-asset income update"; `insurance_general_account`/145 `pension_liability_matching` for long-duration IG ballast; `endowment_opportunistic`146 for HY carry; `private_bank_income` for carry). The prompt's `client_context` is the147 cue.148- `theme`: pick the enum that best matches the headline selection AND the energy pitch149 theme: `lng_export_tailwind` (LNG exporter headline, LNG-export pull signal),150 `midstream_stability` (midstream fee-based), `transition_bond_selectivity` (renewables151 with rate-relief), `oil_oversupply_caution`, `avoid_watchlist_yield_trap` (the desk152 explicitly flags watchlist sensitivity / stale worksheet highlighted watchlist yield).153154### A8. Rotation-specific fields (SELL+BUY tasks)155156- `rotation.trades`: list with `action` BUY/SELL, `instrument_id`, `quantity_usd_m`157 (precision 1). **Ordering: SELL before BUY, then instrument_id ascending within each158 action** (note: not the same ordering as the BUY-only package).159- `risk_metrics`: `post_trade_hy_allocation_pct`, `post_trade_duration_years`,160 `hy_reduction_pct_points` (= pre-trade HY% − post-trade HY%, precision 2),161 `post_trade_watchlist_exposure_usd_m` (precision 1).162- `watchlist_handling.watchlist_sell_ids`: instrument ids sold that were watchlisted,163 ascending instrument_id. `buys_avoid_watchlist`: bool (true iff no buy is watchlisted).164- `risk_note_code`: pick the dominant risk theme: `watchlist_concentration` if clearing a165 watchlist name was the driver; `hy_cap_pressure` if HY cap was the binding constraint;166 `duration_preservation` if the package's main job was keeping duration inside the band;167 `carry_tradeoff` if HY carry was sacrificed for IG quality; `no_action` only if no trade.168169### A9. Rotation sizing logic170171The rotation must satisfy ALL of: clear watchlist (sell every watchlist holding), get172post-trade HY% <= cap, achieve the >= target HY pp reduction, keep duration in band, and173avoid buying watchlist. Because HY pp reduction and the HY cap are both binding, compute174the minimum HY notional to sell = max(0, pre_HY_mv − cap*MV_target, and ensure the pp175reduction threshold). With discrete holdings, sell whole watchlist + enough HY to get176under the cap, then reinvest the proceeds into duration-band-eligible IG candidates from177the shortlist (excluding any watchlisted shortlist name — check the issuer). Favor buys178that preserve carry (acceptable YTM) and keep the blended duration mid-band.179180### A10. Common pitfalls (Workflow A)181182- Stale worksheet quantities (e.g. a holding shown as 10.0 locally but 12.0 in the183 environment) — always use the environment quantity.184- Picking a high-YTM watchlist bond because it "improves carry" — watchlist avoidance185 overrides carry.186- Two buys from the same subsector (fails subsector diversification) even if both fit187 the desk's LNG preference.188- A duration-ineligible distractor (dur 2.3 or 6.7) that looks attractive on yield.189- Treating a HY cap of 20% as a target rather than a ceiling; or forgetting that the HY190 reduction target is in *percentage points*, not percent of HY.191- Using `notional_usd_m` (precision 1) where the task wants `quantity_usd_m`, or192 forgetting the SELL-before-BUY ordering on rotation trades.193194---195196## 2. Workflow B — International equity correlation review197198Tasks: given an index universe and a level window, compute pairwise Pearson correlations199of monthly simple returns, identify extreme pairs, flag China/Asia concentration, name200diversification candidates, and propose sleeve actions.201202### B1. Inputs to fetch203204- `GET /api/index-levels` — the dict of monthly levels per index_id.205- `GET /api/policies` -> `correlation` block: `correlation_high_threshold` (0.8),206 `correlation_low_threshold` (0.2), `review_window_start`, `review_window_end`.207- `GET /api/portfolios/<id>` — the sleeve holdings (which sleeves/indices the book holds).208- The local `review_request.json` — `review_window` (level_start_date/level_end_date),209 `index_universe`, and the CIO `memo.concern_codes`.210211The policy window and the request window normally coincide212(`2025-05-30` .. `2026-04-30`). Use the request window if it is given; fall back to the213policy window.214215### B2. Correlation computation216217For each index in the universe:2181. Take the monthly levels with `date` in `[level_start_date, level_end_date]` inclusive,219 sorted ascending. There are normally **12 levels** (one per month-end) yielding **11220 monthly simple returns** `r_t = level_t / level_{t-1} − 1`.2212. Compute **Pearson correlation** of the return series for each unordered pair.222 Use sample statistics (n−1) — but with n identical across series the choice of n vs223 n−1 cancels in the Pearson ratio; just be consistent. `cov/(sx*sy)`.2243. Round each reported correlation to **3 decimals**.225226`return_observations` (integer) in the `review_window` object = the number of return227observations (11 for a 12-level window). `level_start_date`/`level_end_date` echo the228window bounds.229230### B3. Extreme pairs231232- `highest_positive`: the pair with the maximum correlation.233- `lowest`: the pair with the minimum correlation (often negative).234Each `pair_id` is a **list of exactly 2 index ids sorted ascending alphabetically**.235`correlation` rounded to 3 decimals.236237### B4. Concentration flags238239- `high_threshold_breached`: true if ANY pair correlation >= `correlation_high_threshold`240 (0.8).241- `china_asia_dependence_flag`: true if the China index and/or the Asia-Pacific-ex-Japan242 index correlate >= high threshold with the broad complex (EM, World, ACWI, EAFE) — i.e.243 the portfolio's risk is concentrated in a China/Asia beta cluster.244- `primary_code`:245 - `CHINA_ASIA_DEPENDENCE` when China/Asia high-correlation cluster dominates (concern246 codes like `CHINA_DEDICATED_SLEEVE`, `ASIA_BETA_OVERLAP`).247 - `GLOBAL_DEVELOPED_OVERLAP` when the dominant high-correlation cluster is EAFE/World/248 ACWI (developed overlap) without China/Asia being the headline.249 - `NO_MATERIAL_CONCENTRATION` only if no pair breaches the high threshold.250- The low-correlation diversifier (concern code `LOW_CORRELATION_DIVERSIFIER`) is the251 index whose correlations with the rest are all below the low threshold (often strongly252 negative); Latam typically plays this role.253254### B5. Diversification candidates255256`diversification_candidates`: list of index ids that reduce concentration, from the257template's allowed values, **sorted ascending alphabetically**. Typically the258EM-ex-China, India, and Latam ids (the non-China EM set that breaks the China/Asia259cluster).260261### B6. Sleeve actions262263`list[object]` length per template, **ordered ascending by `sleeve` name**. Each row:264`sleeve`, `action` ∈ {trim, add, hold, hedge, monitor, rotate}, `target_index_id`.265Map from the concentration verdict: trim the concentrated sleeve (e.g. China),266add/rotate into the diversifier(s) (e.g. Latam, EM-ex-China). Typical 2-row set:267trim China (target IDX_CHINA) + add the low-correlation diversifier (target IDX_LATAM).268269### B7. Pitfalls (Workflow B)270271- Using price *levels* instead of *returns* for correlation.272- Forgetting to sort pair ids alphabetically.273- Reporting `return_observations` as the number of levels (12) instead of returns (11).274- Pre-computing correlations from a stale local worksheet that "did not include the final275 month's level" — recompute from `/api/index-levels`.276- Mis-reading the high threshold as 0.9 or low as 0.0 — read them from the policy block.277278---279280## 3. Workflow C — Cross-asset active allocation view updates281282Tasks: produce per-opportunity-set active views (UW/N/OW) with change vs prior,283conviction, rationale code; plus a portfolio-level risk overlay and lineage. A combined284variant (PF-MA-HELIO) also bundles a small correlation summary + sleeve actions +285rebalance trigger + concentration flag + next step.286287### C1. Inputs to fetch288289- `GET /api/allocation/opportunity-sets` — maps each `opportunity_set` to an290 `asset_class` (Equities / Duration / Credit / Currency) and `display_order`.291- `GET /api/allocation/prior-views` — the prior-quarter view records.292- `GET /api/macro-signals` — current signal `score` + `rationale_code` per293 opportunity_set, per `quarter`.294- `GET /api/policies` -> `allocation_mapping` block (view + conviction thresholds), and295 the governing multi-asset policy block.296- The local `allocation_request.json` / `committee_request.json` — `focus_opportunity_sets`297 (the rows requested, **in the order rows must be emitted**), `target_quarter`,298 `prior_quarter`, `policy_id` hints.299300### C2. Selecting the right records301302- **Macro signals**: filter `/api/macro-signals` to records whose `quarter` ==303 `target_quarter`. Use that quarter's `score` and `rationale_code` for each requested304 opportunity set.305- **Prior view**: from `/api/allocation/prior-views`, pick the records whose306 `previous_quarter` == the task's `prior_quarter`. (Each prior-views record carries both307 `quarter` (the target it was the prior for) and `previous_quarter` (the stance's308 quarter); match on `previous_quarter` == `prior_quarter`. The record's `view`309 (UW/N/OW) is the prior view.) The prior `conviction` is NOT reused — only the prior310 view, to compute change.311- Sanity check: the prior views should NOT already equal what the current-quarter macro312 signals imply (if they did, you'd be transcribing not refreshing). If they match, you313 likely selected the wrong quarter's signals.314315### C3. View, conviction, change from the allocation-mapping policy316317From `/api/policies` `allocation_mapping`:318- `view_score_thresholds`: `OW_min` (0.35), `UW_max` (−0.35), `neutral_between`319 [−0.35, 0.35].320- `conviction_thresholds`: `HIGH_abs_min` (0.7), `MEDIUM_abs_min` (0.35),321 `LOW_abs_below` (0.35).322- `view_rank`: OW=1, N=0, UW=−1.323324Derivation (use the macro signal `score` `s`):325- `view`: `OW` if `s >= OW_min`; `UW` if `s <= UW_max`; else `N`.326- `conviction`: `HIGH` if `|s| >= HIGH_abs_min`; `MEDIUM` if `MEDIUM_abs_min <= |s| <327 HIGH_abs_min`; `LOW` if `|s| < LOW_abs_below`.328- `rationale_code`: copy the macro-signal record's `rationale_code` (must be one of the329 template's allowed enum values).330- `change` vs prior view (compare **view ranks**, not scores):331 `UP` if `rank(new) > rank(prior)`; `DOWN` if `rank(new) < rank(prior)`;332 `UNCHANGED` if equal.333- `asset_class`: from `/api/allocation/opportunity-sets` for that opportunity_set334 (Equities / Duration / Credit / Currency).335336Boundary rule: at exactly ±0.35 the view flips to OW/UW and conviction is MEDIUM (not337LOW). Read the exact thresholds from the policy file in the test environment.338339### C4. Row ordering340341- Allocation-views list: **sort rows in the request payload's `focus_opportunity_sets`342 order** (the order the CIO listed them), not alphabetical and not display_order.343- Output exactly the requested number of rows (e.g. 8). One row per requested344 opportunity set.345- For the combined committee variant: `target_sleeve_actions` and `allocation_views` are346 emitted in the template's stated `item_order` (e.g. Emerging Markets, India, Latin347 America, USD).348349### C5. Risk overlay350351`risk_overlay` (allocation-only tasks): `overlay_code`, `primary_action`,352`rationale_codes[]`.353- Derive `overlay_code` from the cluster of view directions + rationale codes:354 - `CREDIT_RISK_REDUCTION` / `trim_credit_beta` when Corporate HY is UW on355 `HY_VALUATION_RISK` (credit beta to cut).356 - `DURATION_QUALITY_TILT` / `tilt_to_duration_quality` when U.S. Treasuries (or core357 duration) is OW on `DURATION_SUPPORT` / `RATE_CUT_SUPPORT` (rotate into duration358 quality).359 - `CURRENCY_DEFENSIVE_HEDGE` / `add_currency_hedge` when a safe-haven currency is OW360 / risk currency UW on `DOLLAR_DEFENSIVE`.361 - `EQUITY_BETA_EXTENSION` / `add_cyclical_equity_beta` when cyclical equities are OW on362 `GROWTH_IMPROVES`/`EUROPE_RECOVERY`.363 - `NO_OVERLAY` / `hold_policy_weights` only if views are broadly neutral.364- `rationale_codes[]`: the rationale codes driving the overlay, in **business-priority365 order, highest priority first** (strongest signal / most actionable risk first; e.g.366 lead with the binding risk like `HY_VALUATION_RISK`, then `DURATION_SUPPORT`,367 `RATE_CUT_SUPPORT`). Each must be from the allowed enum; do not duplicate.368369### C6. Combined committee variant fields (PF-MA-HELIO style)370371- `policy_id`: the governing multi-asset policy id (e.g. `POL_MULTI_ASSET_DEFAULT` for a372 balanced multi-asset model; `POL_MULTI_ASSET_RISK` if the prompt stresses risk-overlay/373 escalation). The portfolio's `constraints.policy_id` is the source of truth.374- `correlation_summary`: length-2 list `[highest_concentration, best_diversifier]`. Each375 item: `pair_role`, `pair` (2 index ids **sorted alphabetically**), `correlation`376 (Pearson of monthly simple returns over the level window, **3 decimals**) — compute377 exactly as in Workflow B but for the request's `correlation_review.index_ids` subset.378- `target_sleeve_actions`: one row per item in the template's `item_order`, each with379 `opportunity_set` + `action`. Map from the new views + correlation verdict:380 UW/trim the concentrated sleeve, OW/add the diversifier, unchanged-OW/hold the381 strong offset, downgrade/trim or hedge the stale-defensive currency.382- `allocation_views`: one row per item in `item_order`, each with `opportunity_set`,383 `prior_view` (from prior-views), `signal_score` (the macro signal score, **3384 decimals**), `view`, `change`, `conviction`, `rationale_code` — derived as in C3.385- `rebalance_trigger`: `correlation_cap_breach` if any reviewed pair >= the correlation386 high threshold; else `hy_cap_pressure` / `duration_drift` / `watchlist_concentration`387 if those pressures are present in the book; else `committee_review`.388- `portfolio_risk_concentration_flag`: true if a correlation cap is breached OR a China/389 Asia dependence concentration is present.390- `next_step`: pick from `approve_rotation`, `approve_with_monitoring`,391 `defer_pending_risk_review`, `reject_constraint_breach`. The392 `multi_asset_risk.committee_escalation_threshold` ("two_or_more_material_exceptions")393 governs: with two or more material exceptions (e.g. correlation breach + a constraint394 breach) -> `defer_pending_risk_review`; a single contained exception with a clear395 remediation -> `approve_with_monitoring`; a clean rotation plan -> `approve_rotation`;396 an unfixable breach -> `reject_constraint_breach`.397398### C7. Lineage fields399400- `as_of_date`: environment `as_of_date`.401- `target_quarter` / `prior_quarter`: from the request (required values in the template).402- `policy_id`: the governing policy id (see C6).403- `task_id`: the template's required value (e.g. `train_003`) — echo it verbatim.404405### C8. Pitfalls (Workflow C)406407- Using the wrong quarter's macro signals (Q3 signals for a Q2 target). Match `quarter`408 == `target_quarter` exactly.409- Matching prior-views on `quarter` instead of `previous_quarter` — that gives the410 already-decided view and makes `change` meaningless.411- Computing `change` from score deltas instead of view-rank deltas (a score that moves412 within the same band is UNCHANGED).413- Emitting rows alphabetically instead of in the request's `focus_opportunity_sets`414 order.415- Leaving `signal_score` unrounded (3 decimals) or rounding it to 2.416- Forgetting `prior_view` in the combined-variant rows, or reusing the prior conviction417 as the new conviction (conviction comes from the current signal score).418- Trusting a stale local "desk note" that keeps an old overweight (e.g. USD OW) — refresh419 from current signals; the downgrade is the point of the refresh.420421---422423## 4. Cross-cutting output conventions424425- **Precision**: obey each field's `precision` in `answer_template.json`. Common values:426 `notional_usd_m` / `quantity_usd_m` = 1 decimal; `post_trade_metrics` /427 `risk_metrics` percentages/durations = 2 decimals; correlations = 3 decimals;428 `signal_score` = 3 decimals. Round (not truncate) at the end.429- **Ordering**: `trade_package` (BUY-only) sorted **ascending by instrument_id**;430 `rotation.trades` sorted **SELL before BUY, then instrument_id ascending** within each431 action; pair ids and index lists **ascending alphabetical**; sleeve_actions **ascending432 by sleeve**; allocation rows in **request payload order**; rationale_codes in433 **business-priority order**.434- **Enums**: every enum field MUST be exactly one of the template's `allowed_values`435 (case-sensitive). Do not invent values.436- **Required values**: fields with `required_value` (portfolio_id, task_id,437 target_quarter, review_quarter, policy_id-where-fixed) must echo exactly.438- **Booleans**: pass/fail and flag fields are real JSON booleans, not strings.439- **JSON only**: emit a single JSON object. No prose, no markdown fences, no trailing440 commas, no comments.441442---443444## 5. Universal execution checklist4454461. Read `prompt.txt`, `answer_template.json`, and the local payload fully; note the447 required top-level keys, required values, enums, precision, and ordering rules.4482. Identify the workflow (A trade strategy / B correlation review / C allocation views,449 possibly the combined C+B committee variant).4503. Fetch `/api/policies` first (gives as_of_date + all thresholds), then the portfolio,451 then the workflow-specific endpoints. Cache responses to reuse across calculations.4524. Reconcile every local quantity/mark/window to the environment; record the precedence453 verdict where required.4545. Compute with the environment's numbers; round only when emitting.4556. Validate: every required key present, every enum legal, every ordering satisfied,456 every boolean reflecting the actual computed constraint, precision correct.4577. Emit only the JSON object.