daemon

package
v2.8.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 50 Imported by: 0

Documentation

Overview

Package daemon implements Canary's long-running runtime authority. It owns broker connectivity, durable and in-memory runtime state, background schedulers, policy execution, and the gated coordination of broker writes; other processes access those capabilities through typed daemon requests.

Index

Constants

View Source
const (
	StreakKeyVIXTerm   = "vix_term"
	StreakKeyVolOfVol  = "vol_of_vol"
	StreakKeyHYGSPY    = "hyg_spy"
	StreakKeyCredit    = "credit_spreads"
	StreakKeyFunding   = "funding_stress"
	StreakKeyUSDJPY    = "usdjpy"
	StreakKeyGammaZero = "gamma_zero"
	StreakKeyBreadth   = "breadth"
)

Indicator keys for the streak store. Stable strings; each maps to one regime row. Constants here so a typo at a call site fails at compile time rather than silently writing to a misnamed key.

View Source
const HYGLookbackDays = 90

HYGLookbackDays is the calendar-day window passed to the HMDS history fetch when computing HYG's 50-day SMA. 50 trading days ≈ 70 calendar days when the window has zero holidays; the US market closes 9-10 days per year, so a 70-day window can come up short on the wrong side of Memorial Day / Labor Day / Thanksgiving. 90 calendar days gives ~10 days of slack — the IBKR HMDS API only bills the call, not the bar count, so this is free. Widened from 70 to 90 in v0.23.0 (commit 02aba13).

View Source
const (

	// MinLegCoverageFraction is the persist-or-not threshold: a
	// compute whose successful-leg fraction falls below this is
	// surfaced as an error (not a warning-flagged result), so the
	// existing gammaErrorRetryTTL machinery in gamma_zero_cache
	// re-attempts on the next call within the same NY trading
	// session. Mirrors breadth's MinCoverageFraction = 0.80 pattern
	// at internal/breadth/spx/types.go: "did not converge" runs are
	// not stored as session truth.
	//
	// Why 0.2 (vs breadth's 0.8): the OI-weighted gamma compute
	// concentrates near ATM, so missing far-OTM legs has small
	// impact on the zero-gamma estimate. ATM strikes are the most
	// liquid and resolve first; 20% coverage typically captures the
	// ATM ±5% band that dominates the gamma profile. Below 20% the
	// signal is too thin to band reliably.
	//
	// Lowered from 0.5 (v0.28.x) to 0.2 (v0.29.0): empirically the
	// IBKR gateway's OPT model-tick delivery is bursty during RTH —
	// landing 20-40% of legs within the per-leg budget is typical,
	// not a degraded run. The previous 0.5 threshold was discarding
	// usable results and forcing a 60s retry cooldown that left the
	// dashboard "computing" for 5-10 minutes. 0.2 is more honest
	// about what the gateway will deliver while still gating on
	// enough signal to compute a meaningful γ-zero.
	MinLegCoverageFraction = 0.2
)

Default calibration window for the zero-gamma compute. Tuned for the trader-side review: 6 expirations beats the SpotGamma 4-expiry default in nominal coverage; ±10 % strike width defines the candidate window and the nearest-80-strikes cap keeps the leg count reasonable; ±15 % sweep range comfortably brackets the typical zero crossing without inflating the profile point count.

WorkerCount 4 matches the documented safe gateway throttle elsewhere in this package (handleChainFetch, around handlers.go:1628). Bumping it requires retuning AcquireMarketDataSlot and is a deliberate follow-up, not a v1 knob.

View Source
const USDJPYLookbackDays = 14

USDJPYLookbackDays is the calendar-day window passed to the HMDS history fetch when computing the 7-trading-day close for USD/JPY. FX trades 24/5 so 7 trading days = 7 weekday FX sessions. 14 calendar days covers 7 FX sessions even when a Monday or Friday bank holiday interrupts the count (US: MLK Day, Memorial Day, Labor Day, Thanksgiving, etc. all fall on Mondays and clip one US-tradable FX day). Widened from 12 to 14 in v0.23.0 (commit 02aba13).

Variables

View Source
var ErrAlreadyRunning = errors.New("another Canary daemon holds the instance lock")

ErrAlreadyRunning means another Canary daemon holds the instance lock for this socket path. Callers (cmd/canaryd) treat this as an expected, non-fatal condition: a duplicate start, exit cleanly.

View Source
var ErrPersistenceInUse = errors.New("another Canary daemon owns the daemon state database")

ErrPersistenceInUse means another process owns the daemon state database. It is deliberately distinct from ErrAlreadyRunning: two daemons using different socket paths must still fail visibly when they resolve to the same daemon.db.

View Source
var ErrTradingDisabled = errors.New("trading disabled")

ErrTradingDisabled is returned when the local order-entry gate is closed or an order-write handler is intentionally unavailable. The dispatcher returns this as CodeTradingDisabled rather than unknown_method so clients get a clear safety refusal instead of a method-typo guess.

Functions

func DefaultPolicyTOML

func DefaultPolicyTOML(name string) ([]byte, error)

