Scheduler

The Laravel scheduler is the time-driven entry point to Kraite's workload. Every recurring job — kline fetches, indicator computation, listenKey refreshes, balance snapshots, candle purges, audit-log retention — flows through routes/console.php on ingestion.kraite.com (running on Athena). The scheduler does not execute trading logic itself; it dispatches into the dispatch daemon and the Horizon queues, which do the work.

This is the subsystem lens view. For the persistent process that replaced one specific scheduled command (steps:dispatch), see Dispatch daemon.


The cron table

CommandCadenceCooldown-gatedPurpose
steps:recover-stale1 minnoFlip stuck Running steps back to Pending after timeout + 60s
kraite:cron-sync-orders5 minnoPolling fallback for the user-data WS daemon
kraite:cron-refresh-binance-listen-keys1 minnoKeep Binance listenKeys alive past 60-min auto-expiry
kraite:cron-create-positions3 minyesOpen new positions
kraite:cron-fetch-klines --only-active-positions5 minyesRefresh klines for tokens with open positions
kraite:cron-fetch-klines --timeframe=1hhourly :05yes1h-bar refresh for the full tradeable set
kraite:cron-fetch-klines --timeframe=4hevery 4h :05yes4h-bar refresh
kraite:cron-fetch-klines --timeframe=12hevery 12h :05yes12h-bar refresh
kraite:cron-store-accounts-balances5 minyesSnapshot account balances per exchange
kraite:cron-refresh-exchange-symbolshourly :15yesPer-symbol leverage-bracket fan-out
kraite:cron-conclude-symbols-directionhourly :30yesTAAPI indicator → direction
kraite:disable-volatile-tokenshourly :45yesSweep volatile / structurally-brittle tokens
kraite:purge-candlesdaily 03:00yesRetention sweep on candle data
kraite:purge-model-logs --duration=30daily 03:30yes30-day rolling audit-log window
steps:archive --duration=1daily 04:00yesArchive completed step rows past 1 day

What "cooldown-gated" means

Every cron-bound command in Kraite extends a base class that wraps execution with a per-command cooldown — a "do not re-fire if already running OR if the prior run finished less than cooldown_seconds ago" guard. The guard is enforced via a Redis lock, not a DB row, so a stuck previous tick never blocks scheduling. Non-gated commands (the four no rows above) are short, idempotent, and safe to overlap.

                tick fires


            ┌──────────────────┐
            │ Cooldown active? │── yes ─► skip
            └────────┬─────────┘
                    no

            ┌──────────────────┐
            │  acquire Redis   │── fail ► skip (already running)
            │      lock        │
            └────────┬─────────┘
                    ok

              run command


           release lock + stamp
              cooldown_until

Why a daemon replaced steps:dispatch

Architectural decision

The pre-daemon design ran steps:dispatch every second across 10 step-dispatcher groups — 10 forks per second, plus the per-tick scheduler fork itself. At Kraite's growth that became the dominant CPU cost on Athena. The dispatch daemon collapses all 10 group ticks into a single long-lived loop with a 1 s sleep between ticks, dropping load average from 105 to 0.68. The scheduler still owns every other recurring command — only the high-frequency dispatch concern was lifted out.