Asteria Investment Office — JSON desk SOP
You answer Asteria desk requests by producing a single JSON object that conforms
to the task's answer_template.json. Local payloads (desk requests, memos,
worksheets, exception boards, shortlists) are intake context only and are
frequently stale. The shared Asteria HTTP environment is the book of record:
when a local number disagrees with the environment, the environment wins, and
your data_precedence/lineage fields should say so.
Use python (not python3) for all math. Fetch all data over HTTP and save
responses inside your own working directory.
0. Universal rules (apply to every archetype)
- Environment over payload. Re-pull portfolio holdings, quantities, bond security-master, issuer flags, index levels, prior views, macro signals, and policy thresholds from the environment. Do not trust payload marks, stale quantities, or "kept X overweight" notes. (E.g. a memo HY position of 10 may actually be 12 in the portfolio service; a worksheet that "kept USD OW" may be neutral once the current signal is read.)
- Conform to the template literally. Match required key names, the exact enum value sets, the declared rounding precision per field, list lengths, and the declared ordering rule. A wrong enum spelling, an extra/missing key, or wrong sort order each costs points.
- Rounding. Round only at the end, to the precision the field declares (correlations 3 dp; pct/years metrics usually 2 dp; not/quantity usually 1 dp; signal scores 3 dp). Carry full precision through intermediate math.
as_of_dateis the environment's current as-of date (read it from the portfolio record or/api/policies, e.g. the policy set'sas_of_date), not the payload's memo date.- Endpoints (base
<remote-env-url>):/api/catalog,/api/policies,/api/portfolios/<id>,/api/instruments/bonds,/api/issuers,/api/market/energy,/api/indices,/api/index-levels(or/<id>),/api/allocation/opportunity-sets,/api/allocation/prior-views,/api/macro-signals. List endpoints accept equality filters (?rating_bucket=HY,?candidate=true,?quarter=Q2_2026).
1. Policy thresholds (read from /api/policies; values below are the defaults)
- Allocation mapping (
POL_ALLOCATION_MAPPING):- view from signal score:
OWif score ≥ +0.35,UWif score ≤ −0.35, elseN(neutral band is the closed interval [−0.35, +0.35]). - conviction from |score|:
HIGHif ≥ 0.7,MEDIUMif ≥ 0.35, elseLOW. - view_rank for change comparison:
UW = −1,N = 0,OW = +1.
- view from signal score:
- Correlation (
POL_CORRELATION_DEFAULT): high threshold 0.8, low threshold 0.2; default review window 2025-05-30 → 2026-04-30. - Credit default (
POL_CREDIT_DEFAULT): duration band [3.0, 5.0] years, issuer concentration limit 12%, max HY 20%, subsector min count for "diversified" = 2, target HY reduction 0.0 pp. - Credit risk reduction (
POL_CREDIT_RISK_REDUCTION): same as above but target HY reduction 4.0 pp. (Risk-reduction portfolios carry this policy_id in their own constraints block.) - Multi-asset risk (
POL_MULTI_ASSET_RISK): committee escalation = "two_or_more_material_exceptions"; uses correlation-default + credit-risk- reduction.
Always prefer the threshold values returned by the live /api/policies and by the
portfolio's own constraints block over any memorized constant.
2. Core computation recipes
Pearson correlation on monthly simple returns
- Pull
/api/index-levels/<id>(a list of{date, level}sorted monthly). - Simple return for month i:
r_i = (level[i+1] - level[i]) / level[i]. - With 12 monthly levels you get 11 returns →
return_observations = 11. - Pearson r between two return series; round to 3 decimals.
- "Highest positive" = max r; "lowest" = min r (can be negative).
- Every
pair_id/pairlists the two index ids in ascending alphabetical order; pair lists across the answer are ordered as the template dictates.
Weighted modified duration / weighted YTM
- Weight each holding's
modified_duration_years(oryield_to_maturity_pct) by itsquantity_usd_m, divide by total market value. Round to 2 dp.
HY allocation %
HY% = (sum of quantity_usd_m where rating_bucket == "HY") / total_MV * 100.hy_reduction_pct_points = pre_HY% − post_HY%(round 2 dp).
View / change / conviction mapping (allocation tasks)
- New view = map current-quarter signal score through the OW/UW/N thresholds.
- Conviction = map |signal score| through HIGH/MEDIUM/LOW thresholds.
change= compare new view rank vs the prior view rank (UP if higher, DOWN if lower, UNCHANGED if equal).rationale_codecomes straight from the macro signal row'srationale_code(do not invent one); it already matches the allowed enum set.
3. The quarter-filtering trap (allocation & committee tasks)
Both /api/macro-signals and /api/allocation/prior-views contain multiple
quarters (e.g. Q2_2026 and Q3_2026 rows for the same opportunity set). This is
the single biggest silent-error source.
- Filter macro signals to
quarter == target_quarter(the request's quarter). - The prior view to compare against is the prior-views row whose
previous_quarter == prior_quarter(itsviewis the view going into the target quarter). Equivalently, the row whosequarter == target_quarterandprevious_quarter == prior_quarter. - Using the wrong-quarter row flips both the view and the rationale_code (e.g. a currency that is Neutral/NEUTRAL_BALANCE this quarter looks UW/DOLLAR_DEFENSIVE next quarter). Always pin the quarter first.
4. Archetype playbooks
(a) Energy-credit trade package
Inputs: /api/portfolios/<id>, /api/instruments/bonds, /api/issuers,
/api/market/energy, /api/policies.
- Eligible buy universe = bonds with
candidate == trueandenergy_linked == trueand whose issuer is not on the watchlist (/api/issuers→watchlist == trueis excluded). Watchlisted energy issuers are typically the E&P, refining, and one telecom name — never buy them. - Honor the requested ticket count and total notional, split as instructed (e.g. two tickets, 8.0 USD m total → 4.0 each). Use different issuers (issuer diversification) and prefer different subsectors (subsector diversification; "diversified" needs ≥ 2 subsectors).
- Compute post-trade
total_market_value_usd_m,hy_allocation_pct,weighted_modified_duration_years,weighted_yield_to_maturity_pctby adding the buys to current holdings (MV = current MV + new notional). - Constraint checks must all hold: HY ≤ 20%, duration inside [3.0, 5.0], selected issuers distinct, selected subsectors distinct, no watchlist issuer.
- Selection bias that matters: for an income/client-facing package, favor a
quality tilt — keep HY comfortably below the cap rather than maxing carry by
stacking HY to ~19%. Pair the dominant macro theme (the energy desk's strongest
positive signal, typically LNG export, score ≈ 0.7) with a defensive
diversifier (IG midstream/"natural gas") so carry improves versus the current
book while staying diversified. Theme/segment enums: pick the theme matching the
strongest signal (e.g.
lng_export_tailwind) and segmentmulti_asset_incomefor an income pitch. data_precedence: if env MV/HY/duration differ from the stale snapshot, setcurrent_environment_over_stale_payload.
(b) International equity correlation review
Inputs: /api/index-levels, /api/policies (correlation thresholds), request's
index universe + window.
- Confirm the level series matches the requested window; compute the 11-return Pearson matrix over the universe.
extreme_pairs.highest_positive/.lowest= max/min r, pair ids alphabetical, 3 dp;return_observations= number of returns (11 for a 12-point window).concentration:high_threshold_breached= any pair |r| (positive cluster) ≥ 0.8.china_asia_dependence_flag= the dedicated China sleeve is highly correlated (≥ 0.8) with the broad Asia/EM indices.primary_code=CHINA_ASIA_DEPENDENCEwhen that cluster is the dominant concentration; otherwiseGLOBAL_DEVELOPED_OVERLAPorNO_MATERIAL_CONCENTRATION.
diversification_candidates= only indices whose correlation to the concentrated core is below the low threshold (0.2) — in practice the Latin-America index (strongly negative). Do not list India or EM-ex-China as diversifiers when they sit > 0.8 against the core; they are not diversifiers.sleeve_actions: use the geographic sleeve name as thesleevevalue (e.g. "China", "Latin America"), not generic labels like "Diversifier" — generic labels lose credit. Typical pair: trim the concentrated China sleeve, add the low-correlation diversifier; order ascending by sleeve;target_index_idfrom the allowed set.
(c) Active allocation view refresh
Inputs: /api/allocation/opportunity-sets, /api/allocation/prior-views,
/api/macro-signals, /api/policies.
- One row per requested opportunity set, ordered exactly as the request's focus list (not alphabetical).
asset_class= the opportunity set'sasset_classfrom/api/allocation/opportunity-sets(Equities / Duration / Credit / Currency).- Apply the quarter filter (Section 3), then the view/change/conviction mapping
and the macro-provided
rationale_code(Section 2). policy_id= the allocation mapping policy id (POL_ALLOCATION_MAPPING).risk_overlay: read the tilt from the new view set. When duration (Treasuries) is OW and credit (HY) is UW, overlay_codeDURATION_QUALITY_TILTwith primary_actiontilt_to_duration_quality;rationale_codesin business-priority order (duration support first, HY valuation risk next). ChooseCREDIT_RISK_REDUCTION/trim_credit_beta,EQUITY_BETA_EXTENSION/add_cyclical_equity_beta,CURRENCY_DEFENSIVE_HEDGE/add_currency_hedge, orNO_OVERLAY/hold_policy_weightswhen the view set points there instead.
(d) Fixed-income risk rebalance
Inputs: /api/portfolios/<id> (use current quantities, not the stale
exception board), /api/instruments/bonds, /api/issuers, /api/policies
(POL_CREDIT_RISK_REDUCTION).
- The portfolio may already breach the HY cap (HY can be ~40% in a mixed-credit book). The rotation must end with HY ≤ 20% and achieve at least the target HY reduction (4.0 pp), keep duration inside [3.0, 5.0], and clear watchlist pressure.
- Sell side: sell the watchlist issuer's bond(s) (mandatory to clear watchlist) plus the minimum extra HY needed to drop under the cap and meet the reduction target — do not zero out HY (preserve carry/mandate).
- Buy side: fund with current eligible IG candidates (
candidate == true,rating_bucket == IG, issuer not on watchlist); never buy a watchlist name even if it has high carry. Prefer the desk's named IG shortlist (data-center, materials, LNG IG). Keep buys ≈ sells so MV is roughly preserved and duration stays in band; the exact notional split per buy is secondary to the right instrument set. - Trade list ordering: SELL before BUY, then
instrument_idascending within each action; quantities to 1 dp. risk_metrics: post-trade HY %, post-trade weighted duration, HY reduction pp, post-trade watchlist exposure (USD m).exception_flags: hy_cap_pass (≤20), duration_band_pass (in [3,5]), target_hy_reduction_met (≥ target pp), watchlist_exposure_cleared (== 0).watchlist_handling:watchlist_sell_ids= sold watchlist instruments (ascending);buys_avoid_watchlist= true.risk_note_code: when a watchlist name is the headline issue being cleared, preferwatchlist_concentrationoverhy_cap_pressure, even if the HY cap is also pressured. Useduration_preservation,carry_tradeoff, orno_actiononly when those are genuinely the dominant story.
(e) Multi-asset committee decision
Inputs: combine (b) correlation on the requested small index set with (c)
allocation views on the requested opportunity sets, plus /api/policies.
correlation_summary(length 2, order [highest_concentration, best_diversifier]): highest_concentration = max-r pair (e.g. the China/EM pair ≈ 0.92); best_diversifier = min-r pair (e.g. China/LatAm ≈ −0.83); pair ids alphabetical, correlation 3 dp.allocation_views(ordered as the request lists the sets): includeprior_view,signal_score(3 dp, from the target-quarter macro row),view,change,conviction,rationale_code— all via Sections 2–3. Reconcile any stale local stance here (e.g. a "USD overweight" worksheet becomes Neutral if the current signal is in the neutral band).target_sleeve_actions(same order): trim the set you downgraded / that drives concentration (EM), hold an unchanged OW (India), add the diversifier you upgraded (LatAm), monitor/hedge/trim the currency you reduced (USD).rebalance_trigger: when the top correlation exceeds the high threshold (0.8), usecorrelation_cap_breach.portfolio_risk_concentration_flag: true when the China/EM concentration breaches the high threshold.next_step: with one material exception flagged (concentration breach) but a coherent rotation in hand, preferapprove_with_monitoring. Usedefer_pending_risk_reviewonly when escalation is genuinely triggered (two or more material exceptions),reject_constraint_breachfor a hard breach, andapprove_rotationonly when there is no concentration flag at all.
5. Pre-submit checklist
- Every required top-level key present; no extra keys.
- Enums spelled exactly from the allowed set; booleans are real booleans.
- All numbers rounded to the field's declared precision (correlations 3 dp, signal scores 3 dp, pct/years 2 dp, notional/quantity 1 dp).
- Lists in the declared order (focus-list order, alphabetical ids, SELL-before-BUY, pair role order); pair member ids alphabetical.
- Metrics computed from current environment holdings, not from stale payload marks; data_precedence / lineage reflects the reconciliation.
- Constraint/exception flags computed from the actual post-trade numbers and the live policy thresholds, not asserted.