DefaultPolicyTOML renders the embedded default for a policy name ("protection" or "opportunity") as activation-ready TOML. It backs `canary policy default <name>`: no template file ships for these policies, so the printable embedded default is the single source and cannot drift from the code the daemon actually runs.

func DefaultStreakStoreDir

func DefaultStreakStoreDir() (string, error)

DefaultStreakStoreDir returns the on-disk cache root for the streak store. Matches the layout used by the contract store and breadth engine ($XDG_CACHE_HOME/ibkr/) so all daemon caches live together.

func IsSubscriptionRejected

func IsSubscriptionRejected(err error) bool

IsSubscriptionRejected reports whether err is a SubscriptionRejectedError. Convenience for the common "gateway said no" branch in fan-out callers.

Types

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger is a tiny slog-backed front for the daemon. It also configures the pkg/ibkr internal logger so library output funnels through the same handler.

func NewLogger

func NewLogger(w io.Writer, level string) *Logger

NewLogger constructs a slog text logger writing to w at the given level ("debug"|"info"|"warn"|"error").

func (*Logger) Debugf

func (l *Logger) Debugf(f string, args ...any)

Debugf logs a formatted message at debug level.

func (*Logger) Errorf

func (l *Logger) Errorf(f string, args ...any)

Errorf logs a formatted message at error level.

func (*Logger) Infof

func (l *Logger) Infof(f string, args ...any)

Infof logs a formatted message at info level.

func (*Logger) Warnf

func (l *Logger) Warnf(f string, args ...any)

Warnf logs a formatted message at warning level.

type Options

type Options struct {
	Config     *config.Resolved
	SocketPath string
	Version    string
	Logger     *Logger
	// StateDatabasePath overrides daemon.db for isolated tests and offline
	// verification. Production leaves it empty and uses the XDG state root.
	StateDatabasePath string
}

Options configures a Server.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the daemon process state.

func New

func New(opts Options) *Server

New constructs a Server with the supplied options.

func (*Server) Start

func (s *Server) Start(ctx context.Context) error

Start runs discovery against the configured (possibly partial) gateway, opens the IB Gateway connection in the background, listens on the Unix socket, and blocks until ctx is cancelled or Stop is called. Returns the first fatal error encountered. Returns ErrAlreadyRunning (without touching the gateway) if another Canary daemon holds the instance lock.

func (*Server) Stop

func (s *Server) Stop()

Stop closes the listener and IBKR connection. Safe to call multiple times. A Server that never reached openSocket (e.g. lock contention exit) must not touch the socket file — that would unlink the active peer's socket and break the running daemon.

type SkewCurve

type SkewCurve struct {
	A, B, C float64
	// contains filtered or unexported fields
}

SkewCurve is a quadratic fit of implied volatility against log-moneyness for one option expiry: σ(m) = A + B·m + C·m², with m = ln(K / S). The sweep uses it to reprice each leg's IV at every scenario spot's moneyness rather than holding the captured snapshot IV fixed — the sticky-moneyness convention.

Why bother: the legacy sticky-IV recipe biases zero-gamma upward because real SPX skew is steep (OTM puts trade richer than ATM, OTM calls cheaper). When the sweep walks spot down 5 %, the dealer-short puts that used to be 5 % OTM are now ATM and their true IV is lower, not the captured-at-snapshot value. Sticky-moneyness recomputes σ at each scenario spot's strike/spot ratio so the leg's gamma reflects the IV the leg WOULD have at that scenario spot. Empirically this shifts zero-gamma by ~30-80 SPX points and tracks SpotGamma's posted numbers materially better.

mLo / mHi are the moneyness range we fitted over. Evaluating outside the range extrapolates a parabola — wild. The IVAtMoneyness method clamps to [mLo, mHi] before evaluating; the curve outside that window flattens to the boundary value rather than projecting.

nPoints is the number of (m, σ) samples in the fit; ok=false when fewer than 3 points were available (degenerate; the caller falls back to sticky-IV for that expiry).

func (*SkewCurve) IVAtMoneyness

func (s *SkewCurve) IVAtMoneyness(m float64) float64

IVAtMoneyness evaluates the curve at moneyness m = ln(K / S). Clamps m to the fitted range before evaluating so the parabolic extrapolation outside the fit window doesn't return runaway IVs at the sweep's outer edges. Returns 0 when the curve is unfit (ok=false) — the caller maps that to sticky-IV fallback for the affected expiry.

The boundary clamp is the right honest call: outside the fitted range we have no information about how skew curves, so freezing the IV to the closest boundary value reads as "best guess from observed data" rather than "parabolic projection beyond the data."

type StreakEntry

type StreakEntry struct {
	LastBand    string  `json:"last_band"`
	SinceDate   string  `json:"since_date"`
	LastSession string  `json:"last_session"`
	Sessions    int     `json:"sessions"`
	LastValue   float64 `json:"last_value"`
	// EligibleLatched records that this red streak earned confirmation
	// eligibility (depth + persistence + freshness) at some point in its
	// life. Once latched, eligibility holds until the band exits red even
	// if the measurement wobbles back inside the minimum depth — the
	// depth-boundary churn guard from internal-docs/design/regime-calibration.md.
	// Cleared on any band change. Freshness is NOT latched: overdue data
	// drops eligibility regardless.
	EligibleLatched bool `json:"eligible_latched,omitempty"`
	// LastBandAt is when LastBand was last measured from live inputs. It
	// dates a band held across an input outage so the row can say how old
	// its reading is instead of implying it is current. Entries written
	// before the field existed decode to the zero value, which means the age
	// is unknown — the row says that rather than inventing one.
	LastBandAt time.Time `json:"last_band_at"`
}

