/* pearl_6_kelly_update — SP5 Task A6 cross-fold-persistent Kelly cap signals.
 *
 * CROSS-FOLD-PERSISTENT — ISV[280..286) are EXEMPT from the fold-reset registry.
 * The cold-start Quarter pathology (project_magnitude_eval_collapse_kelly_capped.md)
 * is caused by portfolio_state having WindowReset lifecycle: every window boundary
 * resets win/loss counts to zero, which forces every new window to cold-start at
 * low maturity → Kelly cap clamps actual_mag to Quarter for the first ~10 trades.
 * Pearl 6's job is to maintain a cross-fold and cross-window representation of
 * Kelly statistics so the maturity-driven cold-start cap does not re-engage.
 *
 * Investigation result (Task A6): portfolio_state has ResetCategory::WindowReset
 * in state_reset_registry.rs (not FoldReset). This means ps[14..18] (Kelly stats)
 * reset at EVERY window boundary — more frequently than fold boundaries.
 * The max() semantics for sample_count are therefore essential for both
 * fold-level and window-level cold-start suppression.
 *
 * DOES NOT use apply_pearls_ad_kernel — two reasons:
 *   1. Pearls A+D's sentinel-bootstrap (zero ISV → first-observation replacement)
 *      is INCOMPATIBLE with cross-fold persistence. If these slots were in the
 *      fold-reset registry, the sentinel would fire at every fold boundary and
 *      replace the cross-fold state — defeating the entire design.
 *   2. The wiener_state_buf is sized for the 110 SP5 producers at slots [174..278)
 *      (below the 2-slot carve-out gap at [278..280)). Pearl 6 slots [280..286)
 *      would map to wiener offsets 213 + (280-174)*3 = 525..543 under the naive
 *      formula — at the far edge of the buffer (wiener_state_buf = 543 floats).
 *      Slots 281..285 would map to offsets 528..543 — within the allocated range
 *      but would be atomically zeroed at fold boundary (part of wiener_state_buf),
 *      which is incompatible with cross-fold persistence. We intentionally leave
 *      wiener offsets [525..543) unused; Pearl 6 does not read or write them.
 *
 * Instead this kernel does its own slow in-kernel EWMA (alpha=0.01, Invariant 1
 * anchor — slow rate preserves cross-fold inertia) for 5 of 6 output slots.
 * Sample count (s==3) uses cumulative-via-max() semantics: at any window/fold
 * boundary portfolio_state ps[14..18] reset so new_obs = total_trades drops; the
 * max(current_isv, new_obs) call keeps the cumulative count non-decreasing.
 *
 * Bayesian priors match kelly_cap_update_kernel.cu exactly (Invariant 1 anchors):
 *   prior_wins=2.0, prior_losses=2.0,
 *   prior_sum_wins=0.01, prior_sum_losses=0.01
 *
 * Conviction_smooth slot (s==1) has new_obs = current: the conviction signal is not
 * derivable from portfolio_state alone. This is an intentional no-op update until
 * Layer B wires the magnitude_conviction_buf source. The EWMA blend with new_obs ==
 * current yields new_isv = current (identity) — the slot holds its cross-fold value.
 *
 * 6-thread single-block (one thread per output slot). No atomicAdd.
 * __threadfence_system() after writes ensures visibility across device contexts.
 */
#include "state_layout.cuh"

