⏱️ AI Export Technical Sampling
AI Export reduces historical numeric density deterministically while preserving scope, indicators, outputs, summaries, latest values, and selected events.
📐 Rational Bucket Formula
Let:
- \(x\): whole-calendar-day distance backward from
snapshot_as_of; - \(P\): shape exponent;
- \(M\): half-transition offset after the seven-day daily ramp;
- \(K\): asymptotic maximum bucket width in calendar days;
- \(T\): total requested inclusive calendar-day count.
The continuous width is:
The integer width is:
Boundaries are generated iteratively:
Each offset interval becomes one bucket when translated back from
snapshot_as_of. Runtime arithmetic uses Decimal with explicit
ROUND_HALF_UP, not binary floating point or banker's rounding.
The result is deterministic, positive, monotonic in historical width, bounded by \(K\), gap-free, overlap-free, and clipped exactly to \(T\).
📅 Recent Daily Window
For every detail level and temporal class:
The most recent seven calendar days therefore remain daily. This is calendar sampling, not market-session sampling. Empty intervals remain in bucket plans and diagnostics. Indicator payloads publish source/non-empty counts but omit empty rows; the public renderer also omits completely empty financial temporal rows. Neither path invents observations or converts absence to zero.
🧮 Full Policy Matrix
Reference counts use exact inclusive periods of 90, 180, and 365 calendar days.
| Detail | Temporal class | P | M | K | 90d | 180d | 365d |
|---|---|---|---|---|---|---|---|
| Compact | Very Fast | 2 | 30 | 30 | 20 | 23 | 29 |
| Compact | Fast | 2 | 25 | 35 | 18 | 21 | 26 |
| Compact | Medium Fast | 2 | 20 | 42 | 16 | 18 | 23 |
| Compact | Medium | 2 | 10 | 42 | 14 | 16 | 20 |
| Compact | Slow | 2 | 5 | 49 | 12 | 14 | 17 |
| Compact | Very Slow | 2 | 5 | 84 | 11 | 12 | 14 |
| Standard | Very Fast | 2 | 30 | 14 | 26 | 33 | 46 |
| Standard | Fast | 2 | 21 | 15 | 23 | 29 | 41 |
| Standard | Medium Fast | 2 | 20 | 17 | 21 | 26 | 37 |
| Standard | Medium | 2 | 15 | 20 | 18 | 23 | 32 |
| Standard | Slow | 2 | 10 | 22 | 16 | 20 | 28 |
| Standard | Very Slow | 2 | 5 | 28 | 13 | 16 | 23 |
| Full | Very Fast | 2 | 30 | 7 | 35 | 49 | 75 |
| Full | Fast | 2 | 28 | 8 | 32 | 44 | 67 |
| Full | Medium Fast | 2 | 23 | 9 | 28 | 38 | 59 |
| Full | Medium | 2 | 16 | 10 | 24 | 33 | 51 |
| Full | Slow | 2 | 10 | 11 | 21 | 29 | 46 |
| Full | Very Slow | 2 | 9 | 14 | 18 | 24 | 38 |
Within one detail level, density decreases from Very Fast to Very Slow. For one temporal class, Full is at least as dense as Standard, which is at least as dense as Compact. Very Fast is the original detail-only baseline.
💹 Price and Rate Policy
Asset prices, FX rates, and equivalent reference series use detail only:
| Detail | P | M | K |
|---|---|---|---|
| Compact | 2 | 30 | 30 |
| Standard | 2 | 30 | 14 |
| Full | 2 | 30 | 7 |
Signal temporal classes never change reference price/rate density.
📈 Indicator Policy
Indicator history uses:
AI Export does not branch on signal codes. The plugin resolves one temporal class from normalized parameters; central policy resolves the matrix row.
🗂️ Initial Instance Mapping
The Asset bundle has 20 instances. Portfolio and Broker reuse it per eligible asset. FX uses the marked 12-instance subset.
| Instance | Signal | Temporal class | In FX |
|---|---|---|---|
ema_20 |
EMA(20) | Medium | ✓ |
ema_50 |
EMA(50) | Slow | ✓ |
ema_200 |
EMA(200) | Very Slow | ✓ |
sma_50 |
SMA(50) | Slow | ✓ |
sma_200 |
SMA(200) | Very Slow | ✓ |
kama_20 |
KAMA(20) | Medium | ✓ |
aroon_25 |
Aroon(25) | Medium | |
adx_14 |
ADX(14) | Medium | |
donchian_20 |
Donchian(20) | Medium Fast | |
rsi_14 |
RSI(14) | Very Fast | ✓ |
macd_12_26_9 |
MACD(12,26,9) | Medium Fast | ✓ |
ppo_12_26_9 |
PPO(12,26,9) | Medium Fast | ✓ |
roc_20 |
ROC(20) | Fast | ✓ |
stoch_rsi_14_3 |
StochRSI(14,3) | Very Fast | ✓ |
cci_20 |
CCI(20) | Fast | |
bollinger_20_2 |
Bollinger(20,2) | Medium Fast | ✓ |
atr_14 |
ATR(14) | Fast | |
natr_14 |
NATR(14) | Fast | |
mfi_14 |
MFI(14) | Very Fast | |
obv |
OBV | Medium |
EMA and SMA use exact parameter-match rules because one plugin produces multiple time horizons. Other listed plugins use one fixed class for their current curated instances. Unknown or ambiguous matches raise; official plugins do not silently fall back.
🧺 Multi-output Indicators
MACD, PPO, Bollinger, Donchian, Aroon, StochRSI, and ADX produce multiple outputs. Every output of one instance shares the same bucket boundaries. Scalar outputs and band components become columns in one row-oriented table.
For each populated cell:
- one observation uses compact
value+ realdate; - multiple observations preserve count, first, min, max, last, and their real observation dates.
period_summary is calculated across the entire exported period, independent of
bucket density. Each output column also carries its latest observed value/date.
📦 Public Indicator History Limits
The backend applies one additional deterministic presentation limit to non-empty indicator buckets for each entity and Signal instance:
| Detail | Non-empty rows |
|---|---|
| Compact | 5 |
| Standard | 10 |
| Full | All |
Compact and Standard select rows uniformly across the full exported period,
including the first and last non-empty rows. This does not change the Signal set,
Asset scope, output columns, latest values, or full-period summary. The public
manifest exposes indicator_history_row_limit; each indicator payload retains
source bucket and source non-empty row counts.
The compact text renderer uses row-relative date references inside history cells:
s is the row start, e is the row end, and +N is a calendar-day offset from
the row start. Event values are positional only when their definition declares the
exact value_fields order. These are lossless text encodings, not additional
financial calculations.
🔥 Calculation Range, Exported Range, and Warm-up
Indicators calculate on sessions — the quote days of a warm-up-inclusive
input — with plugin-owned, parameter-aware warm-up. The loaded series keeps one
point per calendar day. A day filled with an earlier price or rate carries
backward_fill_info, and an Asset's stored weekend or market-holiday row that
repeats the previous close exactly is marked as carried before the signals run
(mark_market_closed_carries, the rule the Risk series use). For every plugin
with computes_on_quote_days — the 17 technical indicators, so every curated
instance — SignalService keeps only the quote days of the selected points
(quote_day_points): points whose price was quoted on their own date, even when
their exchange rate was carried. Periods, warm-up, minimum history, and visible
points therefore count sessions, and indicator values are dated on sessions.
States and annotations are also derived before numeric bucket aggregation.
Only the requested exported period is serialized:
warm-up-inclusive calendar input
→ keep its sessions (quote days)
→ full session-level indicator and event calculation
→ slice to exported period
→ aggregate numeric history
The load window is counted in calendar days:
SignalExecutionPlan.max_history_days_before_visible is the largest warm-up of
the requested instances, in sessions, times SESSION_WARMUP_DAY_MULTIPLIER (two
calendar days per session). Asset loading delegates warm-up to SignalService
through AssetSourceManager, which loads that window before the requested
start. FX sizes its own load of earlier daily rates with the same plan value
(_fx_warmup_days); a day without a published rate is backward-filled, carries
backward_fill_info, and is not a session. The component response declares
warmup_policy: component_owned; the current aggregate calculation_range and
earliest_calculation_date metadata fields are not populated.
FX source history may begin after the requested start. Dates before the first
stored rate are omitted from the warm-up input rather than failing the entire
snapshot. SignalService then includes calculable ok/partial instances and
reports unavailable instances through technical coverage. Coverage stays on the
calendar although indicators count sessions: the snapshot metadata publishes
requested/available ranges and calendar-day coverage, with the counts of observed
and backward-filled days. No future rate is ever used.
🎯 Event Selection
Events are validated, observed-only filtered, epsilon checked, gap checked, and semantically deduplicated before selection. They are detected from original observations, never from buckets.
Policy is independent for every:
For Portfolio and Broker, entity_id retains the originating Asset identity before
events are merged. FX uses the canonical pair identity.
The complete recent window and minimum latest count are detail-owned:
| Detail | Complete recent window | Minimum latest events |
|---|---|---|
| Compact | 7 calendar days | 3 |
| Standard | 21 calendar days | 10 |
| Full | 30 calendar days | 20 |
For each group:
- sort newest to oldest;
- include every event satisfying the detail-owned inclusive recent boundary;
- if fewer than the detail-owned minimum are included, continue backward to that minimum;
- if fewer events exist, include all;
- restore deterministic public chronological order after selection.
This is not a hard cap. A volatile annotation may export more than the minimum when more events occur inside the complete recent window. There is no ranking, relevance score, family quota, or episode consolidation.
Each group exports a selection summary with detected/recent/exported counts, selection status, oldest/newest detected and exported dates, and optional upward/downward counts.
🧭 Focused Context Policies
The complete technical datasets and explicitly technical analyses continue to use the global policy above unchanged.
Semantic Composition V2 also exposes separate focused context components for financial analyses. Their backend-declared policies are intentionally narrower and do not alter global Compact/Standard/Full:
- Portfolio/Broker general snapshots expose current/summary market fields, a 6/12/24-row observed-only uniform path per eligible Asset, and the latest selected structural event per category;
- multi-Asset comparisons expose a bounded set of latest structural event types;
- Portfolio Description uses an aggregate 30-day event digest, with the latest prior event when a type has no recent occurrence;
- Asset Position Context exposes 8/16/30 uniform observed rows;
- FX Market Context exposes 8/16/30 uniform genuine-observation rows;
- Portfolio/Broker aggregate NAV paths expose 8/16/30 uniform rows. NAV values are explicitly flow-inclusive; normalized return uses historical TWRR;
- focused rows carry bucket boundaries, the real representative observation, observation count, normalized index, and return from the first observation. Bucket extrema are omitted because the per-entity summary already carries full-period extrema;
- Drawdown context sections carry no numeric history at all: they reuse the Risk
drawdown_summarycurrent/maximum episode dates and coverage verbatim; - PAC's per-Asset Drawdown snapshot is one observed-price row per Asset and also carries no Drawdown history;
- technical coverage names current Assets excluded from technical eligibility
and publishes a deterministic reason such as
end_value_unavailable; - every detailed indicator block publishes its own
result_statusandpartial_reason_code; - task-specific evidence datasets apply component-local monotonic detail (for
example income Compact aggregates by
(month, asset, income_type), Standard adds bounded recent dated rows, Full exposes every dated row).
These limits are semantic component contracts, not token-triggered truncation. They do not change the Signal or Asset scope; Full retains every non-empty indicator bucket and the complete 30-day/minimum-20 event policy.
🧹 Public Empty Temporal Rows
The generic public renderer applies the same no-empty-row principle to financial temporal tables that technical histories already use.
A row is omitted only when it contains nominal start/end/index metadata and no observation, flow, variation, P&L, extrema, reconciliation, meaningful observed date, or explicit non-absence status. Numeric zero remains meaningful and keeps the row.
This presentation rule does not modify:
- backend bucket boundaries;
- calculations or requested frequency;
- sampling policy;
- requested/effective/available periods;
- coverage or insufficient-history warnings.
Probe diagnostics count empty rows detected/omitted and temporal rows remaining.
🏷️ Latest-per-category Context Events
Focused market-context components select at most one latest event per
(entity_id, signal_category). The category is plugin-owned (read from the
Signal plugin's category, failing loudly for unknown plugins); candidates are
the allowlist-filtered observation-level discrete events. Tie-breaks within a
category are deterministic by (date, annotation key, signal code), and
categories with no eligible event are omitted rather than emitted as null rows.
This is distinct from the technical event policy above, whose recent window and
minimum count depend on detail. The permanent density probe
therefore tracks these event families separately: detailed_event_rows
(complete-policy events), context_event_rows, latest_event_rows with
latest_event_category_count, and the Portfolio Description
event_digest_group_count / event_digest_underlying_event_count.
💱 FX Observed Range Position
FX conversion-timing evidence frames the current rate against the observed period
minimum/maximum as a plain range position (current - min) / (max - min),
deliberately not a historical percentile or rank and not a forecast. A
flat range (observed_maximum == observed_minimum) or an empty period reports
range_position_ratio = None with an explicit reason
(flat_observed_range / no_observed_rates_in_period). Partial source history is
explicit through is_partial_history plus a partial_history_reason
(source_history_starts_after_period_start /
no_genuine_observations_in_period); only genuine, non-backfilled observations
count and no future rate is ever consulted.
The same timing component (fx.timing_context) also publishes trailing 30- and
91-day returns, the period return, and daily-return volatility, all from genuine
observations inside the AI period. A trailing return is null when the observed
history does not reach back to its anchor date.
🧾 Manifest Examples
The public prompt manifest exposes only information that helps interpret the sampled data:
technical_sampling:
detail_level: standard
indicator_history_row_limit: 10
price_policy:
bucket_count: 46
indicator_policies:
- signal_instance_id: ema_200
signal_code: EMA
temporal_class: very_slow
bucket_count: 23
detail_level appears once because every policy in one request shares it.
temporal_class explains the indicator horizon, while bucket_count reports
the backend aggregation density. indicator_history_row_limit reports the
additional non-empty rows retained per entity/instance in the public payload.
P, M, and K remain normative internal policy parameters. They stay in the
matrix, formula tests, mathematical probes, and engineering reports above, but
are intentionally absent from the API response and copied prompt.
Top-level event policy audit:
event_selection:
minimum_latest_events_per_annotation: 10
complete_recent_window_days: 21
grouped_by:
- entity_id
- annotation_key
Per-group payload summary:
entity_id: asset:42
annotation_key: ema_50_ema_200
detected_count: 37
recent_window_count: 6
exported_count: 6
selection_applied: true
oldest_detected_event_date: 2024-02-01
newest_detected_event_date: 2026-07-29
oldest_exported_event_date: 2026-07-18
newest_exported_event_date: 2026-07-29
🧪 Extension and Testing Checklist
When adding or changing an exportable indicator:
- declare plugin-owned
ai_export_temporal_rules; - use fixed rules only for one semantic horizon;
- use explicit parameter matching for multi-horizon plugins;
- verify exactly one rule resolves for every curated instance;
- keep warm-up, input validation, output validation, AI descriptions, states, and annotations plugin-owned;
- verify all outputs share one grid;
- test all relevant detail/class matrix rows and 90/180/365 counts;
- test daily first seven days, monotonic widths, \(K\) bound, exact final boundary, no gap, and no overlap;
- test unavailable/failed omission without sibling loss;
- test detail-owned event boundary inclusivity/minimums, unlimited events inside the complete recent window, grouping, deduplication, and order;
- inspect
technical_sampling,event_selection, and selection summaries; - run documentation validation.
./dev.py test services ai-export
./dev.py test services signal-plugin-matrix
./dev.py mkdocs build
./dev.py mkdocs check-links