StreakEntry is one indicator's persisted band history. LastBand is the band classification observed on the most recent successful tick; LastSession is the NY-tz session key (YYYY-MM-DD) the tick happened in. Sessions counts how many NY sessions in a row the indicator has reported LastBand. LastValue is the raw measurement at LastSession — kept for diagnostics so a human inspecting the file can verify the classification.

type StreakInfo

type StreakInfo = rpc.StreakInfo

StreakInfo is the in-package alias for rpc.StreakInfo so callers in the daemon package can avoid importing the rpc package solely for the type name.

type StreakStore

type StreakStore struct {
	// contains filtered or unexported fields
}

StreakStore persists the streak counters across daemon restarts. Storage shape matches the contract-store convention from 2fbd614: a single JSON file with a version field, atomic temp+rename writes, per-indicator entries keyed on a stable token.

The store is its own persistence domain — distinct from the contract store, gamma cache, and breadth windows — because the invalidation rules differ. Streak entries don't expire on calendar tickover; they persist across days and only change band on a band transition or reset on a long gap (which the Tick logic handles via session counting).

func NewStreakStore

func NewStreakStore(dir string) *StreakStore

NewStreakStore returns a store rooted at dir. Construction is lazy — the on-disk file is read on the first Tick or Get call, not at construction time, so a daemon that constructs the store before touching disk doesn't pay the read cost upfront.

func (*StreakStore) Get

func (s *StreakStore) Get(indicatorKey string) *StreakInfo

Get returns the current StreakInfo for an indicator without modifying it. Used by tests and diagnostics; the production fetch path goes through Tick.

func (*StreakStore) Latch

func (s *StreakStore) Latch(indicatorKey string)

Latch marks the indicator's current red streak as having earned confirmation eligibility. No-op when the entry is missing or not red — the latch only ever decorates a live red streak. Best-effort persist, same contract as Tick.

func (*StreakStore) Latched

func (s *StreakStore) Latched(indicatorKey string) bool

Latched reports the eligibility latch for an indicator's current streak.

func (*StreakStore) PrevBand

func (s *StreakStore) PrevBand(indicatorKey string) string

PrevBand returns the band recorded on the most recent tick — the input exit-hysteresis classification needs. Empty when never seen.

func (*StreakStore) PrevBandAt added in v2.8.0

func (s *StreakStore) PrevBandAt(indicatorKey string) time.Time

PrevBandAt returns when PrevBand was last measured from live inputs. Zero when the indicator has no entry or the entry predates the field.

func (*StreakStore) Tick

func (s *StreakStore) Tick(indicatorKey string, value float64, band string, nowNY time.Time) *StreakInfo

Tick advances the streak counter for indicatorKey using the supplied (value, band) observation, returns a *StreakInfo representing the updated state, and persists the file. Empty band freezes the counter — pass band="" for computing/unavailable/error states so a stale tick doesn't reset a real streak.

nowNY is the wall-clock-now interpreted in America/New_York; the session key is derived from its date portion. Injected for tests.

Logic:

  • First call ever for this key: insert {band, today, sessions: 1, value} → return Sessions: 1.
  • Same band as last call AND last call was on a previous trading day: increment sessions.
  • Same band as last call AND last call was today: leave alone (multiple calls on the same day = same streak, no double-counting).
  • Different band: reset to {band: newBand, since: today, sessions: 1, value: newValue}.
  • Empty band (indicator computing / unavailable / error): freeze the counter — return the existing entry unchanged. The renderer still sees the previous band's streak; a stale tick shouldn't end a streak.

func (*StreakStore) UseCoreStore

func (s *StreakStore) UseCoreStore(store *corestore.Store) error

UseCoreStore makes daemon.db the sole runtime persistence authority and discards any legacy state that may have been loaded before attachment.

type SubscriptionRejectedError

type SubscriptionRejectedError struct {
	Key       string
	Rejection ibkrlib.SubscriptionRejection
}

SubscriptionRejectedError is returned by pollMarketData (and helpers that thread the subscription's reject channel) when the IBKR gateway pushed a terminal subscription error (codes 200/320/321/322/354/10197) for the reqID being polled. Callers use errors.As to inspect the rejection code so they can distinguish "no such contract" from a budget timeout without waiting out the full deadline.

func (*SubscriptionRejectedError) Error

func (e *SubscriptionRejectedError) Error() string

Error formats the subscription key and broker rejection details.

Source Files

Directories

Path Synopsis
Package corestore owns the daemon's authoritative SQLite state in daemon.db.
Package corestore owns the daemon's authoritative SQLite state in daemon.db.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL