streak-api 0.1.4

API for interacting with the STREAK directional markets protocol on Solana
Documentation
//! Per-player stats and ticket balance (`ledger` PDA).
//!
//! ## Lifetime stats (never reset)
//! - [`Ledger::total_wins`]: total rounds where the player was on the winning side.
//! - [`Ledger::total_bets`]: total ticket-debit events (PlaceBet calls that spend tickets).
//! - [`Ledger::peak_win_streak`]: all-time longest consecutive win run on a single series.
//!
//! ## Running streak (breaks on loss)
//! - [`Ledger::win_streak`]: consecutive wins on [`last_win_series_id`] with `period` stepping
//!   by `+1`. Incremented by `ExecutorTreasury` (distribute) on each confirmed win; reset to 0
//!   by `PlaceBet` when the previous-period position is finalized as a loss.
//!
//! ## Weekly stats (lazy reset keyed by [`Ledger::week_number`] vs [`Treasury::current_week`])
//! - [`Ledger::week_wins`]: wins in the current executor-defined week.
//! - [`Ledger::week_bets`]: bets placed in the current week.
//! - [`Ledger::week_peak_streak`]: longest streak achieved this week.
//! - [`Ledger::week_number`]: the [`Treasury::current_week`] value when weekly stats were last
//!   reset. On any instruction that touches weekly fields, call [`Ledger::maybe_reset_weekly`]
//!   first so stale weekly data auto-clears.

use steel::*;

use super::{ledger_pda, StreakAccount};

#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Ledger {
    pub authority: Pubkey,

    // ── ticket balance ────────────────────────────────────────────────────
    /// Whole tickets credited on `BuyTickets`; debited on `PlaceBet`.
    pub tickets: u64,
    /// Post-team-cut USDC micros mirroring `tickets` (1 ticket = `TICKET_MICROS` µ).
    pub unstaked_micros: u64,

    // ── running streak ────────────────────────────────────────────────────
    /// Consecutive wins on [`last_win_series_id`] with `period + 1` steps.
    pub win_streak: u64,
    /// Last `Market::period` confirmed as a win (via `ExecutorTreasury` distribute). `0` = none.
    pub last_win_period: u64,
    /// `series_id` for [`last_win_period`] (streak concurrency key).
    pub last_win_series_id: u16,
    pub _pad0: [u8; 6],

    // ── lifetime stats ────────────────────────────────────────────────────
    /// Total rounds on the winning side (all-time correct calls).
    pub total_wins: u64,
    /// Total ticket-debit events across all `PlaceBet` calls (each stake spend = +1).
    pub total_bets: u64,
    /// All-time highest `win_streak` ever recorded for this player.
    pub peak_win_streak: u64,

    // ── weekly stats (reset lazily when week_number != Treasury::current_week) ──
    /// Wins in the current executor week.
    pub week_wins: u64,
    /// Bets placed in the current executor week.
    pub week_bets: u64,
    /// Longest streak achieved in the current executor week.
    pub week_peak_streak: u64,
    /// Which executor week these `week_*` stats belong to. Compared against
    /// `Treasury::current_week`; if stale, zero weekly fields and update this.
    pub week_number: u64,
}

impl Ledger {
    pub fn pda(authority: Pubkey) -> (Pubkey, u8) {
        ledger_pda(authority)
    }

    /// Reset weekly counters if the executor has advanced [`Treasury::current_week`].
    ///
    /// Call at the top of any code path that reads or writes weekly stats, passing the
    /// value read from `Treasury::current_week`.
    #[inline(always)]
    pub fn maybe_reset_weekly(&mut self, current_week: u64) {
        if self.week_number != current_week {
            self.week_number = current_week;
            self.week_wins = 0;
            self.week_bets = 0;
            self.week_peak_streak = 0;
        }
    }

    /// Record a confirmed win for this player. Called from `ExecutorTreasury` (distribute).
    ///
    /// Updates streak, peak streak, total wins, and weekly equivalents.
    /// `current_week` must be read from `Treasury::current_week` before calling.
    #[inline(always)]
    pub fn record_win(&mut self, market_period: u64, series_id: u16, current_week: u64) {
        self.maybe_reset_weekly(current_week);

        // Advance running streak.
        if self.last_win_period == 0 {
            self.win_streak = 1;
        } else if market_period == self.last_win_period.saturating_add(1)
            && self.last_win_series_id == series_id
        {
            self.win_streak = self.win_streak.saturating_add(1);
        } else {
            self.win_streak = 1;
        }
        self.last_win_period = market_period;
        self.last_win_series_id = series_id;

        // Update peaks.
        if self.win_streak > self.peak_win_streak {
            self.peak_win_streak = self.win_streak;
        }
        if self.win_streak > self.week_peak_streak {
            self.week_peak_streak = self.win_streak;
        }

        // Lifetime + weekly counters.
        self.total_wins = self.total_wins.saturating_add(1);
        self.week_wins = self.week_wins.saturating_add(1);
    }

    /// Record a loss (streak break). Called from `PlaceBet` when the previous period is settled
    /// as a loss for this player.
    ///
    /// `current_week` must be read from `Treasury::current_week` before calling.
    #[inline(always)]
    pub fn record_loss(&mut self, current_week: u64) {
        self.maybe_reset_weekly(current_week);
        // streak breaks; weekly/lifetime peak already captured at last win — keep them
        self.win_streak = 0;
        self.last_win_period = 0;
        self.last_win_series_id = 0;
    }

    /// Record a bet (ticket debit). Called from `PlaceBet` whenever tickets are actually spent.
    ///
    /// `current_week` must be read from `Treasury::current_week` before calling.
    #[inline(always)]
    pub fn record_bet(&mut self, current_week: u64) {
        self.maybe_reset_weekly(current_week);
        self.total_bets = self.total_bets.saturating_add(1);
        self.week_bets = self.week_bets.saturating_add(1);
    }
}

account!(StreakAccount, Ledger);