Asteria Investment Office Task Family
You produce a single strict JSON object that conforms to the task's answer_template.json.
The Asteria environment is a read-only HTTP/JSON API and is the current book of record.
Local payloads (*_request.json, *_memo.json, snapshots) are intake context and may be
stale — when they conflict with the environment, the environment wins.
0. Universal operating rules (apply to every task)
- Read the contract first. Open
input/payloads/answer_template.json. It declares the exact required keys, value types, enum allowed-values, list lengths, ordering rules, per-field rounding precision, and anyrequired_valueconstants. Mirror it exactly: include every required key, use only allowed enum values, respect list lengths and the stated item ordering, and round each numeric field to its declared precision. - Read the local payload for IDs, the review window, focus sets, requested outputs, preferences and thresholds — but treat its marks/holdings/ratings/watchlist/dates as possibly stale.
- Pull current truth from the API. Base URL
http://127.0.0.1:8036. All endpoints areGETreturning JSON. Endpoints:GET /api/catalog— valid portfolio/policy/index/issuer/bond/opportunity-set ids.GET /api/policies— all constraint thresholds and the allocation mapping policy.GET /api/portfoliosandGET /api/portfolios/<id>— objective, constraints, holdings.GET /api/instruments/bonds— held + candidate bond universe (filter?candidate=true).GET /api/issuers— sector, subsector, rating bucket, watchlist, outlook, tags.GET /api/market/energy— oil/gas/LNG/refining/renewables signals + pitch themes.GET /api/indices,GET /api/index-levels,GET /api/index-levels/<index_id>— monthly levels.GET /api/allocation/opportunity-sets— taxonomy (asset_class, display_order).GET /api/allocation/prior-views— prior active views (keyed by quarter).GET /api/macro-signals— current macro/asset signal scores + rationale codes (keyed by quarter).- Filters supported on list endpoints by field name, e.g.
?rating_bucket=HY,?candidate=true,?quarter=Q3_2026,?issuer_id=...,?region=....
- as_of_date = the environment's current as-of date. Read it from
GET /api/policies(as_of_date) or fromGET /api/portfolios/<id>(as_of_date). Do NOT use the local payload's request/memo/snapshot date foras_of_date. (In the observed environment this is2026-05-29; always re-read it rather than hard-coding.) - data_precedence / refresh: If the local payload disagrees with the environment on any
mark, holding, rating, watchlist status, MV, or date, the conflict resolves in favor of the
environment. For a
data_precedencefield, emitcurrent_environment_over_stale_payloadwhen such a conflict exists (it usually does — payloads deliberately carry stale snapshots); useno_conflict_foundonly if everything matches. - Constants: Copy
required_valuefields verbatim (e.g.task_id,portfolio_id,target_quarter). Readpolicy_idfrom the environment (POL.../POLICY_SET_...), not invented. - Output only the JSON object — no prose, no markdown fences.
1. Reference data model (observed)
- Policies (
/api/policies):correlationthresholdshigh=0.80,low=0.20;credit_defaultandcredit_risk_reduction: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;target_hy_reduction_pct= 0.0 (default) or 4.0 (risk-reduction).allocation_mapping:view_score_thresholdsOW_min=0.35, UW_max=−0.35, neutral in (−0.35, 0.35);conviction_thresholdsHIGH |s|≥0.7, MEDIUM |s|≥0.35, LOW |s|<0.35;view_rank{UW:−1, N:0, OW:+1}. The top-levelpolicy_id(e.g.POLICY_SET_2026_05) is the lineage id for allocation memos. Always re-read these — do not hard-code numbers if a new task uses a different policy. - Bonds: each has
rating_bucket(IG/HY),modified_duration_years,yield_to_maturity_pct,issuer_id,subsector,sector,energy_linked(bool),candidate(bool),recommended_theme_tags,spread_bps,coupon_pct,maturity. - Issuers: carry the authoritative
watchlistboolean,credit_outlook,rating_bucket,sector/subsector,research_tags. Watchlist is determined at the issuer level (a bond inherits its issuer's watchlist status). A bond may also tagWATCHLIST_RISKinrecommended_theme_tags, but trust the issuer record. - Quantities are in USD millions. Treat
quantity_usd_mas the position market value for all weight/allocation/duration/yield calculations (par ≈ MV, 1:1). Portfolio MV = sum of holdingquantity_usd_m. - Index levels are monthly. The standard review window has 12 monthly levels → 11 monthly returns.
2. Core calculations and rounding
2.1 Portfolio credit metrics (market-value weighted by quantity_usd_m)
Let q_i = post-trade quantity (USD m) of instrument i, MV = Σ q_i.
total_market_value_usd_m = MV(round 2 dp).hy_allocation_pct = 100 * (Σ q_i over HY bucket) / MV(round 2 dp).weighted_modified_duration_years = (Σ q_i * modified_duration_years_i) / MV(round to template precision — 2 dp for metrics objects).weighted_yield_to_maturity_pct = (Σ q_i * ytm_i) / MV(round 2 dp).issuer_concentration_pct(issuer) = 100 * (Σ q_i for that issuer) / MV; must be ≤issuer_concentration_limit_pct(12%).hy_reduction_pct_points = pre_trade_hy_pct − post_trade_hy_pct(percentage points, 2 dp), where pre/post HY% use their respective MV denominators.watchlist_exposure_usd_m = Σ q_i over holdings whose issuer is on watchlist(round to the field's precision, often 1 dp).
Always compute pre-trade metrics from current environment holdings, apply the trades, then compute post-trade metrics. Round only the final reported number, not intermediates.
2.2 Pearson correlation from monthly index levels
- For each index, fetch
/api/index-levels/<index_id>and keep only levels withlevel_start_date ≤ date ≤ level_end_date(the requested/policy window). Sort ascending by date. - Monthly simple returns:
r_t = level_t / level_{t-1} − 1for consecutive months. 12 levels → 11 returns; reportreturn_observations= number of returns. - Pearson correlation between two return series a, b:
corr = Σ(a−ā)(b−b̄) / sqrt(Σ(a−ā)² · Σ(b−b̄)²). - Round correlations to 3 decimals.
- Compute over the pairwise combinations of the requested index universe only.
2.3 Active allocation view derivation (from macro score + prior view)
For each opportunity set, read its current-quarter macro signal from /api/macro-signals
filtered to the task's target_quarter (the row matching opportunity_set + quarter):
signal_score= that row'sscore(report at template precision, e.g. 3 dp).view: OW if score ≥ 0.35; UW if score ≤ −0.35; else N. (Use policy thresholds, not hard 0.35.)conviction: HIGH if |score| ≥ 0.7; MEDIUM if 0.35 ≤ |score| < 0.7; LOW if |score| < 0.35.prior_view: from/api/allocation/prior-views, the row whosequarterequals the target quarter (itsprevious_quarteris the prior quarter; that row carries the prior view/conviction the desk is updating from). For Q2_2026, use prior-views rows withquarter == Q2_2026.change: compare new view to prior_view viaview_rank{UW:−1,N:0,OW:+1}: rank(new) > rank(prior) →UP; < →DOWN; equal →UNCHANGED.rationale_code: use the macro-signal row'srationale_codedirectly. Exception: when the resolved view isN(neutral), useNEUTRAL_BALANCE. (When the macro rationale is already NEUTRAL_BALANCE this is moot; the safe rule for any N view is NEUTRAL_BALANCE.)asset_class: from/api/allocation/opportunity-sets(Equities / Duration / Credit / Currency).
3. SOPs by task type
Identify the task type from the prompt + which template keys exist:
SOP A — Energy / fixed-income credit trade package (BUY tickets)
Template signature: trade_package, post_trade_metrics, constraint_checks,
sales_positioning, data_precedence.
- Read the portfolio (
/api/portfolios/<id>) for current holdings, MV, constraints, policy_id. Read/api/instruments/bonds,/api/issuers,/api/market/energy. - Determine the desk mandate from the payload: ticket count, total notional, even split, allowed actions (e.g. exactly 2 BUYs, $8.0m total → $4.0m each).
- Eligible candidate filter (intersection of all):
candidate == true(only buy from the candidate universe; held-only bonds are not buys);- matches the desk's exposure theme (e.g.
energy_linked == truefor an energy sleeve); - issuer not on watchlist (drop any bond whose issuer.watchlist is true — e.g. the refining/E&P/telecom names) — this is the "avoid watchlist yield trap" rule;
- align with current energy signals: favor positive signals (LNG, gas, power, midstream, contracted renewables); avoid negative ones (refining). Refining names are also watchlisted.
- Selection to maximize carry while passing constraints and suiting an income pitch:
- Build a package of distinct issuers (issuer diversification) and ≥2 distinct
subsectors (
subsector_min_count_for_diversified). - Keep post-trade
hy_allocation_pct≤max_hy_allocation_pct(20%) and post-tradeweighted_modified_duration_yearsinsideduration_band_years(3.0–5.0). - Prefer a quality/carry balance (e.g. one IG carry name + one HY carry name) rather than stacking the lowest-quality highest-yield names — better income pitch and keeps HY low.
- Tie-break toward the strongest current energy theme (e.g. LNG when LNG signal is highest).
- Build a package of distinct issuers (issuer diversification) and ≥2 distinct
subsectors (
- Compute
post_trade_metricsper §2.1 (apply buys to current holdings). constraint_checks(all booleans):hy_cap_pass(post HY% ≤ cap),duration_band_pass(post WMD within band),selected_issuer_diversification_pass(selected buys are distinct issuers and each issuer post-trade ≤ 12% concentration),selected_subsector_diversification_pass(≥2 distinct subsectors among selected),watchlist_avoidance_pass(no selected issuer on watchlist).sales_positioning.target_segment: map from the payload's client context (e.g. "multi-asset income update" →multi_asset_income; private-bank income →private_bank_income; insurance GA, pension LDI, endowment opportunistic likewise).theme: map to the dominant energy signal (LNG strongest →lng_export_tailwind; midstream defensive →midstream_stability; oil oversupply →oil_oversupply_caution; transition/renewables selectivity →transition_bond_selectivity; if the headline risk is avoiding watchlisted high-yield refiners →avoid_watchlist_yield_trap).data_precedence: compare payload snapshot vs environment (MV, HY%, duration, holdings). Stale snapshot present →current_environment_over_stale_payload.trade_packageordering: ascending by instrument_id; each item{action:"BUY", instrument_id, notional_usd_m}with notional at 1 dp.
SOP B — International equity correlation review
Template signature: review_window, index_set, extreme_pairs, concentration,
diversification_candidates, sleeve_actions.
- Read the requested
index_universeand window from the payload; cross-check the window against/api/policies.correlation(review_window_start/end) and/api/indices. - Compute monthly simple returns and the full pairwise Pearson matrix over the universe
(§2.2). Set
review_window= {level_start_date, level_end_date, return_observations(=11)}.index_set= the universe sorted ascending alphabetically by index id. extreme_pairs.highest_positive= pair with max correlation;extreme_pairs.lowest= pair with min correlation. Eachpair_idis the two ids sorted alphabetically;correlationrounded to 3 dp.concentration:high_threshold_breached= (max correlation across the matrix ≥correlation_high_threshold0.80).china_asia_dependence_flag= true when the China / Asia-Pacific complex is highly correlated with the broad/EM sleeves (e.g. China–AsiaPac and China–EM correlations ≥ 0.80), i.e. the sleeve's risk is concentrated in China/Asia beta.primary_code:CHINA_ASIA_DEPENDENCEif the china_asia flag is set; elseGLOBAL_DEVELOPED_OVERLAPif the dominant high pair is among developed/global indices (World/EAFE/ACWI overlap); elseNO_MATERIAL_CONCENTRATIONif nothing breaches the high threshold.
diversification_candidates(allowed subset, e.g. EM_EX_CHINA / INDIA / LATAM): pick the candidates with the lowest correlation to the concentration anchor (China/Asia complex) — typically the negative/near-zero ones plus the structural China-removal sleeve. Exclude any candidate that is itself highly correlated to the anchor. Sort ascending alphabetically.sleeve_actions(length 2, ascending by sleeve name): trim the concentrated sleeve (e.g. China) and add the best diversifier sleeve (e.g. Latin America). Usetarget_index_idfrom the allowed list;actionfrom {trim, add, hold, hedge, monitor, rotate}.
SOP C — Active allocation view refresh
Template signature: allocation_views (list), risk_overlay, plus lineage
(as_of_date, target_quarter, prior_quarter, policy_id, task_id).
- Lineage: copy
task_id/target_quarter/prior_quarterconstants from the template/payload;policy_idfrom/api/policiestop-level (POLICY_SET_...);as_of_datefrom environment. allocation_views: one row perfocus_opportunity_setsentry, in the payload's focus order (not display_order). For each set derive view/change/conviction/rationale_code andasset_classper §2.3. Verify the row count matchesrequired_length.risk_overlay: synthesize the portfolio-level tilt from the views:- Duration strongly OW + High Yield strongly UW →
DURATION_QUALITY_TILT/tilt_to_duration_quality. - Credit (HY/IG) the dominant risk to cut →
CREDIT_RISK_REDUCTION/trim_credit_beta. - Cyclical equities the dominant positive theme →
EQUITY_BETA_EXTENSION/add_cyclical_equity_beta. - Currency defensiveness the headline →
CURRENCY_DEFENSIVE_HEDGE/add_currency_hedge. - Nothing material →
NO_OVERLAY/hold_policy_weights.rationale_codes: the strongest risk-relevant drivers in business-priority order (highest priority first) — typically the overlay's own theme first (e.g. DURATION_SUPPORT), then the credit-risk theme (HY_VALUATION_RISK), then the dominant equity risk (CHINA_DEPENDENCE). Take these from the macrorationale_codes of the most extreme |score| sets that justify the overlay.
- Duration strongly OW + High Yield strongly UW →
SOP D — Fixed-income risk rebalance (rotation)
Template signature: rotation.trades, risk_metrics, exception_flags,
watchlist_handling, risk_note_code.
- Read current holdings, constraints,
target_hy_reduction_pct(policyPOL_CREDIT_RISK_REDUCTION→ 4.0), and the memo's preferences (min HY reduction, keep duration in CIO band, avoid new watchlist buys). Read bonds + issuers for current ratings/watchlist; the memo's "stale exception board"/"shortlist" are hints, not truth. - Sells: sell the pressure points. Any watchlist holding must be sold (mandatory).
Also sell enough additional HY to bring post-trade HY% under the cap and to meet/exceed the
target HY reduction, but do not over-sell — keep non-watchlist HY carry where the cap and
reduction targets are already satisfied (the goal is reduction, not elimination). Sell the
entire position of each instrument you choose to sell (full current
quantity_usd_m). - Buys (funding): the rotation is self-funded — total BUY notional = total SELL
notional, so portfolio MV is unchanged. Fund from current eligible candidates
(
candidate==true), never buy a watchlist issuer. Prefer IG names that keep duration inside the band and avoid a duration shortfall (longer-dated IG raises duration toward the band's upper half without breaching 5.0). Reject any shortlisted name whose issuer is on the watchlist (rejection reason = watchlist). risk_metrics:post_trade_hy_allocation_pct(2 dp),post_trade_duration_years(2 dp),hy_reduction_pct_points= pre HY% − post HY% (2 dp),post_trade_watchlist_exposure_usd_m(1 dp; should be 0.0 once all watchlist holdings are sold).exception_flags:hy_cap_pass(post ≤ 20%),duration_band_pass(post within 3–5),target_hy_reduction_met(reduction ≥ target),watchlist_exposure_cleared(post WL exp = 0).watchlist_handling:watchlist_sell_ids= ascending instrument_ids of the watchlist holdings sold (only issuers actually on watchlist; non-watchlist HY sells are NOT listed);buys_avoid_watchlist= true if no buy is a watchlist issuer.risk_note_code: the headline binding issue —watchlist_concentrationif clearing watchlist was the driver;hy_cap_pressureif HY cap was the driver;duration_preservationif duration was the constraint;carry_tradeoffif carry give-up was the theme;no_actionif no trade.tradesordering: SELL before BUY, then ascending instrument_id within each action. Each trade{action, instrument_id, quantity_usd_m}at 1 dp.
SOP E — Combined committee decision file
Template signature: correlation_summary, target_sleeve_actions, allocation_views,
rebalance_trigger, portfolio_risk_concentration_flag, next_step.
correlation_summary(length 2, order [highest_concentration, best_diversifier]): run §2.2 over the requested equity index ids.highest_concentration= max-correlation pair;best_diversifier= min-correlation pair.pairids sorted alphabetically; correlation 3 dp.allocation_views(fixed item_order from template, e.g. EM, India, LatAm, USD): derive per §2.3, includingprior_view,signal_score(3 dp),view,change,conviction,rationale_code.target_sleeve_actions(same item_order): map each view to an action — UW →trim, OW →add, equity N →hold/monitor; a currency sleeve flagged for defensiveness →hedge(e.g. USD). Honor the committee's stated focus when it dictates a defensive hedge.portfolio_risk_concentration_flag= true if the highest equity correlation ≥correlation_high_threshold(0.80).rebalance_trigger:correlation_cap_breachif the concentration breaches the correlation cap; elsehy_cap_pressure/duration_drift/watchlist_concentrationif those are binding; elsecommittee_review.next_step:approve_with_monitoringwhen a concentration is flagged but the rotation is acceptable;approve_rotationif clean;defer_pending_risk_reviewif unresolved exceptions;reject_constraint_breachif a hard limit is breached and cannot be cleared.- Honor the stale-note instruction to refresh (e.g. a payload that "kept USD overweight" must be re-derived from current macro signals — likely no longer OW).
4. Enum mapping quick reference
- View: score ≥ +0.35 →
OW; ≤ −0.35 →UW; elseN. - Conviction: |score| ≥ 0.7 →
HIGH; ≥ 0.35 →MEDIUM; elseLOW. - Change: rank(new) vs rank(prior) using {UW:−1,N:0,OW:+1} →
UP/DOWN/UNCHANGED. - Rationale: macro row's
rationale_code; if view isNuseNEUTRAL_BALANCE. Codes commonly seen: GROWTH_IMPROVES, RATE_CUT_SUPPORT, CREDIT_SPREAD_RISK, DOLLAR_DEFENSIVE, CHINA_DEPENDENCE, LATAM_DIVERSIFIER, INDIA_OFFSET, DURATION_SUPPORT, HY_VALUATION_RISK, EUROPE_RECOVERY, JAPAN_POLICY_RISK, NEUTRAL_BALANCE. - Sleeve action: UW→trim, OW→add, N→hold/monitor, currency-defensive→hedge.
- Energy theme: LNG dominant→lng_export_tailwind; midstream→midstream_stability; oil oversupply→oil_oversupply_caution; transition/renewables→transition_bond_selectivity; watchlist HY avoidance→avoid_watchlist_yield_trap.
5. Common pitfalls / exclusions
- Do not use the local payload's date for
as_of_date; use the environment as-of date. - Do not buy held-only or watchlist-issuer bonds. Buys come from
candidate==trueand non-watchlist issuers only. - Watchlist is an issuer attribute — resolve every bond's watchlist status via its issuer.
- 11 returns from 12 levels. Filter levels strictly to the requested window before
computing returns; report
return_observationsaccordingly. - Pair ids and index_set must be alphabetically sorted; trade/rotation lists have their own ordering rules (instrument_id ascending; SELL-before-BUY) — read the template's ordering note per field, they differ.
- Self-funded rotations: total BUY notional must equal total SELL notional (MV unchanged) unless the payload says otherwise. Even-split BUY packages must split the total evenly.
- Round only the final reported value to the template precision; compute on unrounded values.
- prior_view comes from the prior-views row whose
quarter== target quarter (itsprevious_quarteris the quarter before). Do not pick the wrong quarter's row. - Risk-overlay
rationale_codesare ordered by business priority (overlay theme first), not by raw score magnitude. - Emit only the JSON object; include every required key; never invent enum values or ids.
6. Verification checklist before returning
- Validate against
answer_template.json: all required keys present, enums valid, list lengths and orderings correct, required_value constants exact. - Re-confirm each numeric field's rounding precision.
- Re-derive views/correlations from the current environment (not the stale payload).
- Confirm constraint booleans are consistent with the computed metrics (HY cap, duration band, issuer concentration, watchlist, target reduction).