extern "C" __global__ void pearl_6_kelly_update(
    const float* __restrict__ portfolio_state,   /* [n_envs * ps_stride] */
    int   n_envs,
    int   ps_stride,
    float* __restrict__ isv_signals,             /* direct ISV write — cross-fold-persistent */
    int   kelly_f_idx,                           /* ISV slot 280 */
    int   conviction_idx,                        /* ISV slot 281 */
    int   trade_var_idx,                         /* ISV slot 282 */
    int   sample_count_idx,                      /* ISV slot 283 */
    int   win_rate_idx,                          /* ISV slot 284 */
    int   loss_rate_idx                          /* ISV slot 285 */
) {
    if (blockIdx.x != 0 || threadIdx.x >= 6) return;

    int s = (int)threadIdx.x;

    /* Bayesian priors — match kelly_cap_update_kernel.cu exactly (Invariant 1 anchors). */
    const float PRIOR_WINS       = 2.0f;
    const float PRIOR_LOSSES     = 2.0f;
    const float PRIOR_SUM_WINS   = 0.01f;
    const float PRIOR_SUM_LOSSES = 0.01f;

    /* Aggregate across all envs. Each thread re-reads independently.
     * 6 threads × n_envs reads: acceptable for cold-path (per-epoch). */
    float total_wins   = 0.0f;
    float total_losses = 0.0f;
    float sum_wins     = 0.0f;
    float sum_losses   = 0.0f;
    float kelly_sum    = 0.0f;
    int   kelly_count  = 0;

    for (int e = 0; e < n_envs; ++e) {
        const float* ps = portfolio_state + (long long)e * ps_stride;
        float wc = ps[PS_KELLY_WIN_COUNT];
        float lc = ps[PS_KELLY_LOSS_COUNT];
        float sw = ps[PS_KELLY_SUM_WINS];
        float sl = ps[PS_KELLY_SUM_LOSSES];

        total_wins   += wc;
        total_losses += lc;
        sum_wins     += sw;
        sum_losses   += sl;

        /* Per-env Bayesian-prior Kelly fraction — mirrors kelly_cap_update_kernel.cu exactly.
         * Note: this kernel does NOT apply the half-Kelly factor (×0.5 for risk management)
         * that kelly_cap_update applies. Pearl 6 tracks the raw statistical Kelly fraction
         * as a signal; kelly_cap_update_kernel continues to own the capped, health-coupled
         * ISV[47] slot consumed by position sizing. */
        float eff_w = wc + PRIOR_WINS;
        float eff_l = lc + PRIOR_LOSSES;
        float wr    = eff_w / (eff_w + eff_l);
        float aw    = (sw + PRIOR_SUM_WINS)   / eff_w;
        float al    = (sl + PRIOR_SUM_LOSSES) / eff_l;
        float pf    = aw / fmaxf(al, 0.0001f);
        float kf    = (pf * wr - (1.0f - wr)) / fmaxf(pf, 0.0001f);
        kf          = fmaxf(0.0f, fminf(1.0f, kf));
        kelly_sum  += kf;
        kelly_count++;
    }

    float total_trades = total_wins + total_losses;

    /* Resolve ISV slot index for this thread. */
    int idx_table[6] = {
        kelly_f_idx, conviction_idx, trade_var_idx,
        sample_count_idx, win_rate_idx, loss_rate_idx
    };
    int idx     = idx_table[s];
    float current = isv_signals[idx];

    float new_obs;
    if (s == 0) {
        /* kelly_f_smooth: mean raw Kelly fraction across envs (not half-Kelly). */
        new_obs = (kelly_count > 0)
            ? (kelly_sum / (float)kelly_count)
            : 0.5f;

    } else if (s == 1) {
        /* conviction_smooth: not derivable from portfolio_state.
         * Intentional identity update — Layer B replaces this with the actual
         * magnitude_conviction_buf source. The EWMA blend of (new_obs == current)
         * is an identity: new_isv = (1-a)*current + a*current = current. */
        new_obs = current;

    } else if (s == 2) {
        /* trade_var_smooth: variance of per-env Kelly fractions across envs
         * (measures cross-env PnL dispersion). Computed as the second moment
         * `ksum_sq / kelly_count` — i.e. variance, NOT standard deviation;
         * matches the slot name TRADE_VAR_SMOOTH_INDEX.
         * Second pass over envs: same Bayesian-prior formula, accumulate sq-dev. */
        float kelly_mean = (kelly_count > 0)
            ? (kelly_sum / (float)kelly_count)
            : 0.5f;
        float ksum_sq = 0.0f;
        for (int e = 0; e < n_envs; ++e) {
            const float* ps = portfolio_state + (long long)e * ps_stride;
            float wc = ps[PS_KELLY_WIN_COUNT];
            float lc = ps[PS_KELLY_LOSS_COUNT];
            float sw = ps[PS_KELLY_SUM_WINS];
            float sl = ps[PS_KELLY_SUM_LOSSES];
            float eff_w = wc + PRIOR_WINS;
            float eff_l = lc + PRIOR_LOSSES;
            float wr  = eff_w / (eff_w + eff_l);
            float aw  = (sw + PRIOR_SUM_WINS)   / eff_w;
            float al  = (sl + PRIOR_SUM_LOSSES) / eff_l;
            float pf  = aw / fmaxf(al, 0.0001f);
            float kf  = (pf * wr - (1.0f - wr)) / fmaxf(pf, 0.0001f);
            kf = fmaxf(0.0f, fminf(1.0f, kf));
            float d = kf - kelly_mean;
            ksum_sq += d * d;
        }
        new_obs = (kelly_count > 1)
            ? (ksum_sq / (float)kelly_count)
            : 0.0f;

    } else if (s == 3) {
        /* kelly_sample_count: CROSS-FOLD-PERSISTENT cumulative trade count.
         * portfolio_state resets at window boundaries (WindowReset lifecycle),
         * so total_trades drops back to a small number after every window.
         * max(current, new_obs) keeps the cumulative count non-decreasing:
         *   - within a window: total_trades grows → max grows with it
         *   - at window/fold boundary: total_trades drops → max keeps old value
         *   - after cold-start re-accumulates past old max: max grows again
         * This is the primary mechanism breaking the cold-start Kelly cap pathology. */
        new_obs = total_trades;

    } else if (s == 4) {
        /* win_rate_smooth: aggregate win rate across all envs. */
        new_obs = (total_trades > 0.0f)
            ? (total_wins / total_trades)
            : 0.5f;

    } else {
        /* s == 5: loss_rate_smooth: aggregate loss rate across all envs. */
        new_obs = (total_trades > 0.0f)
            ? (total_losses / total_trades)
            : 0.5f;
    }

    /* Update ISV with cross-fold-aware blending.
     *
     * KELLY_EMA_ALPHA = 0.01 — Invariant 1 anchor (slow rate). A rate of 0.01
     * means the effective window is ~100 steps; this is intentionally slow to
     * preserve cross-fold inertia so a single bad window doesn't collapse the
     * long-run Kelly estimate. The kelly_cap_update kernel (ISV[47]) continues
     * to provide the health-coupled, half-Kelly-capped per-epoch estimate for
     * position sizing; Pearl 6 tracks the longer-horizon statistical picture. */
    const float KELLY_EMA_ALPHA = 0.01f;

    float new_isv;
    if (s == 3) {
        /* Sample count: cumulative-via-max() — never decreases across windows/folds. */
        new_isv = fmaxf(current, new_obs);
    } else {
        /* Slow EWMA: blend current cross-fold value with new per-epoch observation. */
        new_isv = (1.0f - KELLY_EMA_ALPHA) * current + KELLY_EMA_ALPHA * new_obs;
    }

    isv_signals[idx] = new_isv;
    __threadfence_system();
}
