Asteria Investment Office — Portfolio-Risk Task Skill
Operating SOP for solving Asteria portfolio-risk evaluation tasks. The test
solver receives only a task input/ (prompt + payloads/answer_template.json +
a request payload) plus this skill and the remote environment. Three workflow
families appear; a task may involve one or combine two.
Base URL: <remote-env-url> (GET only). Use curl or python3+urllib.
0. Golden rules (apply to every task)
- Environment = book of record. Every
input/payloads/*.json is intake
context and may be stale (old marks, old quantities, old desk notes).
Always reconcile to the environment. When a local payload conflicts with the
environment, prefer the environment. The data_precedence answer field is
current_environment_over_stale_payload whenever any conflict exists (e.g.
stale snapshot MV/quantity differs from /api/portfolios/<id>).
as_of_date in your answer = the environment's current as-of date. Read
it from /api/portfolios/<id> (as_of_date) or /api/policies
(as_of_date). Do not copy a date stamped on a local request payload or
a stale worksheet unless the prompt explicitly overrides.
- No precomputed correlations. The environment stores index levels, never
correlations. Compute Pearson correlations yourself from monthly simple
returns (see Workflow B).
- Precision follows each field in
answer_template.json. Round the final
value to the declared decimals. JSON numerics like 5.8 and 5.80 are equal,
but always round to the specified precision (2/3/1 decimals) before emitting.
- Read the answer template first. It declares required keys, enums,
ordering, and precision per field. Conform exactly: missing keys, wrong
enum values, wrong sort order, or wrong precision all fail.
- Output is a single JSON object matching the template — no narrative
outside JSON unless the prompt asks for it.
1. Environment endpoints (what each is for)
| Endpoint |
Use |
GET /api/policies |
All constraint/threshold/mapping policies + as_of_date. |
GET /api/portfolios |
List of portfolio summaries. |
GET /api/portfolios/<id> |
Objective, constraints, current holdings (instrument_id, quantity_usd_m, sleeve), MV, as_of. |
GET /api/instruments/bonds |
Bond universe. Filters: ?candidate=true, ?rating_bucket=HY. Fields: instrument_id, issuer_id, rating_bucket(IG/HY), rating, modified_duration_years, yield_to_maturity_pct, coupon_pct, spread_bps, sector, subsector, energy_linked, candidate, recommended_theme_tags. |
GET /api/issuers |
issuer_id, sector, subsector, rating_bucket, watchlist(bool), credit_outlook, research_tags. |
GET /api/market/energy |
Oil/gas/LNG/refining/renewables signal scores + pitch_themes. |
GET /api/indices |
Index metadata (region, level window). |
GET /api/index-levels |
All monthly index levels: dict index_id -> [{date, level}]. |
GET /api/index-levels/<index_id> |
One index's monthly levels. |
GET /api/allocation/opportunity-sets |
Cross-asset taxonomy: opportunity_set -> asset_class (Equities/Duration/Credit/Currency). |
GET /api/allocation/prior-views |
Prior-quarter active views (see Workflow C). |
GET /api/macro-signals |
Current signal scores + rationale_code per opportunity_set/quarter. |
GET /api/catalog may return empty; ignore it and use the typed endpoints above.
Reuse fetches. /api/instruments/bonds, /api/issuers, /api/index-levels,
/api/allocation/prior-views, /api/macro-signals are small and shared across
tasks — fetch once, cache to /tmp/*.json, reuse.
2. Policy reference (/api/policies)
credit_default (POL_CREDIT_DEFAULT): max_hy_allocation_pct=20.0,
duration_band_years=[3.0, 5.0], issuer_concentration_limit_pct=12.0,
subsector_min_count_for_diversified=2, target_hy_reduction_pct=0.0.
credit_risk_reduction (POL_CREDIT_RISK_REDUCTION): same as above but
target_hy_reduction_pct=4.0 (used by risk-reduction rotations).
correlation (POL_CORRELATION_DEFAULT):
correlation_high_threshold=0.8, correlation_low_threshold=0.2,
review window (e.g. 2025-05-30 → 2026-04-30).
allocation_mapping (POL_ALLOCATION_MAPPING):
view_score_thresholds: OW if score ≥ 0.35; UW if score ≤ -0.35; N in (-0.35, 0.35).
conviction_thresholds: HIGH if |score| ≥ 0.70; MEDIUM if |score| ≥ 0.35; LOW if |score| < 0.35.
view_rank: OW=1, N=0, UW=-1 (for computing change vs prior).
multi_asset (POL_MULTI_ASSET_DEFAULT): uses allocation_mapping +
correlation_default + credit_default. Used by multi-asset sleeves.
multi_asset_risk (POL_MULTI_ASSET_RISK): uses correlation_default +
credit_risk_reduction; committee_escalation_threshold=
"two_or_more_material_exceptions" → next_step logic in Workflow C.
The portfolio's constraints.policy_id tells you which policy set governs it.
3. Workflow A — Energy / fixed-income credit trade strategy
Tasks: build a BUY package (income sleeve) or a SELL+BUY rotation
(risk-reduction). Answer fields: trade_package/rotation.trades,
post_trade_metrics / risk_metrics, constraint_checks / exception_flags,
sales_positioning or watchlist_handling, risk_note_code, data_precedence.
3.1 Gather state
GET /api/portfolios/<id> → holdings (instrument_id, quantity_usd_m),
MV, constraints.policy_id.
GET /api/instruments/bonds → bond metrics (rating_bucket, modified_duration,
YTM, spread, energy_linked, subsector, issuer_id).
GET /api/issuers → cross-reference watchlist flag per issuer_id and
subsector/sector/rating.
GET /api/market/energy → signal scores + pitch_themes (energy tasks).
3.2 Pre-trade metrics (market-value-weighted)
Using quantity_usd_m as the market-value weight:
HY allocation % = Σ(HY quantity) / Σ(all quantity) × 100.
weighted modified duration = Σ(qty × modified_duration_years) / Σ(qty).
weighted YTM = Σ(qty × yield_to_maturity_pct) / Σ(qty).
Same weighting applies post-trade after applying buys/sells (post MV = pre MV
- buys − sells; for a "new sleeve allocation" MV grows by the buy notional; for
a "fund from proceeds" rotation MV stays ~constant, i.e. Σbuys = Σsells).
3.3 Eligibility & exclusion rules (critical)
- Watchlist avoidance: never BUY a bond whose issuer
watchlist is true.
Cross-check the issuer_id from /api/issuers. Watchlist flags are on the
issuer, not the bond — a non-watchlist-looking bond whose issuer is
watchlisted is still forbidden.
- Duration-ineligible distractors: bonds with
modified_duration_years
outside [3.0, 5.0] are ineligible as BUY candidates (the duration band is
enforced on both the portfolio and effectively on eligible candidates; long
2033/2034 paper at dur 5.4–6.7 and short paper at dur 2.3–2.9 are
distractors). Always prefer candidates with duration inside the band.
- HY cap: post-trade HY % must be ≤ 20.0 (
hy_cap_pass). If the book is
already over cap, the rotation must sell enough HY to get under.
- Duration band: post-trade weighted modified duration must be in [3.0, 5.0]
(
duration_band_pass). For rotations, keep duration roughly flat ("without
creating a duration shortfall").
- Diversification (
selected_*_diversification_pass): the selected BUY
package must span ≥2 distinct issuers AND ≥2 distinct subsectors
(subsector_min_count_for_diversified=2). This is a check on the selected
trades, not the whole book. Do not pair two bonds from the same issuer or
same subsector.
- Avoid doubling up on already-overweight issuers where it would push a single
issuer over 12% concentration, but the hard "selected" check is the 2-buy
diversification rule above.
3.4 Selection SOP — income BUY package (2 tickets, even split)
- Compute the package notional from the desk request (e.g. 8.0m total → 4.0
each, 1-decimal precision).
- Among
candidate=true bonds that are energy_linked=true (for energy
sleeves), non-watchlist, duration-in-band, pick an anchor matching the
desk's top preference and the strongest /api/market/energy signal. The LNG
export signal is typically strongest; an IG LNG exporter bond (IG, dur ~4,
themes LNG_EXPORTS/GAS_DEMAND) is the usual anchor.
- Pick the second bond from a different issuer and different subsector,
also energy-linked, non-watchlist, duration-in-band, that improves carry
(YTM above the pre-trade weighted YTM). If HY budget allows (post HY ≤ 20%),
a non-watchlist HY carry bond maximizes carry improvement; choose the
highest-YTM eligible HY diversifier. Renewables / merchant-power HY are
typical carry diversifiers.
- Verify all five constraint_checks; compute post_trade_metrics (2 decimals);
notional_usd_m to 1 decimal; sort trade_package ascending by
instrument_id.
sales_positioning.target_segment: take from the request's client_context
(e.g. "multi-asset income" → multi_asset_income).
sales_positioning.theme: pick the enum matching the dominant
pitch_themes/signal (LNG-led → lng_export_tailwind; renewables+LNG →
transition_bond_selectivity; avoiding watchlist HY carry →
avoid_watchlist_yield_trap; midstream → midstream_stability).
data_precedence: current_environment_over_stale_payload if the stale
snapshot/MV/quantity differs from the environment, else no_conflict_found.
3.5 Selection SOP — risk-reduction rotation (SELL+BUY)
- Identify pressure points to sell: watchlist holdings (issuer
watchlist=true) first, then HY holdings, prioritizing the lowest-carry HY
for sale while keeping the highest-carry non-watchlist HY to preserve carry.
- Sell enough HY to (a) clear watchlist exposure to 0 and (b) get post-trade
HY % ≤ 20 (and meet the
target_hy_reduction_pct ≥ 4 pp where the policy is
POL_CREDIT_RISK_REDUCTION — getting under the 20% cap always satisfies 4 pp).
- Buy IG
candidate=true bonds from the candidate shortlist that are
non-watchlist, duration-in-band. Fund the buys with the sell proceeds
(Σbuys = Σsells, MV constant). Pick the highest-YTM IG candidates across
different sectors for carry + diversification.
- Trivially never buy a watchlist HY candidate even if it appears on the
desk's shortlist with a "high carry" comment (
buys_avoid_watchlist=true).
- Sort
rotation.trades SELL before BUY, then instrument_id ascending
within each action. quantity_usd_m to 1 decimal.
risk_metrics: post_trade_hy_allocation_pct (2), post_trade_duration_years
(2), hy_reduction_pct_points (2) = pre_HY% − post_HY%,
post_trade_watchlist_exposure_usd_m (1).
watchlist_handling.watchlist_sell_ids: all sold watchlist instrument_ids,
ascending.
risk_note_code: the dominant theme — watchlist_concentration if a
watchlist name was the acute risk removed; hy_cap_pressure if the HY-over-
cap was the driver; duration_preservation if keeping duration in-band was
the binding constraint; carry_tradeoff if high-carry HY was sacrificed for
IG quality. no_action only if no trades.
4. Workflow B — International equity correlation review
Tasks: pair correlations across an index universe from monthly levels;
highest/lowest pairs; China/Asia dependence concentration; diversification
candidate set; sleeve actions.
4.1 Compute correlations (yourself)
GET /api/index-levels → levels per index. Use the window from the request's
review_window (level_start_date, level_end_date) — which equals the policy
correlation.review_window_start/end and the index metadata window.
- For each index, take levels with
start_date <= date <= end_date, sorted by
date. Monthly simple return r_t = (level_t − level_{t-1}) / level_{t-1}.
return_observations = number of returns = (number of levels in window) − 1
(12 monthly levels → 11 returns).
- Pearson correlation between two return series:
r = Σ((xᵢ−x̄)(yᵢ−ȳ)) / sqrt(Σ(xᵢ−x̄)² · Σ(yᵢ−ȳ)²).
- Round correlations to 3 decimals.
pair_id / pair = the two index ids in ascending alphabetical order.
4.2 Extreme pairs
highest_positive = the pair with the maximum correlation.
lowest = the pair with the minimum correlation (most negative).
Both report pair_id (sorted) and correlation (3 decimals).
4.3 Concentration flags
china_asia_dependence_flag = true when the China index and the Asia-Pacific-
ex-Japan index (and/or EM) are correlated above the high threshold (0.8),
i.e. the dedicated China + Asia sleeves carry overlapping beta.
high_threshold_breached = true when the relevant concentration pair crosses
correlation_high_threshold (0.8).
primary_code: CHINA_ASIA_DEPENDENCE if the China/Asia/EM overlap is the
dominant concentration (matches the CIO concern codes
ASIA_BETA_OVERLAP / CHINA_DEDICATED_SLEEVE); GLOBAL_DEVELOPED_OVERLAP if the
dominant high correlations are developed-world (EAFE/WORLD/ACWI);
NO_MATERIAL_CONCENTRATION if nothing breaches.
4.4 Diversification candidates & sleeve actions
diversification_candidates: the ex-China EM diversifier pool (e.g.
IDX_EM_EX_CHINA, IDX_INDIA, IDX_LATAM), ascending alphabetical — the sleeves
the committee evaluates for diversification. (The genuinely low/negatively-
correlated one — typically LatAm vs China — is the one to add.)
sleeve_actions (length per template, ordered ascending by sleeve name):
trim the concentrated sleeve (the high-beta China/Asia sleeve → action
trim, target that index) and add the low-correlation diversifier sleeve
(→ action add, target the diversifying index). Use monitor/hold/rotate
sparingly only when no size change is warranted. target_index_id must be
from the template's allowed set.
5. Workflow C — Cross-asset active allocation view updates
Tasks: produce per-opportunity-set active views (view, change, conviction,
rationale_code) from prior views + macro signals; choose a risk overlay;
and (combined variant) link correlations to sleeve actions + rebalance trigger
5.1 Source the inputs
- Prior (prior-quarter) views:
GET /api/allocation/prior-views. Each
record has quarter, previous_quarter, opportunity_set, view, conviction.
For a target quarter Q with prior quarter P, the prior view = the record
where quarter=Q and previous_quarter=P. (Its view/conviction fields
ARE the prior-quarter view you compare against — do not treat them as the new
view.) The new view is derived from signals below.
- Current signals:
GET /api/macro-signals, filter quarter=Q. Each gives
score (signal_score) and rationale_code.
- Mapping policy:
POL_ALLOCATION_MAPPING (§2).
- Asset class: from
/api/allocation/opportunity-sets (opportunity_set →
asset_class: Equities / Duration / Credit / Currency).
5.2 Derive each allocation row
For each opportunity_set in the request's focus list (in that order):
asset_class from the opportunity-sets taxonomy.
signal_score = macro-signal score, 3 decimals.
view (new) from score via mapping: OW if ≥0.35, UW if ≤−0.35, else N.
conviction from |score|: HIGH ≥0.70, MEDIUM ≥0.35, LOW <0.35.
rationale_code = macro-signal rationale_code (must be one of the enum).
prior_view (combined-variant template) = the prior-views record's view.
change = compare new view rank vs prior view rank (OW=1,N=0,UW=−1):
UP if new>prior, DOWN if new<prior, UNCHANGED if equal.
- Order rows exactly as the request's focus_opportunity_sets list (or the
template's
item_order).
5.3 Choose the risk overlay
Pick the single overlay that captures the dominant cross-asset rotation:
- If credit (Corporate High Yield) is UW and/or duration (U.S. Treasuries) is OW
→ rotate credit→duration:
DURATION_QUALITY_TILT / tilt_to_duration_quality.
- If HY UW is the single clearest risk with no duration bid →
CREDIT_RISK_REDUCTION / trim_credit_beta.
- If cyclical equities (Europe/LatAm) are the leading OWs with no defensive
rotation →
EQUITY_BETA_EXTENSION / add_cyclical_equity_beta.
- If USD/defensive currency view is the headline →
CURRENCY_DEFENSIVE_HEDGE / add_currency_hedge.
- Only
NO_OVERLAY / hold_policy_weights when no view departs materially from
neutral.
rationale_codes: the 1–3 enum codes driving the overlay, business priority
order (largest |signal| / most material risk first). For a duration-quality
tilt use the duration-support and credit-risk codes; for credit-risk reduction
use the negative credit/equity-risk codes ordered by severity.
policy_id (where the template requires it) = POL_ALLOCATION_MAPPING.
5.4 Combined correlation + allocation variant (multi-asset committee JSON)
This variant (e.g. PF-MA-HELIO) asks for both a correlation_summary and
allocation_views plus target_sleeve_actions, rebalance_trigger,
portfolio_risk_concentration_flag, next_step.
correlation_summary (length 2, order [highest_concentration, best_diversifier]): from the named index subset, the highest-correlation
pair = concentration risk; the most-negative pair = best diversifier. Pair
ids sorted alphabetically; correlation 3 decimals; compute as in §4.
allocation_views: as §5.2 for the named opportunity sets (typically
Emerging Markets, India, Latin America, USD), in the template's item_order.
target_sleeve_actions: one per opportunity set in item_order. Map the
view+correlation to an action: trim sleeves whose index is in the
high-concentration pair and whose view is UW; add sleeves that are the
low-correlation diversifier / OW offset; hold for sleeves whose view moved
to neutral (revert to policy weights); monitor/hedge only when clearly
warranted.
rebalance_trigger: correlation_cap_breach if a pair exceeds the 0.8 high
threshold (the usual trigger for these reviews); else hy_cap_pressure /
duration_drift / watchlist_concentration if a credit exception dominates;
committee_review as the catch-all.
portfolio_risk_concentration_flag: true if any concentration pair breaches
the high threshold (0.8).
next_step: count material exceptions (correlation breach + any credit
exception). Under POL_MULTI_ASSET_RISK, two-or-more →
defer_pending_risk_review; one addressable exception →
approve_with_monitoring; clean rotation that resolves the breach →
approve_rotation; unresolvable breach → reject_constraint_breach.
Under POL_MULTI_ASSET_DEFAULT a single correlation breach that the
rotation addresses typically → approve_with_monitoring.
6. Precision & ordering conventions (all workflows)
| Field |
Precision |
Notes |
notional_usd_m, quantity_usd_m |
1 decimal |
USD millions. |
total_market_value_usd_m |
2 decimals |
|
hy_allocation_pct, weighted_modified_duration_years, weighted_yield_to_maturity_pct |
2 decimals |
market-value-weighted. |
post_trade_hy_allocation_pct, post_trade_duration_years, hy_reduction_pct_points |
2 decimals |
|
post_trade_watchlist_exposure_usd_m |
1 decimal |
|
| correlation values |
3 decimals |
Pearson of monthly simple returns. |
signal_score |
3 decimals |
from macro-signals. |
return_observations |
integer |
= levels_in_window − 1. |
| booleans |
true/false |
lower-case JSON. |
Ordering rules (strict):
trade_package: ascending by instrument_id.
rotation.trades: SELL before BUY, then instrument_id ascending within each action.
index_set, diversification_candidates, pair/pair_id: ascending alphabetical by index id.
sleeve_actions: ascending by sleeve.
allocation_views/target_sleeve_actions: the request's focus_opportunity_sets order (or template item_order).
watchlist_sell_ids: ascending instrument_id.
rationale_codes: business priority, highest priority first.
7. Common pitfalls & exclusion rules
- Stale worksheet trap: a desk payload may state MV, HY%, duration, or
holding quantities that differ from the environment. Always recompute from
/api/portfolios/<id> holdings + /api/instruments/bonds. Set
data_precedence = current_environment_over_stale_payload.
- Watchlist is on the issuer, not the bond: a bond's
rating/sector can
look fine while its issuer_id is watchlist=true. Always join to
/api/issuers. Watchlist HY "high carry" candidates on a desk shortlist are
deliberate traps — never buy them.
- Duration-ineligible distractors: long-dated 2032–2034 paper (dur 5.4–6.7)
and very short 2028 paper (dur 2.3–2.9) sit outside [3,5]; exclude as buys.
- HY cap math: HY % is of post-trade MV (after buys/sells), not pre-trade.
For a rotation that keeps MV constant, the denominator is unchanged; for a
new-sleeve BUY package, MV grows by the buy notional.
- Diversification is on the selected package: do not reject a pair just
because the broader book is concentrated; the
selected_*_diversification_pass
checks the 2 chosen buys span ≥2 issuers and ≥2 subsectors.
- Correlations are not stored: never look for a "correlation" field in the
environment; compute from
index-levels. Use simple (not log) returns.
- Prior view vs new view: in
/api/allocation/prior-views, the record
quarter=Q, previous_quarter=P holds the prior (P) view for a Q target.
The new Q view comes from Q's macro-signals + the mapping policy. Getting
this backwards inverts every change.
- Conviction uses |score|: a UW view at score −0.373 has MEDIUM conviction
(|0.373|≥0.35), not LOW. A score of exactly ±0.35 is MEDIUM (≥ threshold).
- change direction: UP/DOWN by view rank, not by score magnitude. OW(1)→N(0)
is DOWN; N(0)→UW(−1) is DOWN; N→OW is UP.
- Enum casing & values: match the template's allowed_values exactly (e.g.
multi_asset_income not multi-asset-income; Q2_2026 not Q2-2026).
- Trailing zeros: JSON numerics
5.8 and 5.80 are equal, but always round
to the template's declared decimals before emitting.
8. End-to-end solving checklist
- Read
prompt.txt, payloads/answer_template.json, and the request payload.
Note required keys, enums, ordering, precision, and which workflow(s) apply.
- Fetch the portfolio (
/api/portfolios/<id>) and the relevant universe
endpoints; cache large ones to /tmp.
- Identify the governing
policy_id from the portfolio constraints and read
the matching policy thresholds from /api/policies.
- Apply the workflow SOP (§3 / §4 / §5).
- Recompute every numeric field from environment data; round to template
precision; apply the template's sort order.
- Re-check every enum value against
allowed_values; re-check every boolean
(constraint pass/fail) against the policy thresholds.
- Emit a single JSON object conforming to the template — no extra keys, no
narrative.
Reusable correlation snippet (python3)
import json, itertools
from statistics import mean
lv = json.load(open('/tmp/index_levels_all.json'))
START='2025-05-30'; END='2026-04-30'
def rets(idx):
rows=sorted([r for r in lv[idx] if START<=r['date']<=END],key=lambda r:r['date'])
L=[r['level'] for r in rows]
return [(L[i]-L[i-1])/L[i-1] for i in range(1,len(L))]
def pearson(x,y):
n=len(x); mx=mean(x); my=mean(y)
sxy=sum((x[i]-mx)*(y[i]-my) for i in range(n))
return sxy/((sum((a-mx)**2 for a in x)**0.5)*(sum((b-my)**2 for b in y)**0.5))
R={i:rets(i) for i in UNIVERSE}
pairs=sorted(((a,b,pearson(R[a],R[b])) for a,b in itertools.combinations(sorted(UNIVERSE),2)),key=lambda t:t[2])
highest=max(pairs,key=lambda t:t[2]); lowest=pairs[0] # pairs[0] is min
Reusable weighted-metric snippet
bonds={b['instrument_id']:b for b in json.load(open('/tmp/bonds_all.json'))}
# port = {instrument_id: market_value_usd_m}
mv=sum(port.values())
hy=sum(q for iid,q in port.items() if bonds[iid]['rating_bucket']=='HY')/mv*100
dur=sum(q*bonds[iid]['modified_duration_years'] for iid,q in port.items())/mv
ytm=sum(q*bonds[iid]['yield_to_maturity_pct'] for iid,q in port.items())/mv
print(round(mv,2),round(hy,2),round(dur,2),round(ytm,2))
1---2name: self-attempt-02-173description: Asteria Investment Office — Portfolio-Risk Task Skill4---5# Asteria Investment Office — Portfolio-Risk Task Skill67Operating SOP for solving Asteria portfolio-risk evaluation tasks. The test8solver receives only a task `input/` (prompt + payloads/answer_template.json +9a request payload) plus this skill and the remote environment. Three workflow10families appear; a task may involve one or combine two.1112Base URL: `<remote-env-url>` (GET only). Use `curl` or `python3`+`urllib`.1314---1516## 0. Golden rules (apply to every task)17181. **Environment = book of record.** Every `input/payloads/*.json` is *intake19 context* and may be stale (old marks, old quantities, old desk notes).20 Always reconcile to the environment. When a local payload conflicts with the21 environment, prefer the environment. The `data_precedence` answer field is22 `current_environment_over_stale_payload` whenever any conflict exists (e.g.23 stale snapshot MV/quantity differs from `/api/portfolios/<id>`).242. **`as_of_date`** in your answer = the environment's current as-of date. Read25 it from `/api/portfolios/<id>` (`as_of_date`) or `/api/policies`26 (`as_of_date`). Do **not** copy a date stamped on a local request payload or27 a stale worksheet unless the prompt explicitly overrides.283. **No precomputed correlations.** The environment stores index *levels*, never29 correlations. Compute Pearson correlations yourself from monthly simple30 returns (see Workflow B).314. **Precision follows each field in `answer_template.json`.** Round the final32 value to the declared decimals. JSON numerics like `5.8` and `5.80` are equal,33 but always round to the specified precision (2/3/1 decimals) before emitting.345. **Read the answer template first.** It declares required keys, enums,35 ordering, and precision per field. Conform exactly: missing keys, wrong36 enum values, wrong sort order, or wrong precision all fail.376. **Output is a single JSON object** matching the template — no narrative38 outside JSON unless the prompt asks for it.3940---4142## 1. Environment endpoints (what each is for)4344| Endpoint | Use |45|---|---|46| `GET /api/policies` | All constraint/threshold/mapping policies + `as_of_date`. |47| `GET /api/portfolios` | List of portfolio summaries. |48| `GET /api/portfolios/<id>` | Objective, constraints, **current holdings** (instrument_id, quantity_usd_m, sleeve), MV, as_of. |49| `GET /api/instruments/bonds` | Bond universe. Filters: `?candidate=true`, `?rating_bucket=HY`. Fields: instrument_id, issuer_id, rating_bucket(IG/HY), rating, modified_duration_years, yield_to_maturity_pct, coupon_pct, spread_bps, sector, subsector, energy_linked, candidate, recommended_theme_tags. |50| `GET /api/issuers` | issuer_id, sector, subsector, rating_bucket, **watchlist(bool)**, credit_outlook, research_tags. |51| `GET /api/market/energy` | Oil/gas/LNG/refining/renewables signal scores + pitch_themes. |52| `GET /api/indices` | Index metadata (region, level window). |53| `GET /api/index-levels` | All monthly index levels: dict index_id -> [{date, level}]. |54| `GET /api/index-levels/<index_id>` | One index's monthly levels. |55| `GET /api/allocation/opportunity-sets` | Cross-asset taxonomy: opportunity_set -> asset_class (Equities/Duration/Credit/Currency). |56| `GET /api/allocation/prior-views` | Prior-quarter active views (see Workflow C). |57| `GET /api/macro-signals` | Current signal scores + rationale_code per opportunity_set/quarter. |5859`GET /api/catalog` may return empty; ignore it and use the typed endpoints above.6061**Reuse fetches.** `/api/instruments/bonds`, `/api/issuers`, `/api/index-levels`,62`/api/allocation/prior-views`, `/api/macro-signals` are small and shared across63tasks — fetch once, cache to `/tmp/*.json`, reuse.6465---6667## 2. Policy reference (`/api/policies`)6869- `credit_default` (POL_CREDIT_DEFAULT): `max_hy_allocation_pct`=20.0,70 `duration_band_years`=[3.0, 5.0], `issuer_concentration_limit_pct`=12.0,71 `subsector_min_count_for_diversified`=2, `target_hy_reduction_pct`=0.0.72- `credit_risk_reduction` (POL_CREDIT_RISK_REDUCTION): same as above but73 `target_hy_reduction_pct`=4.0 (used by risk-reduction rotations).74- `correlation` (POL_CORRELATION_DEFAULT):75 `correlation_high_threshold`=0.8, `correlation_low_threshold`=0.2,76 review window (e.g. 2025-05-30 → 2026-04-30).77- `allocation_mapping` (POL_ALLOCATION_MAPPING):78 - `view_score_thresholds`: OW if score ≥ 0.35; UW if score ≤ -0.35; N in (-0.35, 0.35).79 - `conviction_thresholds`: HIGH if |score| ≥ 0.70; MEDIUM if |score| ≥ 0.35; LOW if |score| < 0.35.80 - `view_rank`: OW=1, N=0, UW=-1 (for computing change vs prior).81- `multi_asset` (POL_MULTI_ASSET_DEFAULT): uses allocation_mapping +82 correlation_default + credit_default. Used by multi-asset sleeves.83- `multi_asset_risk` (POL_MULTI_ASSET_RISK): uses correlation_default +84 credit_risk_reduction; `committee_escalation_threshold`=85 "two_or_more_material_exceptions" → next_step logic in Workflow C.8687The portfolio's `constraints.policy_id` tells you which policy set governs it.8889---9091## 3. Workflow A — Energy / fixed-income credit trade strategy9293Tasks: build a BUY package (income sleeve) **or** a SELL+BUY rotation94(risk-reduction). Answer fields: trade_package/rotation.trades,95post_trade_metrics / risk_metrics, constraint_checks / exception_flags,96sales_positioning or watchlist_handling, risk_note_code, data_precedence.9798### 3.1 Gather state991. `GET /api/portfolios/<id>` → holdings (instrument_id, quantity_usd_m),100 MV, constraints.policy_id.1012. `GET /api/instruments/bonds` → bond metrics (rating_bucket, modified_duration,102 YTM, spread, energy_linked, subsector, issuer_id).1033. `GET /api/issuers` → cross-reference **watchlist** flag per issuer_id and104 subsector/sector/rating.1054. `GET /api/market/energy` → signal scores + pitch_themes (energy tasks).106107### 3.2 Pre-trade metrics (market-value-weighted)108Using `quantity_usd_m` as the market-value weight:109- `HY allocation %` = Σ(HY quantity) / Σ(all quantity) × 100.110- `weighted modified duration` = Σ(qty × modified_duration_years) / Σ(qty).111- `weighted YTM` = Σ(qty × yield_to_maturity_pct) / Σ(qty).112113Same weighting applies post-trade after applying buys/sells (post MV = pre MV114+ buys − sells; for a "new sleeve allocation" MV grows by the buy notional; for115a "fund from proceeds" rotation MV stays ~constant, i.e. Σbuys = Σsells).116117### 3.3 Eligibility & exclusion rules (critical)118- **Watchlist avoidance**: never BUY a bond whose issuer `watchlist` is true.119 Cross-check the issuer_id from `/api/issuers`. Watchlist flags are on the120 *issuer*, not the bond — a non-watchlist-looking bond whose issuer is121 watchlisted is still forbidden.122- **Duration-ineligible distractors**: bonds with `modified_duration_years`123 outside [3.0, 5.0] are ineligible as BUY candidates (the duration band is124 enforced on both the portfolio and effectively on eligible candidates; long125 2033/2034 paper at dur 5.4–6.7 and short paper at dur 2.3–2.9 are126 distractors). Always prefer candidates with duration inside the band.127- **HY cap**: post-trade HY % must be ≤ 20.0 (`hy_cap_pass`). If the book is128 already over cap, the rotation must sell enough HY to get under.129- **Duration band**: post-trade weighted modified duration must be in [3.0, 5.0]130 (`duration_band_pass`). For rotations, keep duration roughly flat ("without131 creating a duration shortfall").132- **Diversification** (`selected_*_diversification_pass`): the *selected* BUY133 package must span ≥2 distinct issuers AND ≥2 distinct subsectors134 (`subsector_min_count_for_diversified`=2). This is a check on the **selected135 trades**, not the whole book. Do not pair two bonds from the same issuer or136 same subsector.137- Avoid doubling up on already-overweight issuers where it would push a single138 issuer over 12% concentration, but the hard "selected" check is the 2-buy139 diversification rule above.140141### 3.4 Selection SOP — income BUY package (2 tickets, even split)1421. Compute the package notional from the desk request (e.g. 8.0m total → 4.0143 each, 1-decimal precision).1442. Among `candidate=true` bonds that are `energy_linked=true` (for energy145 sleeves), non-watchlist, duration-in-band, pick an **anchor** matching the146 desk's top preference and the strongest `/api/market/energy` signal. The LNG147 export signal is typically strongest; an IG LNG exporter bond (IG, dur ~4,148 themes LNG_EXPORTS/GAS_DEMAND) is the usual anchor.1493. Pick the **second** bond from a *different issuer and different subsector*,150 also energy-linked, non-watchlist, duration-in-band, that improves carry151 (YTM above the pre-trade weighted YTM). If HY budget allows (post HY ≤ 20%),152 a non-watchlist HY carry bond maximizes carry improvement; choose the153 highest-YTM eligible HY diversifier. Renewables / merchant-power HY are154 typical carry diversifiers.1554. Verify all five constraint_checks; compute post_trade_metrics (2 decimals);156 `notional_usd_m` to 1 decimal; sort `trade_package` ascending by157 instrument_id.1585. `sales_positioning.target_segment`: take from the request's client_context159 (e.g. "multi-asset income" → `multi_asset_income`).1606. `sales_positioning.theme`: pick the enum matching the dominant161 `pitch_themes`/signal (LNG-led → `lng_export_tailwind`; renewables+LNG →162 `transition_bond_selectivity`; avoiding watchlist HY carry →163 `avoid_watchlist_yield_trap`; midstream → `midstream_stability`).1647. `data_precedence`: `current_environment_over_stale_payload` if the stale165 snapshot/MV/quantity differs from the environment, else `no_conflict_found`.166167### 3.5 Selection SOP — risk-reduction rotation (SELL+BUY)1681. Identify **pressure points** to sell: watchlist holdings (issuer169 `watchlist`=true) first, then HY holdings, prioritizing the lowest-carry HY170 for sale while keeping the highest-carry non-watchlist HY to preserve carry.1712. Sell enough HY to (a) clear watchlist exposure to 0 and (b) get post-trade172 HY % ≤ 20 (and meet the `target_hy_reduction_pct` ≥ 4 pp where the policy is173 POL_CREDIT_RISK_REDUCTION — getting under the 20% cap always satisfies 4 pp).1743. Buy IG `candidate=true` bonds from the candidate shortlist that are175 non-watchlist, duration-in-band. Fund the buys with the sell proceeds176 (Σbuys = Σsells, MV constant). Pick the highest-YTM IG candidates across177 different sectors for carry + diversification.1784. Trivially **never** buy a watchlist HY candidate even if it appears on the179 desk's shortlist with a "high carry" comment (`buys_avoid_watchlist`=true).1805. Sort `rotation.trades` **SELL before BUY**, then instrument_id ascending181 within each action. `quantity_usd_m` to 1 decimal.1826. `risk_metrics`: `post_trade_hy_allocation_pct` (2), `post_trade_duration_years`183 (2), `hy_reduction_pct_points` (2) = pre_HY% − post_HY%,184 `post_trade_watchlist_exposure_usd_m` (1).1857. `watchlist_handling.watchlist_sell_ids`: all sold watchlist instrument_ids,186 ascending.1878. `risk_note_code`: the dominant theme — `watchlist_concentration` if a188 watchlist name was the acute risk removed; `hy_cap_pressure` if the HY-over-189 cap was the driver; `duration_preservation` if keeping duration in-band was190 the binding constraint; `carry_tradeoff` if high-carry HY was sacrificed for191 IG quality. `no_action` only if no trades.192193---194195## 4. Workflow B — International equity correlation review196197Tasks: pair correlations across an index universe from monthly levels;198highest/lowest pairs; China/Asia dependence concentration; diversification199candidate set; sleeve actions.200201### 4.1 Compute correlations (yourself)2021. `GET /api/index-levels` → levels per index. Use the window from the request's203 `review_window` (level_start_date, level_end_date) — which equals the policy204 `correlation.review_window_start/end` and the index metadata window.2052. For each index, take levels with `start_date <= date <= end_date`, sorted by206 date. **Monthly simple return** r_t = (level_t − level_{t-1}) / level_{t-1}.2073. `return_observations` = number of returns = (number of levels in window) − 1208 (12 monthly levels → 11 returns).2094. **Pearson correlation** between two return series:210 r = Σ((xᵢ−x̄)(yᵢ−ȳ)) / sqrt(Σ(xᵢ−x̄)² · Σ(yᵢ−ȳ)²).2115. Round correlations to **3 decimals**.2126. `pair_id` / `pair` = the two index ids in **ascending alphabetical order**.213214### 4.2 Extreme pairs215- `highest_positive` = the pair with the maximum correlation.216- `lowest` = the pair with the **minimum** correlation (most negative).217Both report `pair_id` (sorted) and `correlation` (3 decimals).218219### 4.3 Concentration flags220- `china_asia_dependence_flag` = true when the China index and the Asia-Pacific-221 ex-Japan index (and/or EM) are correlated above the high threshold (0.8),222 i.e. the dedicated China + Asia sleeves carry overlapping beta.223- `high_threshold_breached` = true when the relevant concentration pair crosses224 `correlation_high_threshold` (0.8).225- `primary_code`: `CHINA_ASIA_DEPENDENCE` if the China/Asia/EM overlap is the226 dominant concentration (matches the CIO concern codes227 ASIA_BETA_OVERLAP / CHINA_DEDICATED_SLEEVE); `GLOBAL_DEVELOPED_OVERLAP` if the228 dominant high correlations are developed-world (EAFE/WORLD/ACWI); 229 `NO_MATERIAL_CONCENTRATION` if nothing breaches.230231### 4.4 Diversification candidates & sleeve actions232- `diversification_candidates`: the ex-China EM diversifier pool (e.g.233 IDX_EM_EX_CHINA, IDX_INDIA, IDX_LATAM), ascending alphabetical — the sleeves234 the committee evaluates for diversification. (The genuinely low/negatively-235 correlated one — typically LatAm vs China — is the one to *add*.)236- `sleeve_actions` (length per template, ordered ascending by `sleeve` name):237 trim the concentrated sleeve (the high-beta China/Asia sleeve → action238 `trim`, target that index) and add the low-correlation diversifier sleeve239 (→ action `add`, target the diversifying index). Use `monitor`/`hold`/`rotate`240 sparingly only when no size change is warranted. `target_index_id` must be241 from the template's allowed set.242243---244245## 5. Workflow C — Cross-asset active allocation view updates246247Tasks: produce per-opportunity-set active views (view, change, conviction,248rationale_code) from prior views + macro signals; choose a risk overlay;249and (combined variant) link correlations to sleeve actions + rebalance trigger250+ next step.251252### 5.1 Source the inputs253- **Prior (prior-quarter) views**: `GET /api/allocation/prior-views`. Each254 record has `quarter`, `previous_quarter`, `opportunity_set`, `view`, `conviction`.255 For a target quarter Q with prior quarter P, the **prior view** = the record256 where `quarter`=Q and `previous_quarter`=P. (Its `view`/`conviction` fields257 ARE the prior-quarter view you compare against — do not treat them as the new258 view.) The new view is derived from signals below.259- **Current signals**: `GET /api/macro-signals`, filter `quarter`=Q. Each gives260 `score` (signal_score) and `rationale_code`.261- **Mapping policy**: `POL_ALLOCATION_MAPPING` (§2).262- **Asset class**: from `/api/allocation/opportunity-sets` (opportunity_set →263 asset_class: Equities / Duration / Credit / Currency).264265### 5.2 Derive each allocation row266For each opportunity_set in the request's focus list (in that order):2671. `asset_class` from the opportunity-sets taxonomy.2682. `signal_score` = macro-signal score, **3 decimals**.2693. `view` (new) from score via mapping: OW if ≥0.35, UW if ≤−0.35, else N.2704. `conviction` from |score|: HIGH ≥0.70, MEDIUM ≥0.35, LOW <0.35.2715. `rationale_code` = macro-signal `rationale_code` (must be one of the enum).2726. `prior_view` (combined-variant template) = the prior-views record's `view`.2737. `change` = compare new view rank vs prior view rank (OW=1,N=0,UW=−1):274 `UP` if new>prior, `DOWN` if new<prior, `UNCHANGED` if equal.2758. Order rows exactly as the request's focus_opportunity_sets list (or the276 template's `item_order`).277278### 5.3 Choose the risk overlay279Pick the single overlay that captures the dominant cross-asset rotation:280- If credit (Corporate High Yield) is UW and/or duration (U.S. Treasuries) is OW281 → rotate credit→duration: `DURATION_QUALITY_TILT` / `tilt_to_duration_quality`.282- If HY UW is the single clearest risk with no duration bid →283 `CREDIT_RISK_REDUCTION` / `trim_credit_beta`.284- If cyclical equities (Europe/LatAm) are the leading OWs with no defensive285 rotation → `EQUITY_BETA_EXTENSION` / `add_cyclical_equity_beta`.286- If USD/defensive currency view is the headline →287 `CURRENCY_DEFENSIVE_HEDGE` / `add_currency_hedge`.288- Only `NO_OVERLAY` / `hold_policy_weights` when no view departs materially from289 neutral.290`rationale_codes`: the 1–3 enum codes driving the overlay, **business priority291order** (largest |signal| / most material risk first). For a duration-quality292tilt use the duration-support and credit-risk codes; for credit-risk reduction293use the negative credit/equity-risk codes ordered by severity.294295`policy_id` (where the template requires it) = `POL_ALLOCATION_MAPPING`.296297### 5.4 Combined correlation + allocation variant (multi-asset committee JSON)298This variant (e.g. PF-MA-HELIO) asks for both a `correlation_summary` and299`allocation_views` plus `target_sleeve_actions`, `rebalance_trigger`,300`portfolio_risk_concentration_flag`, `next_step`.3011. `correlation_summary` (length 2, order `[highest_concentration,302 best_diversifier]`): from the named index subset, the highest-correlation303 pair = concentration risk; the most-negative pair = best diversifier. Pair304 ids sorted alphabetically; correlation 3 decimals; compute as in §4.3052. `allocation_views`: as §5.2 for the named opportunity sets (typically306 Emerging Markets, India, Latin America, USD), in the template's `item_order`.3073. `target_sleeve_actions`: one per opportunity set in `item_order`. Map the308 view+correlation to an action: trim sleeves whose index is in the309 high-concentration pair and whose view is UW; `add` sleeves that are the310 low-correlation diversifier / OW offset; `hold` for sleeves whose view moved311 to neutral (revert to policy weights); `monitor`/`hedge` only when clearly312 warranted.3134. `rebalance_trigger`: `correlation_cap_breach` if a pair exceeds the 0.8 high314 threshold (the usual trigger for these reviews); else `hy_cap_pressure` /315 `duration_drift` / `watchlist_concentration` if a credit exception dominates;316 `committee_review` as the catch-all.3175. `portfolio_risk_concentration_flag`: true if any concentration pair breaches318 the high threshold (0.8).3196. `next_step`: count material exceptions (correlation breach + any credit320 exception). Under `POL_MULTI_ASSET_RISK`, two-or-more →321 `defer_pending_risk_review`; one addressable exception →322 `approve_with_monitoring`; clean rotation that resolves the breach →323 `approve_rotation`; unresolvable breach → `reject_constraint_breach`.324 Under `POL_MULTI_ASSET_DEFAULT` a single correlation breach that the325 rotation addresses typically → `approve_with_monitoring`.326327---328329## 6. Precision & ordering conventions (all workflows)330331| Field | Precision | Notes |332|---|---|---|333| `notional_usd_m`, `quantity_usd_m` | 1 decimal | USD millions. |334| `total_market_value_usd_m` | 2 decimals | |335| `hy_allocation_pct`, `weighted_modified_duration_years`, `weighted_yield_to_maturity_pct` | 2 decimals | market-value-weighted. |336| `post_trade_hy_allocation_pct`, `post_trade_duration_years`, `hy_reduction_pct_points` | 2 decimals | |337| `post_trade_watchlist_exposure_usd_m` | 1 decimal | |338| correlation values | 3 decimals | Pearson of monthly simple returns. |339| `signal_score` | 3 decimals | from macro-signals. |340| `return_observations` | integer | = levels_in_window − 1. |341| booleans | true/false | lower-case JSON. |342343**Ordering rules (strict):**344- `trade_package`: ascending by `instrument_id`.345- `rotation.trades`: SELL before BUY, then `instrument_id` ascending within each action.346- `index_set`, `diversification_candidates`, `pair`/`pair_id`: ascending alphabetical by index id.347- `sleeve_actions`: ascending by `sleeve`.348- `allocation_views`/`target_sleeve_actions`: the request's focus_opportunity_sets order (or template `item_order`).349- `watchlist_sell_ids`: ascending instrument_id.350- `rationale_codes`: business priority, highest priority first.351352---353354## 7. Common pitfalls & exclusion rules355356- **Stale worksheet trap**: a desk payload may state MV, HY%, duration, or357 holding quantities that differ from the environment. Always recompute from358 `/api/portfolios/<id>` holdings + `/api/instruments/bonds`. Set359 `data_precedence` = `current_environment_over_stale_payload`.360- **Watchlist is on the issuer, not the bond**: a bond's `rating`/`sector` can361 look fine while its `issuer_id` is `watchlist=true`. Always join to362 `/api/issuers`. Watchlist HY "high carry" candidates on a desk shortlist are363 deliberate traps — never buy them.364- **Duration-ineligible distractors**: long-dated 2032–2034 paper (dur 5.4–6.7)365 and very short 2028 paper (dur 2.3–2.9) sit outside [3,5]; exclude as buys.366- **HY cap math**: HY % is of post-trade MV (after buys/sells), not pre-trade.367 For a rotation that keeps MV constant, the denominator is unchanged; for a368 new-sleeve BUY package, MV grows by the buy notional.369- **Diversification is on the selected package**: do not reject a pair just370 because the broader book is concentrated; the `selected_*_diversification_pass`371 checks the 2 chosen buys span ≥2 issuers and ≥2 subsectors.372- **Correlations are not stored**: never look for a "correlation" field in the373 environment; compute from `index-levels`. Use simple (not log) returns.374- **Prior view vs new view**: in `/api/allocation/prior-views`, the record375 `quarter=Q, previous_quarter=P` holds the **prior (P)** view for a Q target.376 The new Q view comes from Q's `macro-signals` + the mapping policy. Getting377 this backwards inverts every `change`.378- **Conviction uses |score|**: a UW view at score −0.373 has MEDIUM conviction379 (|0.373|≥0.35), not LOW. A score of exactly ±0.35 is MEDIUM (≥ threshold).380- **change direction**: UP/DOWN by view rank, not by score magnitude. OW(1)→N(0)381 is DOWN; N(0)→UW(−1) is DOWN; N→OW is UP.382- **Enum casing & values**: match the template's allowed_values exactly (e.g.383 `multi_asset_income` not `multi-asset-income`; `Q2_2026` not `Q2-2026`).384- **Trailing zeros**: JSON numerics `5.8` and `5.80` are equal, but always round385 to the template's declared decimals before emitting.386387---388389## 8. End-to-end solving checklist3903911. Read `prompt.txt`, `payloads/answer_template.json`, and the request payload.392 Note required keys, enums, ordering, precision, and which workflow(s) apply.3932. Fetch the portfolio (`/api/portfolios/<id>`) and the relevant universe394 endpoints; cache large ones to `/tmp`.3953. Identify the governing `policy_id` from the portfolio constraints and read396 the matching policy thresholds from `/api/policies`.3974. Apply the workflow SOP (§3 / §4 / §5).3985. Recompute every numeric field from environment data; round to template399 precision; apply the template's sort order.4006. Re-check every enum value against `allowed_values`; re-check every boolean401 (constraint pass/fail) against the policy thresholds.4027. Emit a single JSON object conforming to the template — no extra keys, no403 narrative.404405### Reusable correlation snippet (python3)406```python407import json, itertools408from statistics import mean409lv = json.load(open('/tmp/index_levels_all.json'))410START='2025-05-30'; END='2026-04-30'411def rets(idx):412 rows=sorted([r for r in lv[idx] if START<=r['date']<=END],key=lambda r:r['date'])413 L=[r['level'] for r in rows]414 return [(L[i]-L[i-1])/L[i-1] for i in range(1,len(L))]415def pearson(x,y):416 n=len(x); mx=mean(x); my=mean(y)417 sxy=sum((x[i]-mx)*(y[i]-my) for i in range(n))418 return sxy/((sum((a-mx)**2 for a in x)**0.5)*(sum((b-my)**2 for b in y)**0.5))419R={i:rets(i) for i in UNIVERSE}420pairs=sorted(((a,b,pearson(R[a],R[b])) for a,b in itertools.combinations(sorted(UNIVERSE),2)),key=lambda t:t[2])421highest=max(pairs,key=lambda t:t[2]); lowest=pairs[0] # pairs[0] is min422```423424### Reusable weighted-metric snippet425```python426bonds={b['instrument_id']:b for b in json.load(open('/tmp/bonds_all.json'))}427# port = {instrument_id: market_value_usd_m}428mv=sum(port.values())429hy=sum(q for iid,q in port.items() if bonds[iid]['rating_bucket']=='HY')/mv*100430dur=sum(q*bonds[iid]['modified_duration_years'] for iid,q in port.items())/mv431ytm=sum(q*bonds[iid]['yield_to_maturity_pct'] for iid,q in port.items())/mv432print(round(mv,2),round(hy,2),round(dur,2),round(ytm,2))433```