Documentation
¶
Overview ¶
Ubiquitous language glossary for the Kite MCP trading platform.
This file disambiguates domain terms that appear throughout the codebase under multiple names or with overlapping meanings. Each term is defined as a documented type alias or constant so that code reads as domain prose and the compiler enforces consistent usage.
Reading this file should give any new contributor a mental model of the bounded contexts and their key concepts.
Package domain contains DDD value objects and domain events for the trading platform. These are pure domain types with zero external dependencies — they model the ubiquitous language of Indian equity trading (Money in INR, Quantities, InstrumentKeys like "NSE:RELIANCE") and domain events that capture what happened in the system.
Index ¶
- Constants
- Variables
- func AnomalyCacheAggregateID(email string) string
- func GTTAggregateID(triggerID int) string
- func GTTRejectedAggregateID(triggerID int, email string, occurredAt time.Time) string
- func IsPercentageDirection(d Direction) bool
- func MFAggregateID(id string) string
- func MFOrderRejectedAggregateID(orderID, email string, occurredAt time.Time) string
- func NativeAlertAggregateID(uuid, email string) string
- func OrderRejectedAggregateID(orderID, email string, occurredAt time.Time) string
- func PaperOrderAggregateID(orderID string) string
- func PaperTradingAggregateID(email string) string
- func PluginWatcherAggregateID(path string) string
- func PositionAggregateID(email string, instrument InstrumentKey, product string) string
- func PositionConvertedAggregateID(email, exchange, tradingsymbol, oldProduct string) string
- func RiskguardCountersAggregateID(email string) string
- func TelegramSubscriptionAggregateID(email string) string
- func TrailingStopAggregateID(trailingStopID string) string
- func ValidateAlertSpec(direction Direction, targetPrice, referencePrice float64) error
- func ValidateIceberg(total, disclosed int) error
- func ValidateLotSize(qty, lotSize int) error
- func ValidateTickSize(price, tickSize float64) error
- func ValidateTradingsymbolFormat(symbol string) error
- type APIKey
- type APISecret
- type AdminActor
- type AdminRole
- type Alert
- func (a *Alert) InstrumentKey() string
- func (a *Alert) IsActive() bool
- func (a *Alert) IsComposite() bool
- func (a *Alert) IsPercentageAlert() bool
- func (a *Alert) MarkTriggered(currentPrice float64) bool
- func (a *Alert) MatchesInstrument(instrumentToken uint32) bool
- func (a *Alert) NeedsNotification() bool
- func (a *Alert) PercentageChange(currentPrice float64) float64
- func (a *Alert) ShouldTrigger(currentPrice float64) bool
- type AlertCreatedEvent
- type AlertDeletedEvent
- type AlertTriggeredEvent
- type AlertType
- type AndSpec
- type AnomalyBaselineSnapshottedEvent
- type AnomalyCacheEvictedEvent
- type AnomalyCacheInvalidatedEvent
- type CompositeCondition
- type CompositeLogic
- type ConsentWithdrawnEvent
- type Credential
- type CredentialRegisteredEvent
- type CredentialResolution
- type CredentialRevokedEvent
- type CredentialRotatedEvent
- type CredentialSource
- type Direction
- type Event
- type EventDispatcher
- type Family
- type FamilyInvitedEvent
- type FamilyMemberRemovedEvent
- type GTTDeletedEvent
- type GTTModifiedEvent
- type GTTPlacedEvent
- type GTTRejectedEvent
- type GlobalFreezeEvent
- type GlobalFreezeReason
- type Holding
- type InstrumentKey
- type InstrumentRules
- type KiteSessionData
- type KiteToken
- type MCPSessionID
- type MFOrderCancelledEvent
- type MFOrderPlacedEvent
- type MFOrderRejectedEvent
- type MFSIPCancelledEvent
- type MFSIPPlacedEvent
- type Money
- type NativeAlertDeletedEvent
- type NativeAlertModifiedEvent
- type NativeAlertPlacedEvent
- type NotSpec
- type OAuthToken
- type OrSpec
- type Order
- func (o Order) CanCancel() bool
- func (o Order) DTO() broker.Order
- func (o Order) FillPercentage() float64
- func (o Order) ID() string
- func (o Order) IsCancelled() bool
- func (o Order) IsComplete() bool
- func (o Order) IsPending() bool
- func (o Order) IsRejected() bool
- func (o Order) IsTerminal() bool
- func (o Order) Status() string
- type OrderCancelledEvent
- type OrderCandidate
- type OrderFilledEvent
- type OrderFreezeReason
- type OrderModifiedEvent
- type OrderPlacedEvent
- type OrderPlacement
- type OrderRejectedEvent
- type OrderSpec
- type PaperOrderRejectedEvent
- type PaperTradingDisabledEvent
- type PaperTradingEnabledEvent
- type PaperTradingResetEvent
- type PluginRegisteredEvent
- type PluginReloadTriggeredEvent
- type PluginUnregisteredEvent
- type PluginWatcherStartedEvent
- type PluginWatcherStoppedEvent
- type Position
- type PositionClosedEvent
- type PositionConvertedEvent
- type PositionOpenedEvent
- type PriceSpec
- type Profile
- func (p Profile) HasCommodityAccess() bool
- func (p Profile) HasDerivativesAccess() bool
- func (p Profile) HasEquityAccess() bool
- func (p Profile) IsIntradayEligible() bool
- func (p Profile) IsZerodhaAccount() bool
- func (p Profile) SupportsExchange(exchange string) bool
- func (p Profile) SupportsProduct(product string) bool
- type Quantity
- type QuantitySpec
- type RiskLimitBreachedEvent
- type RiskguardDailyCounterResetEvent
- type RiskguardKillSwitchTrippedEvent
- type RiskguardRejectionEvent
- type Session
- func (s Session) AccessToken() string
- func (s Session) DTO() SessionData
- func (s Session) Email() string
- func (s Session) HasToken() bool
- func (s Session) IsAuthenticated() bool
- func (s Session) IsAuthenticatedAt(now time.Time) bool
- func (s Session) IsExpired() bool
- func (s Session) IsExpiredAt(now time.Time) bool
- func (s Session) IssuedAt() time.Time
- func (s Session) TokenAgeHours() float64
- func (s Session) TokenAgeHoursAt(now time.Time) float64
- type SessionClearedEvent
- type SessionCreatedEvent
- type SessionData
- type SessionID
- type SessionInvalidatedEvent
- type Spec
- type TelegramChatBoundEvent
- type TelegramSubscribedEvent
- type TierChangedEvent
- type TrailingStopCancelledEvent
- type TrailingStopSetEvent
- type TrailingStopTriggeredEvent
- type UserFrozenEvent
- type UserSuspendedEvent
- type WatchlistCreatedEvent
- type WatchlistDeletedEvent
- type WatchlistItemAddedEvent
- type WatchlistItemRemovedEvent
Constants ¶
const ( // TransactionBuy represents a buy order. TransactionBuy = "BUY" // TransactionSell represents a sell order. TransactionSell = "SELL" )
const ( // OrderTypeMarket is a market order — executed at best available price. OrderTypeMarket = "MARKET" // OrderTypeLimit is a limit order — executed at specified price or better. OrderTypeLimit = "LIMIT" // OrderTypeSL is a stop-loss order — becomes a limit order when trigger price is hit. OrderTypeSL = "SL" // OrderTypeSLM is a stop-loss market order — becomes a market order when trigger price is hit. OrderTypeSLM = "SL-M" )
const ( // ProductCNC is Cash & Carry — delivery-based equity holding. ProductCNC = "CNC" // ProductMIS is Margin Intraday Settlement — intraday leveraged trading. ProductMIS = "MIS" // ProductNRML is Normal — used for F&O positions carried overnight. ProductNRML = "NRML" )
const ( // ExchangeNSE is the National Stock Exchange. ExchangeNSE = "NSE" // ExchangeBSE is the Bombay Stock Exchange. ExchangeBSE = "BSE" // ExchangeNFO is the NSE Futures & Options segment. ExchangeNFO = "NFO" // ExchangeBFO is the BSE Futures & Options segment. ExchangeBFO = "BFO" // ExchangeMCX is the Multi Commodity Exchange. ExchangeMCX = "MCX" // ExchangeCDS is the Currency Derivatives Segment. ExchangeCDS = "CDS" )
const ( // OrderStatusOpen means the order is accepted by the exchange and sitting // in the book awaiting fill. Cancellable. OrderStatusOpen = "OPEN" // OrderStatusTriggerPending means a stop-loss / SL-M order's trigger // price has not yet been crossed. Cancellable. OrderStatusTriggerPending = "TRIGGER PENDING" // OrderStatusValidationPending is a transient state while Kite validates // the order. Not cancellable (will transition to OPEN or REJECTED). OrderStatusValidationPending = "VALIDATION PENDING" // OrderStatusAMOReqReceived is the overnight-order accepted state. // Pending but not yet open in the book. OrderStatusAMOReqReceived = "AMO REQ RECEIVED" // OrderStatusComplete is the terminal filled state. OrderStatusComplete = "COMPLETE" // OrderStatusCancelled is the terminal user/system-cancelled state. OrderStatusCancelled = "CANCELLED" // OrderStatusRejected is the terminal risk-rejected state. OrderStatusRejected = "REJECTED" )
const ( // PositionLong means the position quantity is > 0 — the user owns the // instrument / is net-long. PositionLong = "LONG" // PositionShort means the position quantity is < 0 — the user has sold // more than bought / is net-short. PositionShort = "SHORT" // PositionFlat means the position quantity is exactly 0 — the position // is closed out but may still show residual P&L from the day's activity. PositionFlat = "FLAT" )
--- Position direction constants ---
Direction is exposed as a stringly-typed value (not a domain.Direction, which is already taken by alerts). The values are display-friendly identifiers used in dashboards and debug logs.
Variables ¶
var NewINR = money.NewINR
NewINR re-exports money.NewINR so existing call sites `domain.NewINR(N)` continue to work unchanged. Package-level function value (`var`, not `func`) is the cheapest re-export — Go resolves the call exactly like the original.
var NewMoney = money.NewMoney
NewMoney re-exports money.NewMoney for the validated-positive constructor. Same identity guarantee as NewINR.
var ValidCompositeLogics = map[CompositeLogic]bool{ CompositeLogicAnd: true, CompositeLogicAny: true, }
ValidCompositeLogics is the set of accepted CompositeLogic values — mirrors ValidDirections and simplifies caller validation.
var ValidDirections = map[Direction]bool{ DirectionAbove: true, DirectionBelow: true, DirectionDropPct: true, DirectionRisePct: true, }
ValidDirections is the set of all supported alert directions.
Functions ¶
func AnomalyCacheAggregateID ¶
AnomalyCacheAggregateID returns the natural aggregate key for anomaly-cache events. Format: "anomaly:<email>". The "anomaly:" prefix keeps these streams disjoint from per-email user-aggregate streams (UserFrozenEvent, etc.) and the "riskguard:<email>" stream, so a future projector can replay the anomaly cache aggregate cleanly without filtering by event type.
func GTTAggregateID ¶
GTTAggregateID returns the natural aggregate key for GTT success events. Format: fmt.Sprintf("%d", triggerID) — matches the existing appendAuxEvent aggregate IDs and the GTTRejectedAggregateID format so all GTT lifecycle events sort under one stream.
func GTTRejectedAggregateID ¶
GTTRejectedAggregateID returns the natural aggregate key for GTTRejectedEvent. TriggerID is fmt.Sprintf'd with "%d" to match the existing success-path aggregate ID format in kc/usecases/gtt_usecases.go (which emits gtt.placed/modified/deleted keyed by fmt.Sprintf("%d", triggerID)). Empty TriggerID + email falls back to "gtt-rejected:<email>:<rfc3339-nanos>"; both empty lands in "gtt-rejected:unknown".
func IsPercentageDirection ¶
IsPercentageDirection returns true if the direction is a percentage-change type.
func MFAggregateID ¶
MFAggregateID returns the natural aggregate key for MF success events. The id parameter is OrderID (for MFOrder*) or SIPID (for MFSIP*). Both ID namespaces are Kite-assigned strings, distinct across the surface, so a single helper handles both. Empty falls back to "mf:unknown" so malformed dispatches don't collide with real rows.
func MFOrderRejectedAggregateID ¶
MFOrderRejectedAggregateID returns the natural aggregate key for MFOrderRejectedEvent. Format mirrors OrderRejectedAggregateID: non-empty OrderID joins the aggregate stream; empty OrderID + non-empty email falls back to "mf-rejected:<email>:<rfc3339-nanos>"; pathological "neither" lands in "mf-rejected:unknown".
func NativeAlertAggregateID ¶
NativeAlertAggregateID returns the natural aggregate key for native alert events. Non-empty UUID joins the alert aggregate stream; empty UUID falls back to the email (matching the prior PlaceNativeAlertUseCase aggregate-id choice for placement events where the broker hadn't yet assigned a UUID). Both empty falls back to "native-alert:unknown" so malformed dispatches don't collide with real rows.
func OrderRejectedAggregateID ¶
OrderRejectedAggregateID returns the natural aggregate key for an OrderRejectedEvent. When OrderID is non-empty (modify/cancel paths, where the caller supplied the broker-assigned ID), the rejection joins the existing order aggregate stream — a downstream projector walking aggregate_id="ORD-123" sees place→reject→modify→reject→cancel transitions in one chronological view. When OrderID is empty (place_order failure, no broker ID was assigned), the event keys by "rejected:<email>:<unix-nanos>" so each rejection lands in its own aggregate slot without colliding with other users' rejections or future rejections from the same user. The "rejected:" prefix keeps these stand-alone streams disjoint from real order streams so a projector that filters by aggregate_type="Order" doesn't conflate "no broker ID issued" rejections with placed orders.
func PaperOrderAggregateID ¶
PaperOrderAggregateID returns the natural aggregate key for paper- trading order events. Currently used by PaperOrderRejectedEvent only; future paper.* events should reuse this helper so the per-paper- order aggregate stream stays coherent. Empty OrderID falls back to "paper:unknown" so malformed dispatches don't collide with real rows.
func PaperTradingAggregateID ¶
PaperTradingAggregateID returns the natural aggregate key for paper-trading lifecycle events. Keyed by email — the user's paper account is per-user so the full enable / reset / disable lifecycle replays under one stream. Empty falls back to "paper-trading:unknown" so malformed dispatches don't collide with real rows.
Note the disjoint key prefix from PaperOrderAggregateID ("PAPER_<n>" for individual paper orders) — these two paper aggregate streams (per-user lifecycle vs per-order rejection) live under different ID shapes intentionally so projector consumers can query them separately.
func PluginWatcherAggregateID ¶
PluginWatcherAggregateID returns the natural aggregate key for plugin- watcher events. Per-path mutations (registered, unregistered, reload_triggered) key by the absolute Path so a single plugin's lifecycle replays as a coherent stream. Watcher-lifecycle events (started, stopped) have no path and key by the singleton string "plugin-watcher:global" so they form their own aggregate stream disjoint from per-plugin streams.
func PositionAggregateID ¶
func PositionAggregateID(email string, instrument InstrumentKey, product string) string
PositionAggregateID returns the natural aggregate key for position events. Format: "email:exchange:tradingsymbol:product". Both PositionOpenedEvent and PositionClosedEvent for the same (user, instrument, product) triple land under the same aggregate ID, allowing event-store replay to reconstruct the full position history.
func PositionConvertedAggregateID ¶
PositionConvertedAggregateID returns the natural aggregate key for position-conversion events. Format: "<email>|<exchange>|<tradingsymbol>|<oldProduct>". The pipe separator (rather than colon) avoids ambiguity with the time.RFC3339 colons projector consumers parse out of OrderRejectedEvent's synthetic keys. Empty email falls back to "position-converted:unknown" so a malformed dispatch lands in its own quarantine slot rather than colliding with real rows.
func RiskguardCountersAggregateID ¶
RiskguardCountersAggregateID returns the natural aggregate key for per-user riskguard counter events. Format: "riskguard:<email>" or "riskguard:global" for the kill-switch (global-scope) events. The "riskguard:" prefix keeps these aggregate streams disjoint from per-email user-aggregate streams (UserFrozenEvent, etc.) so a future projector can replay the counters aggregate cleanly.
func TelegramSubscriptionAggregateID ¶
TelegramSubscriptionAggregateID returns the natural aggregate key for per-user Telegram subscription events. Format: "telegram:<email>". The "telegram:" prefix keeps these aggregate streams disjoint from per-email user-aggregate streams (UserFrozenEvent, etc.) and the "riskguard:<email>" / "anomaly:<email>" streams, so a future projector can replay the Telegram subscription aggregate cleanly without filtering by event type.
func TrailingStopAggregateID ¶
TrailingStopAggregateID returns the natural aggregate key for trailing-stop trigger events. Keyed by TrailingStopID (uuid-derived 8-char prefix from kc/alerts/trailing.go) — globally unique across users, so no email prefix is needed. Empty falls back to "trailing-stop:unknown" so malformed dispatches don't collide with real trailing-stop rows.
func ValidateAlertSpec ¶
ValidateAlertSpec enforces construction invariants for a single-leg price alert. Rules:
- Direction must be in ValidDirections.
- TargetPrice must be strictly positive (a zero / negative threshold is never a valid trigger).
- For percentage directions (drop_pct / rise_pct): ReferencePrice must be positive AND TargetPrice must be ≤ 100 (percent).
Called from CreateAlertUseCase.Execute and the telegram /setalert handler so the rule lives in one place. Previously duplicated at kc/usecases/create_alert.go:75, kc/eventsourcing/alert_aggregate.go:76, and kc/telegram/trading_commands.go:349.
func ValidateIceberg ¶
ValidateIceberg enforces the "disclosed ≤ total" invariant for iceberg orders on Kite. Iceberg variety splits a large order into visible chunks of `disclosed` quantity at a time; disclosed > total is logically impossible, and zero/negative values are nonsensical for a chunked order.
This is a free function rather than a value-object constructor because iceberg is a property of the order placement request, not of the order itself after acceptance — the broker stores the expanded legs, not the configuration.
func ValidateLotSize ¶
ValidateLotSize enforces that an order's total quantity is a positive integer multiple of the instrument's lot size. Equity typically has lotSize = 1 so any positive qty passes; derivatives (F&O) enforce the contract size (NIFTY futures lotSize = 50 at time of writing).
lotSize <= 0 is treated as bad configuration — legitimate equity rows report lotSize = 1, never 0.
func ValidateTickSize ¶
ValidateTickSize enforces that an order's price is aligned to the instrument's tick size. NSE equity ticks are typically 0.05; some indices / MF entries have tickSize = 0, which we treat as "no tick rule" (the broker accepts arbitrary precision).
Uses a 1e-9 epsilon because float remainders on decimal-rational tick sizes drift (2500.45 mod 0.05 is not exactly zero in IEEE-754).
func ValidateTradingsymbolFormat ¶
ValidateTradingsymbolFormat enforces that a tradingsymbol is non-empty and contains only printable, non-whitespace ASCII-ish characters. Kite's tradingsymbol convention is uppercase alphanumerics plus a few separators like "-" and "&" (e.g. "M&M"); we don't enforce the exact charset — just reject clearly bad inputs (empty, whitespace, control chars) so invalid rows fail fast at the domain boundary instead of surfacing as opaque Kite API errors later.
Types ¶
type APIKey ¶
type APIKey struct {
// contains filtered or unexported fields
}
APIKey is a value object for a Kite Connect developer-app API key. Constructor rejects empty / whitespace-only values so downstream persistence layers can treat "has APIKey" as a non-null invariant.
func NewAPIKey ¶
NewAPIKey constructs a validated APIKey. Leading/trailing whitespace is stripped; the resulting value must be non-empty. The Kite developer console emits fixed-length alphanumeric keys — we deliberately do not enforce length or character class here because the API evolves and per-app key formats differ across regions (paper-trading keys carry dash prefixes, for example). Non-empty is the load-bearing invariant.
type APISecret ¶
type APISecret struct {
// contains filtered or unexported fields
}
APISecret is a value object for the paired Kite Connect API secret. Secrets are sensitive credentials — this type mirrors APIKey's invariants and carries a masking helper so log-sites do not accidentally leak the full value.
func NewAPISecret ¶
NewAPISecret constructs a validated APISecret. Non-empty (after trimming) is the only construction invariant — length / char-class rules vary per app.
type AdminActor ¶
type AdminActor = string
AdminActor identifies the email of a user performing an admin action. Used in audit events (GlobalFreezeEvent.By, UserSuspendedEvent.By) and family management (FamilyInvitedEvent.AdminEmail). This is a runtime identity — who did it — not a permission flag.
type AdminRole ¶
type AdminRole = bool
AdminRole is the authorization concept: whether a user email is in the ADMIN_EMAILS allow-list. Checked by middleware before admin endpoints. This is a compile-time hint only — actual enforcement is in the auth layer.
type Alert ¶
type Alert struct {
ID string `json:"id"`
Email string `json:"email"`
Tradingsymbol string `json:"tradingsymbol"`
Exchange string `json:"exchange"`
InstrumentToken uint32 `json:"instrument_token"`
TargetPrice float64 `json:"target_price"`
Direction Direction `json:"direction"`
ReferencePrice float64 `json:"reference_price,omitempty"`
Triggered bool `json:"triggered"`
CreatedAt time.Time `json:"created_at"`
TriggeredAt time.Time `json:"triggered_at,omitempty"`
TriggeredPrice float64 `json:"triggered_price,omitempty"`
NotificationSentAt time.Time `json:"notification_sent_at,omitempty"`
// Composite alert fields (populated only when AlertType == AlertTypeComposite).
// These live alongside the single-leg fields in the same row — Option B
// from the session handoff. A NULL alert_type in the DB is normalized
// to AlertTypeSingle on load.
AlertType AlertType `json:"alert_type,omitempty"`
CompositeName string `json:"composite_name,omitempty"`
CompositeLogic CompositeLogic `json:"composite_logic,omitempty"`
Conditions []CompositeCondition `json:"conditions,omitempty"`
}
Alert represents a price alert for a specific instrument. Rich domain entity with lifecycle behavior — the 8 methods below are the canonical definition of "what it means for an alert to trigger / fire / need notification". Persistence is handled by kc/alerts.Store which wraps this entity.
Composite alerts reuse the same table (Option B per session handoff): AlertType = AlertTypeComposite, CompositeLogic + Conditions populated, and the top-level Direction/TargetPrice ignored by the evaluator. Single-leg alerts leave the composite fields zero-valued.
func (*Alert) InstrumentKey ¶
InstrumentKey returns the "exchange:tradingsymbol" identifier for the alerted instrument.
func (*Alert) IsComposite ¶
IsComposite returns true if this alert aggregates 2+ conditions.
func (*Alert) IsPercentageAlert ¶
IsPercentageAlert returns true if this alert uses a percentage-change direction.
func (*Alert) MarkTriggered ¶
MarkTriggered transitions the alert to triggered state with the given price. Returns true if newly triggered, false if already triggered.
func (*Alert) MatchesInstrument ¶
MatchesInstrument returns true if the alert is for the given instrument token.
func (*Alert) NeedsNotification ¶
NeedsNotification returns true if the alert has fired but no notification has been sent.
func (*Alert) PercentageChange ¶
PercentageChange returns the signed percentage change of currentPrice from ReferencePrice. Returns 0 if ReferencePrice is not set (<= 0).
func (*Alert) ShouldTrigger ¶
ShouldTrigger checks if the current price meets this alert's trigger condition.
type AlertCreatedEvent ¶
type AlertCreatedEvent struct {
Email string
AlertID string
Instrument InstrumentKey
TargetPrice Money
Direction string // "above", "below", "drop_pct", "rise_pct"
Timestamp time.Time
}
AlertCreatedEvent is emitted when a new price alert is created.
func (AlertCreatedEvent) EventType ¶
func (e AlertCreatedEvent) EventType() string
func (AlertCreatedEvent) OccurredAt ¶
func (e AlertCreatedEvent) OccurredAt() time.Time
type AlertDeletedEvent ¶
AlertDeletedEvent is emitted when a price alert is deleted.
func (AlertDeletedEvent) EventType ¶
func (e AlertDeletedEvent) EventType() string
func (AlertDeletedEvent) OccurredAt ¶
func (e AlertDeletedEvent) OccurredAt() time.Time
type AlertTriggeredEvent ¶
type AlertTriggeredEvent struct {
Email string
AlertID string
Instrument InstrumentKey
TargetPrice Money
CurrentPrice Money
Direction string // "above", "below", "drop_pct", "rise_pct"
Timestamp time.Time
}
AlertTriggeredEvent is emitted when a price alert fires.
func (AlertTriggeredEvent) EventType ¶
func (e AlertTriggeredEvent) EventType() string
func (AlertTriggeredEvent) OccurredAt ¶
func (e AlertTriggeredEvent) OccurredAt() time.Time
type AlertType ¶
type AlertType string
AlertType distinguishes a single-leg alert from a composite (multi-leg) alert. Stored in the `alert_type` column of the alerts table.
const ( // AlertTypeSingle is the default — a plain single-instrument, single- // condition alert. Existing pre-composite rows are normalized to this // value on load. AlertTypeSingle AlertType = "single" // AlertTypeComposite is a multi-leg alert that combines 2+ conditions // across instruments via CompositeLogic (AND/ANY). Composite rows carry // a non-empty Conditions slice; the top-level Direction/TargetPrice // fields are ignored by the evaluator. AlertTypeComposite AlertType = "composite" )
type AndSpec ¶
AndSpec is satisfied only when both Left and Right are satisfied.
func (*AndSpec[T]) IsSatisfiedBy ¶
type AnomalyBaselineSnapshottedEvent ¶
type AnomalyBaselineSnapshottedEvent struct {
UserEmail string
Days int
Mean float64
Stdev float64
Count float64
BelowFloor bool // true when count<minBaselineOrders and mean/stdev are zeroed sentinels
Timestamp time.Time
}
AnomalyBaselineSnapshottedEvent is emitted when the audit Store's in-memory UserOrderStats baseline cache writes a fresh (mean, stdev, count) tuple for a (user, days) pair. Carries the user email plus the snapshotted statistical fields so a downstream consumer can reconstruct the anomaly baseline projection without re-querying the 30-day order history.
Aggregate-ID rule: keyed by AnomalyCacheAggregateID(UserEmail) (one logical baseline per user across all days windows; Days is a payload field, not part of the aggregate key). Below-threshold snapshots (count < minBaselineOrders) write the floor sentinel (mean=0, stdev=0) — BelowFloor=true so projector consumers can distinguish "user is new and we suppressed stats" from "real distribution centred at zero" (the latter is impossible at the SQL layer but BelowFloor makes the intent unambiguous).
func (AnomalyBaselineSnapshottedEvent) EventType ¶
func (e AnomalyBaselineSnapshottedEvent) EventType() string
func (AnomalyBaselineSnapshottedEvent) OccurredAt ¶
func (e AnomalyBaselineSnapshottedEvent) OccurredAt() time.Time
type AnomalyCacheEvictedEvent ¶
type AnomalyCacheEvictedEvent struct {
UserEmail string
Days int
Reason string // "ttl_expired" / "size_overflow"
Timestamp time.Time
}
AnomalyCacheEvictedEvent is emitted when the cache drops a single entry for a reason other than user-scoped invalidation. Reason distinguishes the two eviction paths:
- "ttl_expired" — lazy eviction on Get() when storedAt+ttl < now
- "size_overflow" — random single-entry eviction on Set() when len(entries) >= maxEntries and the incoming key is net-new
Aggregate-ID rule: keyed by AnomalyCacheAggregateID(UserEmail). UserEmail may be empty for size_overflow when the evicted entry's key was not parseable — defence in depth, since cacheKey() is the only writer, but the event still carries enough to forensically trace the dropped slot.
func (AnomalyCacheEvictedEvent) EventType ¶
func (e AnomalyCacheEvictedEvent) EventType() string
func (AnomalyCacheEvictedEvent) OccurredAt ¶
func (e AnomalyCacheEvictedEvent) OccurredAt() time.Time
type AnomalyCacheInvalidatedEvent ¶
AnomalyCacheInvalidatedEvent is emitted when every cached UserOrderStats entry for a given user is purged — typically when a new place_order / modify_order row lands in the audit log so the next anomaly check sees the fresh data. Reason tags the trigger ("order_recorded" / "manual" / "admin_clear").
Aggregate-ID rule: keyed by AnomalyCacheAggregateID(UserEmail) — invalidation is per user, all days windows at once.
func (AnomalyCacheInvalidatedEvent) EventType ¶
func (e AnomalyCacheInvalidatedEvent) EventType() string
func (AnomalyCacheInvalidatedEvent) OccurredAt ¶
func (e AnomalyCacheInvalidatedEvent) OccurredAt() time.Time
type CompositeCondition ¶
type CompositeCondition struct {
Exchange string `json:"exchange"`
Tradingsymbol string `json:"tradingsymbol"`
InstrumentToken uint32 `json:"instrument_token"`
Operator Direction `json:"operator"`
Value float64 `json:"value"`
ReferencePrice float64 `json:"reference_price,omitempty"`
}
CompositeCondition is one leg of a composite alert. Every leg targets a distinct instrument with its own operator + threshold. `ReferencePrice` is required for percentage-change operators (drop_pct/rise_pct).
This type is the on-disk wire format for the `conditions_json` column. Tag renames are schema breaks — add a migration if you rename.
func NewCompositeConditionStrict ¶
func NewCompositeConditionStrict( exchange, tradingsymbol string, operator Direction, value, referencePrice float64, ) (CompositeCondition, error)
NewCompositeConditionStrict constructs a CompositeCondition with its invariants enforced: non-empty exchange + symbol, valid direction, and the same threshold / reference-price rules as ValidateAlertSpec. Returns a zero-value CompositeCondition on error so callers can't accidentally persist a half-valid leg.
instrumentToken is optional — it may be 0 when the caller hasn't resolved the token yet (composite-creation flow looks up tokens per leg after validation).
type CompositeLogic ¶
type CompositeLogic string
CompositeLogic describes how the per-leg Conditions are combined. AND = every leg must satisfy its condition simultaneously. ANY = any single leg firing triggers the composite.
const ( // CompositeLogicAnd requires every leg to satisfy its condition. CompositeLogicAnd CompositeLogic = "AND" // CompositeLogicAny fires as soon as any single leg's condition is met. CompositeLogicAny CompositeLogic = "ANY" )
type ConsentWithdrawnEvent ¶
type ConsentWithdrawnEvent struct {
Email string
EmailHash string
Reason string
Timestamp time.Time
}
ConsentWithdrawnEvent is emitted when a user invokes their DPDP §6(4) right to rescind previously-granted consent. The withdrawal does not erase the original grant from consent_log (the log is append-only and auditors need to see the full history); instead, the prior grant row gets stamped with withdrawn_at and a new "withdraw" action row is appended.
EmailHash is the SHA-256 hex digest of the lowercased email — same canonical form audit.HashEmail produces — so the event correlates to the audit log without leaking the plaintext email through the event dispatcher / persister chain. Plaintext Email is retained alongside for in-process consumers (Telegram notifier, riskguard) that need it for operational outreach. PR-D Item 2 will migrate other domain events to this dual-field shape.
func (ConsentWithdrawnEvent) EventType ¶
func (e ConsentWithdrawnEvent) EventType() string
func (ConsentWithdrawnEvent) OccurredAt ¶
func (e ConsentWithdrawnEvent) OccurredAt() time.Time
type Credential ¶
type Credential struct {
// contains filtered or unexported fields
}
Credential is the rich domain aggregate for a single user's Kite developer app credentials. It binds an email identity to an (APIKey, APISecret) pair and enforces construction invariants via value-object types and an email-presence check. Rotation-detection lives here so the infrastructure store stays a thin persistence gate.
func NewCredential ¶
func NewCredential(email string, apiKey APIKey, apiSecret APISecret) (Credential, error)
NewCredential constructs a Credential after validating all three fields. Email must be non-empty; apiKey and apiSecret must each be valid value objects. Email is normalised to lower-case so rotation detection matches the case-insensitive semantics of the persistence store.
func (Credential) APIKey ¶
func (c Credential) APIKey() APIKey
APIKey returns the credential's API key value object.
func (Credential) APISecret ¶
func (c Credential) APISecret() APISecret
APISecret returns the credential's API secret value object.
func (Credential) AppID ¶
func (c Credential) AppID() string
AppID codifies the Kite convention that "AppID = API key" for developer apps. Centralising it on the aggregate means downstream callers (store, registry backfill, admin UI) query one source of truth rather than hard-coding the equivalence.
func (Credential) Email ¶
func (c Credential) Email() string
Email returns the lower-cased email identity of this credential.
func (Credential) IsRotationOf ¶
func (c Credential) IsRotationOf(prior Credential) bool
IsRotationOf reports whether this credential represents a key rotation relative to prior. Rotation = same user (case-insensitive email) with a different APIKey. Used by the persistence store to trigger cached-token invalidation when a user replaces their developer-app credentials.
Same APIKey or different user returns false — only a deliberate key swap for an existing identity qualifies, mirroring the legacy credential_store.go:95 condition `existing.APIKey != stored.APIKey`.
type CredentialRegisteredEvent ¶
CredentialRegisteredEvent is emitted the first time a user registers Kite API credentials (no prior CredentialStore entry for this email). Distinct from CredentialRotatedEvent so auditors can tell onboarding from key rotation without walking the full credential history.
func (CredentialRegisteredEvent) EventType ¶
func (e CredentialRegisteredEvent) EventType() string
func (CredentialRegisteredEvent) OccurredAt ¶
func (e CredentialRegisteredEvent) OccurredAt() time.Time
type CredentialResolution ¶
type CredentialResolution struct {
// contains filtered or unexported fields
}
CredentialResolution carries the outcome of resolving credentials for a user — either a per-user pair, a global fallback, or nothing usable. The rules deciding which one applies live on this type's constructors so CredentialService can be a thin pass-through.
func ResolveCredentials ¶
func ResolveCredentials(perUser Credential, globalKey, globalSecret string) (CredentialResolution, bool)
ResolveCredentials applies the per-user-then-global fallback rule. The rule lives on the domain (not on a Service) so any caller — REST adapter, CLI, future SDK — gets identical behaviour by construction.
Return value: a CredentialResolution + a boolean. The boolean is `true` iff at least one of the two sources yielded a non-empty pair; `false` means the user can't authenticate at all and the caller should fail loudly.
Inputs:
- perUser: the per-user credential as resolved from the credential store. Pass the zero value when the user has nothing on record.
- globalKey/globalSecret: the server-wide creds (env-var sourced). Either may be empty; both empty + perUser empty → none.
func (CredentialResolution) APIKey ¶
func (r CredentialResolution) APIKey() string
APIKey returns the resolved key. Empty when Source == None.
func (CredentialResolution) APISecret ¶
func (r CredentialResolution) APISecret() string
APISecret returns the resolved secret. Empty when Source == None.
func (CredentialResolution) IsResolved ¶
func (r CredentialResolution) IsResolved() bool
IsResolved is the canonical "do we have anything to talk to Kite with" rule — replaces a sprinkle of "apiKey != \"\" && apiSecret != \"\"" boolean hand-rolling at call sites.
func (CredentialResolution) QualifiesForTrading ¶
func (r CredentialResolution) QualifiesForTrading(s Session) bool
QualifiesForTrading reports whether the resolved credentials, paired with a specific Kite session, are good enough to place an order. Three rules:
- The credential resolution itself must be Source != None.
- The associated Session must IsAuthenticated() (has a non-expired token cached for THIS user).
- The session's email matches the user we're resolving for — guards against cross-account reuse if a caller mixes up the inputs.
This is the single rule the trading layer needs to consult before place_order / modify_order / etc. Putting it on the domain object (not on CredentialService) means any layer with a Credential + a Session in hand can answer the question identically — no service indirection.
func (CredentialResolution) Source ¶
func (r CredentialResolution) Source() CredentialSource
Source returns where the resolution came from.
type CredentialRevokedEvent ¶
CredentialRevokedEvent is emitted when credentials are removed — via the dashboard DELETE endpoint, admin force-revoke, or the credentials half of DeleteMyAccount. Reason tags the lifecycle narrative ("user_self", "admin_revoke", "credential_rotation", "account_deleted").
func (CredentialRevokedEvent) EventType ¶
func (e CredentialRevokedEvent) EventType() string
func (CredentialRevokedEvent) OccurredAt ¶
func (e CredentialRevokedEvent) OccurredAt() time.Time
type CredentialRotatedEvent ¶
CredentialRotatedEvent is emitted when a user replaces existing Kite API credentials with a new key/secret pair. Emitted by UpdateMyCredentials when a prior credential entry exists for the email.
func (CredentialRotatedEvent) EventType ¶
func (e CredentialRotatedEvent) EventType() string
func (CredentialRotatedEvent) OccurredAt ¶
func (e CredentialRotatedEvent) OccurredAt() time.Time
type CredentialSource ¶
type CredentialSource int
CredentialSource enumerates where a resolved credential came from. Carrying the source on the resolution makes downstream telemetry, logging, and registry-sync decisions explicit instead of inferred.
const ( // CredentialSourceNone means no credentials are available for the // user; the caller must reject the operation or trigger onboarding. CredentialSourceNone CredentialSource = iota // CredentialSourcePerUser means the user brought their own Kite // developer-app credentials and we use those. CredentialSourcePerUser // CredentialSourceGlobal means the user is using the server's global // API key/secret (single-user / dev-mode deployments). CredentialSourceGlobal )
func (CredentialSource) String ¶
func (s CredentialSource) String() string
String returns a stable label suitable for logging.
type Event ¶
type Event interface {
// EventType returns a unique string identifier for the event kind.
EventType() string
// OccurredAt returns the timestamp when the event was created.
OccurredAt() time.Time
}
Event is the interface all domain events must satisfy.
type EventDispatcher ¶
type EventDispatcher struct {
// contains filtered or unexported fields
}
EventDispatcher is a simple in-process pub/sub for domain events. Handlers are called synchronously in the order they were registered. Use goroutines inside handlers if async processing is needed.
func NewEventDispatcher ¶
func NewEventDispatcher() *EventDispatcher
NewEventDispatcher creates a ready-to-use dispatcher.
func (*EventDispatcher) Dispatch ¶
func (d *EventDispatcher) Dispatch(event Event)
Dispatch sends an event to all registered handlers for its type. Handlers are called synchronously under a read lock, so Subscribe calls from within a handler will deadlock — use a goroutine if needed.
func (*EventDispatcher) Subscribe ¶
func (d *EventDispatcher) Subscribe(eventType string, handler func(Event))
Subscribe registers a handler for the given event type. The handler will be called every time an event of that type is dispatched.
type Family ¶
type Family struct {
// contains filtered or unexported fields
}
Family represents an admin's family-billing membership state at a specific moment. Construct via NewFamily; the value type is immutable after construction so it's safe to pass through layers without defensive copying.
AdminEmail is the lower-cased identity of the plan owner. CurrentSize is how many family members are currently linked. MaxSize is the cap from the admin's billing plan (1 for free / dev / no-plan).
func NewFamily ¶
NewFamily constructs a validated Family value. adminEmail must be non-empty (after trim+lower); currentSize and maxSize must be non-negative; maxSize must be >= 1 (every plan supports at least the admin themselves). Returns an error rather than panicking on bad input so callers can fail loudly.
func (Family) AdminEmail ¶
AdminEmail returns the lower-cased plan-owner email.
func (Family) AvailableSeats ¶
AvailableSeats returns how many more family members can be added without breaching the plan cap. Always >= 0.
func (Family) CanInvite ¶
CanInvite reports whether the admin has room for one more family member. The canonical rule for invite eligibility — call sites should not re-derive this from MemberCount() < MaxUsers().
func (Family) CurrentSize ¶
CurrentSize returns the number of currently-linked family members.
func (Family) IsAtCapacity ¶
IsAtCapacity reports whether the family has reached the plan limit. Equivalent to !CanInvite(); both names exist because call sites read more naturally one way or the other ("can we invite?" vs "are we full?").
func (Family) IsMemberOf ¶
IsMemberOf reports whether memberEmail belongs to this family. Email matching is case-insensitive.
type FamilyInvitedEvent ¶
FamilyInvitedEvent is emitted when an admin invites a family member.
func (FamilyInvitedEvent) EventType ¶
func (e FamilyInvitedEvent) EventType() string
func (FamilyInvitedEvent) OccurredAt ¶
func (e FamilyInvitedEvent) OccurredAt() time.Time
type FamilyMemberRemovedEvent ¶
FamilyMemberRemovedEvent is emitted when an admin unlinks a family member from their billing plan.
func (FamilyMemberRemovedEvent) EventType ¶
func (e FamilyMemberRemovedEvent) EventType() string
func (FamilyMemberRemovedEvent) OccurredAt ¶
func (e FamilyMemberRemovedEvent) OccurredAt() time.Time
type GTTDeletedEvent ¶
GTTDeletedEvent is emitted on successful broker deletion of a GTT. Pairs with GTTPlacedEvent under the same TriggerID — the place / modify / delete lifecycle replays from the aggregate stream.
func (GTTDeletedEvent) EventType ¶
func (e GTTDeletedEvent) EventType() string
func (GTTDeletedEvent) OccurredAt ¶
func (e GTTDeletedEvent) OccurredAt() time.Time
type GTTModifiedEvent ¶
type GTTModifiedEvent struct {
Email string
TriggerID int
Instrument InstrumentKey
TransactionType string
Product string
Type string
TriggerValue float64
Quantity float64 // see GTTPlacedEvent.Quantity comment
LimitPrice float64
UpperTriggerValue float64
UpperQuantity float64
UpperLimitPrice float64
LowerTriggerValue float64
LowerQuantity float64
LowerLimitPrice float64
Timestamp time.Time
}
GTTModifiedEvent is emitted on successful broker modification of a GTT. Carries the post-modify params; pre-modify state can be reconstructed by walking the GTT aggregate stream backward to the most recent GTTPlacedEvent / GTTModifiedEvent.
func (GTTModifiedEvent) EventType ¶
func (e GTTModifiedEvent) EventType() string
func (GTTModifiedEvent) OccurredAt ¶
func (e GTTModifiedEvent) OccurredAt() time.Time
type GTTPlacedEvent ¶
type GTTPlacedEvent struct {
Email string
TriggerID int
Instrument InstrumentKey
TransactionType string // "BUY" / "SELL"
Product string // CNC / MIS / NRML
Type string // "single" or "two-leg"
TriggerValue float64 // single-leg trigger
// Quantity is float64 to match the GTT cqrs commands (Kite GTT API
// allows fractional quantities for some derivative products).
Quantity float64 // single-leg quantity
LimitPrice float64 // single-leg limit price
UpperTriggerValue float64 // two-leg upper trigger
UpperQuantity float64 // two-leg upper quantity
UpperLimitPrice float64 // two-leg upper limit
LowerTriggerValue float64 // two-leg lower trigger
LowerQuantity float64 // two-leg lower quantity
LowerLimitPrice float64 // two-leg lower limit
Timestamp time.Time
}
GTTPlacedEvent is emitted on successful broker placement of a GTT (Good-Till-Triggered) order. Captures the trigger params verbatim from the Kite GTTParams so a forensic walk can reconstruct the trigger window without re-querying the broker.
For "single" type, only TriggerValue / Quantity / LimitPrice are meaningful. For "two-leg" type, Upper* and Lower* fields carry the upper / lower trigger pairs and the single-leg fields are unused.
Aggregate-ID rule: keyed by TriggerID via GTTAggregateID. Pairs with GTTRejectedEvent / GTTModifiedEvent / GTTDeletedEvent under the same TriggerID so the full GTT lifecycle replays as a coherent stream.
func (GTTPlacedEvent) EventType ¶
func (e GTTPlacedEvent) EventType() string
func (GTTPlacedEvent) OccurredAt ¶
func (e GTTPlacedEvent) OccurredAt() time.Time
type GTTRejectedEvent ¶
type GTTRejectedEvent struct {
Email string
TriggerID int // Kite-assigned GTT trigger ID; 0 for place failures
Source string // "place" / "modify" / "delete"
Reason string // broker error message, best-effort
Timestamp time.Time
}
GTTRejectedEvent is emitted when a GTT mutation fails at the broker round-trip. Source distinguishes the three GTT mutation paths:
- "place" — PlaceGTT broker call failed.
- "modify" — ModifyGTT broker call failed (e.g. trigger inactive).
- "delete" — DeleteGTT broker call failed (e.g. already triggered).
Aggregate-ID rule: when TriggerID is non-zero (modify/delete paths, where Kite assigned an int64 ID at GTT placement), the rejection joins the existing GTT aggregate stream stringified to "<id>" — same shape the success-path appendAuxEvent uses (kc/usecases/gtt_usecases.go). When TriggerID is 0 (place rejection — broker never assigned one), falls back to the synthetic "gtt-rejected:<email>:<rfc3339-nanos>" key.
func (GTTRejectedEvent) EventType ¶
func (e GTTRejectedEvent) EventType() string
func (GTTRejectedEvent) OccurredAt ¶
func (e GTTRejectedEvent) OccurredAt() time.Time
type GlobalFreezeEvent ¶
GlobalFreezeEvent is emitted when an admin activates the server-wide trading freeze.
func (GlobalFreezeEvent) EventType ¶
func (e GlobalFreezeEvent) EventType() string
func (GlobalFreezeEvent) OccurredAt ¶
func (e GlobalFreezeEvent) OccurredAt() time.Time
type GlobalFreezeReason ¶
type GlobalFreezeReason = string
GlobalFreezeReason documents why a server-wide freeze was activated.
type Holding ¶
type Holding struct {
// contains filtered or unexported fields
}
Holding is the rich domain entity for a broker portfolio holding. It wraps a broker.Holding DTO and exposes the same Money-aware accessor pattern Position uses (Slice 6) — PnL() returns INR-tagged Money, plus convenience computations (InvestedValue, CurrentValue) that previously lived as inline float multiplication at consumer sites.
The wrapped DTO is the source of truth; existing broker / persistence code continues to use broker.Holding directly. Use ToDomainHolding or NewHoldingFromBroker at the adapter boundary.
Slice 6b of the Money VO sweep: this wrapper is the keystone for migrating Holding.PnL consumers (currently bare-float reads in kc/usecases/widget_usecases.go and mcp/plugin_widget_returns_matrix.go) to type-tagged Money at the JSON-emit boundary. Mirrors the Position wrapper added in commits a926f8a / 5ce3eb0.
func NewHoldingFromBroker ¶
func NewHoldingFromBroker(b broker.Holding) Holding
NewHoldingFromBroker lifts a broker.Holding DTO into the rich domain entity. Identity-preserving — DTO() returns the original DTO unchanged.
func ToDomainHolding ¶
func ToDomainHolding(b broker.Holding) Holding
ToDomainHolding is a converter alias — identical to NewHoldingFromBroker, named for ergonomic use at adapter boundaries (matches the ToDomainPosition naming convention).
func (Holding) CurrentValue ¶
CurrentValue returns the mark-to-market value as Money — lastPrice times quantity, INR-tagged. The Slice 1 boundary pattern: aggregations of CurrentValue across many holdings can now flow through Money.Add (currency-aware) rather than the bare-float `total += h.LastPrice * float64(h.Quantity)` idiom.
func (Holding) DTO ¶
func (h Holding) DTO() broker.Holding
DTO returns the underlying broker DTO for passthrough to code that still consumes broker.Holding directly. Used by adapters and JSON-emit boundaries that need the bare-float wire shape.
func (Holding) InstrumentKey ¶
func (h Holding) InstrumentKey() InstrumentKey
InstrumentKey returns the canonical (EXCHANGE:SYMBOL) identifier for this holding's instrument. Useful when joining holdings with LTP maps keyed by instrument key. Same shape Position uses so a unified holdings+positions LTP fetch can key consistently.
func (Holding) InvestedValue ¶
InvestedValue returns the cost basis as Money — averagePrice times quantity, INR-tagged. Useful for portfolio summary computations that need the cost basis as a typed value (and for downstream Money.Add aggregation across multiple holdings when the consumer wants typed totals).
func (Holding) IsHeld ¶
IsHeld reports whether the holding has non-zero quantity. A holding row with Quantity == 0 may still appear (T+1 settlement quirks, recently-sold residuals) but is not "held" in the active-portfolio sense. Mirrors Position.IsOpen.
func (Holding) PnL ¶
PnL returns the broker-reported holding P&L as a Money value. As of Slice 6e c2, broker.Holding.PnL is already Money — the adapter (broker/zerodha/convert.go) wraps gokiteconnect's INR float at the boundary. This wrapper accessor passes through.
Sign is preserved: a winning position returns positive Money, a losing position returns negative Money. The zero Money is the "no movement / freshly bought" sentinel — callers that branch on win-vs-loss should use IsPositive / IsNegative / IsZero rather than comparing the raw Float64() against zero.
type InstrumentKey ¶
InstrumentKey is a value object identifying a tradable instrument by exchange and trading symbol. This is the canonical format used across the Kite Connect API ("NSE:RELIANCE", "BSE:INFY", "NFO:NIFTY25APRFUT").
func NewInstrumentKey ¶
func NewInstrumentKey(exchange, symbol string) InstrumentKey
NewInstrumentKey creates an InstrumentKey from an exchange and symbol without strict validation. Retained for existing callers (tests, legacy adapters) that feed in already-trusted broker data. New call sites that sit on the order-placement boundary should prefer NewInstrumentKeyStrict.
func NewInstrumentKeyStrict ¶
func NewInstrumentKeyStrict(exchange, symbol string) (InstrumentKey, error)
NewInstrumentKeyStrict constructs an InstrumentKey with construction invariants enforced: non-empty exchange, non-empty trading symbol, and exchange against the whitelist. Whitespace is trimmed; result is uppercase. Use at system boundaries (MCP tool handlers, order commands) to reject bad input at domain entry rather than surfacing ambiguous broker errors later.
func ParseInstrumentKey ¶
func ParseInstrumentKey(s string) (InstrumentKey, error)
ParseInstrumentKey parses a string like "NSE:RELIANCE" into an InstrumentKey. Returns an error if the format is invalid.
func (InstrumentKey) IsZero ¶
func (k InstrumentKey) IsZero() bool
IsZero returns true if the key is unset.
func (InstrumentKey) String ¶
func (k InstrumentKey) String() string
String returns the canonical "EXCHANGE:SYMBOL" representation.
type InstrumentRules ¶
type InstrumentRules struct {
// contains filtered or unexported fields
}
InstrumentRules bundles the lot-size and tick-size metadata for one instrument so a caller can run the two related checks without threading the primitives through every call site.
Construct with NewInstrumentRules; the Exchange + Tradingsymbol fields are informational (used in error messages) — actual whitelist validation lives on InstrumentKey.NewInstrumentKeyStrict.
func NewInstrumentRules ¶
func NewInstrumentRules(exchange, tradingsymbol string, lotSize int, tickSize float64) InstrumentRules
NewInstrumentRules constructs an InstrumentRules bundle. Called from the order-placement pipeline once the instrument metadata has been resolved via instruments.Manager.Find.
func (InstrumentRules) CheckPrice ¶
func (r InstrumentRules) CheckPrice(price float64) error
CheckPrice reports whether price is aligned to the instrument's tick size. Returns a descriptive error otherwise.
func (InstrumentRules) CheckQuantity ¶
func (r InstrumentRules) CheckQuantity(qty int) error
CheckQuantity reports whether qty satisfies the instrument's lot-size rule. Returns a descriptive error otherwise.
func (InstrumentRules) Exchange ¶
func (r InstrumentRules) Exchange() string
Exchange returns the exchange code (for error messages / diagnostics).
func (InstrumentRules) Tradingsymbol ¶
func (r InstrumentRules) Tradingsymbol() string
Tradingsymbol returns the symbol (for error messages / diagnostics).
type KiteSessionData ¶
type KiteSessionData struct {
Kite zerodha.KiteSDK // authenticated Kite SDK handle (interface — nil-checkable)
Broker broker.Client // broker-agnostic interface (wraps Kite via zerodha adapter)
Email string // Google-authenticated email (empty for local dev)
}
KiteSessionData is the transient runtime state of an authenticated MCP session: the broker SDK handle, the broker-port adapter, and the OAuth-resolved email. It lives in kc/domain (not kc parent) so kc/ports/session.go can reference it without importing the kc parent package — the Anchor 5 PR 5.6 / Wave B-1 inversion goal.
Anchor 5 PR 5.6 (per .research/anchor-5-prs-design.md): the original declaration in kc/manager.go:239 used `Kite *KiteConnect` where kc.KiteConnect was a thin one-field wrapper around zerodha.KiteSDK (see prior kc/manager.go:205-208). The wrapper carried no auth/ retry/caching/telemetry logic of its own, so this PR collapsed the indirection: callsites that previously did `kiteSession.Kite.Client. METHOD(...)` now do `kiteSession.Kite.METHOD(...)` — saving one dereference and severing the kc-parent dependency. kc/manager.go keeps `type KiteSessionData = domain.KiteSessionData` as a backward- compatibility alias so the 56-file reverse-dep set continues to compile via the legacy kc.KiteSessionData reference path.
The field types reach only already-extracted modules:
- zerodha.KiteSDK lives in github.com/zerodha/kite-mcp-server/ broker/zerodha (broker module — extracted at commit 5d74acf)
- broker.Client lives in github.com/zerodha/kite-mcp-server/ broker (same module)
Both are reached from kc/domain via the existing broker require/ replace in kc/domain/go.mod (no new replaces needed).
type KiteToken ¶
type KiteToken = string
KiteToken is the Kite Connect access token for broker API calls.
type MCPSessionID ¶
type MCPSessionID = string
MCPSessionID uniquely identifies an MCP protocol session on this server.
type MFOrderCancelledEvent ¶
MFOrderCancelledEvent is emitted on successful broker cancellation of an MF order. Aggregate-ID is OrderID — pairs with MFOrderPlacedEvent under the same key for full lifecycle replay.
func (MFOrderCancelledEvent) EventType ¶
func (e MFOrderCancelledEvent) EventType() string
func (MFOrderCancelledEvent) OccurredAt ¶
func (e MFOrderCancelledEvent) OccurredAt() time.Time
type MFOrderPlacedEvent ¶
type MFOrderPlacedEvent struct {
Email string
OrderID string
Tradingsymbol string
TransactionType string
Amount float64 // ISIN buy: rupees; sell-by-quantity: 0
Quantity float64 // sell-by-quantity: units; buy-by-amount: 0
Tag string
Timestamp time.Time
}
MFOrderPlacedEvent is emitted on successful broker placement of a one-off mutual-fund buy/sell order. Replaces the prior untyped appendAuxEvent("mf.order_placed", ...) payload. Carries the broker- assigned OrderID + the user-supplied params verbatim so a forensic walk can reconstruct the placement context without re-querying the broker.
Aggregate-ID rule: keyed by OrderID via MFAggregateID — pairs with MFOrderRejectedEvent (cancel-source) under the same key when both fire on the same broker order. Same shape as appendAuxEvent's existing aggregate ID for "mf.order_placed" so existing audit rows and new typed events sort under one stream.
func (MFOrderPlacedEvent) EventType ¶
func (e MFOrderPlacedEvent) EventType() string
func (MFOrderPlacedEvent) OccurredAt ¶
func (e MFOrderPlacedEvent) OccurredAt() time.Time
type MFOrderRejectedEvent ¶
type MFOrderRejectedEvent struct {
Email string
OrderID string // MF order ID or SIP ID; empty for place_* failures
Source string // "place_order" / "cancel_order" / "place_sip" / "cancel_sip"
Reason string // broker error message, best-effort
Timestamp time.Time
}
MFOrderRejectedEvent is emitted when a mutual-fund mutation fails at the broker round-trip — distinct from real OrderRejectedEvent (equity/derivatives) so projector consumers can filter MF surface without parsing OrderID prefixes (Kite assigns separate ID namespaces for MF orders, MF SIPs, and equity orders).
Source distinguishes the four MF mutation paths:
- "place_order" — PlaceMFOrder broker call failed (insufficient funds, fund not allowed, market closed for MF).
- "cancel_order" — CancelMFOrder broker call failed (already processed, not found).
- "place_sip" — PlaceMFSIP broker call failed.
- "cancel_sip" — CancelMFSIP broker call failed.
Aggregate-ID rule: when OrderID is non-empty (cancel paths, where the caller supplied an existing MF order/SIP ID), the rejection joins the MF aggregate stream rooted on that ID. When OrderID is empty (place rejection — broker never assigned one), falls back to the synthetic "mf-rejected:<email>:<rfc3339-nanos>" key so each rejection lands in its own slot. Mirrors OrderRejectedAggregateID's empty-ID handling.
func (MFOrderRejectedEvent) EventType ¶
func (e MFOrderRejectedEvent) EventType() string
func (MFOrderRejectedEvent) OccurredAt ¶
func (e MFOrderRejectedEvent) OccurredAt() time.Time
type MFSIPCancelledEvent ¶
MFSIPCancelledEvent is emitted on successful broker cancellation of an MF SIP. Aggregate-ID is SIPID — pairs with MFSIPPlacedEvent.
func (MFSIPCancelledEvent) EventType ¶
func (e MFSIPCancelledEvent) EventType() string
func (MFSIPCancelledEvent) OccurredAt ¶
func (e MFSIPCancelledEvent) OccurredAt() time.Time
type MFSIPPlacedEvent ¶
type MFSIPPlacedEvent struct {
Email string
SIPID string
Tradingsymbol string
Amount float64
Frequency string // "monthly", "weekly", etc.
Instalments int // -1 = perpetual; positive = fixed count
InitialAmount float64 // optional one-time first instalment
InstalmentDay int // day-of-month for monthly SIPs
Tag string
Timestamp time.Time
}
MFSIPPlacedEvent is emitted on successful broker creation of a new systematic investment plan. SIPs are recurring MF orders with their own ID namespace (SIPID, distinct from MFOrder OrderID), so they key under the SIPID via MFAggregateID — disjoint stream from one-off MF orders.
func (MFSIPPlacedEvent) EventType ¶
func (e MFSIPPlacedEvent) EventType() string
func (MFSIPPlacedEvent) OccurredAt ¶
func (e MFSIPPlacedEvent) OccurredAt() time.Time
type Money ¶
type Money = money.Money
Money is the canonical monetary value object. As of Slice 6e it lives in the leaf package kc/money so the broker package can import it without inverting the existing kc/domain → broker import direction (kc/domain itself imports broker for its rich Holding / Position / Order wrappers; broker → kc/domain would be a cycle).
This type alias preserves the established Slice 1-6d API surface — 65+ files, 372+ constructor sites — without churn. domain.Money is structurally identical to money.Money: fields (Amount, Currency), methods (Add, Sub, Multiply, GreaterThan, Float64, IsPositive, IsZero, IsNegative, String), and struct literals all keep working.
New code may import kc/money directly OR keep using kc/domain for the existing convention; both compile to the same type at the call site.
type NativeAlertDeletedEvent ¶
NativeAlertDeletedEvent is emitted on successful broker deletion of a native alert. One event per UUID — DeleteNativeAlertUseCase loops over UUIDs and emits per-alert (matching the existing appendAuxEvent loop shape).
func (NativeAlertDeletedEvent) EventType ¶
func (e NativeAlertDeletedEvent) EventType() string
func (NativeAlertDeletedEvent) OccurredAt ¶
func (e NativeAlertDeletedEvent) OccurredAt() time.Time
type NativeAlertModifiedEvent ¶
NativeAlertModifiedEvent is emitted on successful broker modification of an existing native alert. UUID is required — the modify use case validates it upfront.
func (NativeAlertModifiedEvent) EventType ¶
func (e NativeAlertModifiedEvent) EventType() string
func (NativeAlertModifiedEvent) OccurredAt ¶
func (e NativeAlertModifiedEvent) OccurredAt() time.Time
type NativeAlertPlacedEvent ¶
type NativeAlertPlacedEvent struct {
Email string
UUID string // optional — broker may assign lazily
Timestamp time.Time
}
NativeAlertPlacedEvent is emitted on successful broker creation of a server-side (Kite-native) price alert. Replaces the prior untyped appendAuxEvent("native_alert.placed", ...) emit.
UUID may be empty at place time — the broker assigns the alert UUID lazily and the immediate response doesn't always carry it. The aggregate ID falls back to email in that case (matching the prior PlaceNativeAlertUseCase behaviour where keying-by-email was the documented choice). Once the broker exposes the UUID via list, a future "native_alert.uuid_assigned" event could close the loop.
Distinct from AlertCreatedEvent (custom in-process alerts) which fires from kc/alerts/store.go on user-side alert creation.
func (NativeAlertPlacedEvent) EventType ¶
func (e NativeAlertPlacedEvent) EventType() string
func (NativeAlertPlacedEvent) OccurredAt ¶
func (e NativeAlertPlacedEvent) OccurredAt() time.Time
type NotSpec ¶
NotSpec negates the inner specification.
func (*NotSpec[T]) IsSatisfiedBy ¶
type OAuthToken ¶
type OAuthToken = string
OAuthToken is the JWT issued by this server's OAuth layer.
type OrSpec ¶
OrSpec is satisfied when at least one of Left or Right is satisfied.
func (*OrSpec[T]) IsSatisfiedBy ¶
type Order ¶
type Order struct {
// contains filtered or unexported fields
}
Order is the rich domain entity for a broker order. It wraps a broker.Order DTO (no duplicate field storage) and exposes lifecycle methods that encapsulate the business rules of an order's state transitions.
Consumers previously duplicated these checks inline (e.g. `if o.Status == "OPEN" || o.Status == "TRIGGER PENDING"`). The Order entity centralises them so that a change in status semantics (e.g. Kite adding a new intermediate state) requires a single edit here.
The wrapped DTO stays the source of truth — see DTO() for passthrough to existing broker / persistence code.
func NewOrderFromBroker ¶
func NewOrderFromBroker(b broker.Order) Order
NewOrderFromBroker lifts a broker.Order DTO into the rich domain entity.
func ToDomainOrder ¶
func ToDomainOrder(b broker.Order) Order
ToDomainOrder is a converter alias — identical to NewOrderFromBroker, named for ergonomic use at adapter boundaries. Example: `d := domain.ToDomainOrder(brokerOrder)`.
func (Order) CanCancel ¶
CanCancel reports whether the order is in a cancellable state. An order is cancellable only while it is resting in the book awaiting fill (OPEN) or awaiting its stop-loss trigger (TRIGGER PENDING).
func (Order) DTO ¶
func (o Order) DTO() broker.Order
DTO returns the underlying broker DTO for passthrough to code that still consumes broker.Order directly (persistence, broker adapters).
func (Order) FillPercentage ¶
FillPercentage returns the percentage of the order quantity that has been filled, in the range [0, 100]. Returns 0 when Quantity is zero (avoids a divide-by-zero) and clamps to 100 when FilledQuantity > Quantity (which should not happen but has been observed on partial-then-modified orders).
func (Order) IsCancelled ¶
IsCancelled reports whether the order reached the CANCELLED state. Case-insensitive.
func (Order) IsComplete ¶
IsComplete reports whether the order reached the COMPLETE state. Case-insensitive — matches the broker's reported status regardless of casing variations observed on edge transports.
func (Order) IsPending ¶
IsPending reports whether the order is in any non-terminal, in-flight state. This is the complement of IsTerminal excluding the zero-value/empty status, which is treated as neither pending nor terminal (unknown).
func (Order) IsRejected ¶
IsRejected reports whether the order reached the REJECTED state. Case-insensitive.
func (Order) IsTerminal ¶
IsTerminal reports whether the order has reached a terminal state and will not transition further. Terminal states are COMPLETE / CANCELLED / REJECTED.
type OrderCancelledEvent ¶
OrderCancelledEvent is emitted after an order is successfully cancelled.
func (OrderCancelledEvent) EventType ¶
func (e OrderCancelledEvent) EventType() string
func (OrderCancelledEvent) OccurredAt ¶
func (e OrderCancelledEvent) OccurredAt() time.Time
type OrderCandidate ¶
type OrderCandidate struct {
Quantity int
Price Money
Exchange string
Tradingsymbol string
TransactionType string // "BUY" or "SELL"
OrderType string // "MARKET", "LIMIT", "SL", "SL-M"
}
OrderCandidate bundles the fields needed for composite order validation. This is a transient value — not persisted, just passed through specs. Price is a Money value object (currency-aware); MARKET / SL-M orders carry the zero-Money by convention, and OrderSpec.IsSatisfiedBy skips the price check on those order types.
type OrderFilledEvent ¶
type OrderFilledEvent struct {
Email string
OrderID string
FilledQty Quantity
FilledPrice Money
Status string
Timestamp time.Time
}
OrderFilledEvent is emitted after an order is filled by the exchange.
Status carries the broker-reported terminal status (T4):
- "COMPLETE" — full quantity filled, single tranche. The pre-T4 default; fill_watcher only emitted on this case.
- "PARTIAL" — partial fill (multi-tranche execution or kill-fill timeout). Downstream consumers should treat FilledQty as the actual matched quantity, not the order's requested quantity.
- "AMO" — after-market order, queued for next session. Emitted when the broker accepts the order outside trading hours; the "fill" here is logical placement, not an exchange match.
Empty Status means the producer didn't set it (legacy emitters). New emit sites must set Status explicitly so the projection / activity feed can distinguish partial fills.
func (OrderFilledEvent) EventType ¶
func (e OrderFilledEvent) EventType() string
func (OrderFilledEvent) OccurredAt ¶
func (e OrderFilledEvent) OccurredAt() time.Time
type OrderFreezeReason ¶
type OrderFreezeReason = string
OrderFreezeReason documents why a per-user freeze was applied.
type OrderModifiedEvent ¶
OrderModifiedEvent is emitted after an order is successfully modified.
func (OrderModifiedEvent) EventType ¶
func (e OrderModifiedEvent) EventType() string
func (OrderModifiedEvent) OccurredAt ¶
func (e OrderModifiedEvent) OccurredAt() time.Time
type OrderPlacedEvent ¶
type OrderPlacedEvent struct {
Email string
OrderID string
Instrument InstrumentKey
Qty Quantity
Price Money
TransactionType string // "BUY" or "SELL"
Timestamp time.Time
}
OrderPlacedEvent is emitted after an order is successfully placed.
func (OrderPlacedEvent) EventType ¶
func (e OrderPlacedEvent) EventType() string
func (OrderPlacedEvent) OccurredAt ¶
func (e OrderPlacedEvent) OccurredAt() time.Time
type OrderPlacement ¶
type OrderPlacement struct {
// contains filtered or unexported fields
}
OrderPlacement is the DDD aggregate root for a not-yet-submitted order. It binds validated value objects (instrument, qty, price) with the transaction/order-type strings and enforces placement-time invariants at construction: price must be positive for non-MARKET orders, and transaction type must be BUY or SELL.
Service/use-case code constructs one of these and reads the validated fields back rather than re-checking primitives inline. Follow-on code (riskguard, broker call) operates on a known-good placement or an error — the placement cannot be in a halfway-valid state.
func NewOrderPlacement ¶
func NewOrderPlacement( instrument InstrumentKey, qty Quantity, price Money, transactionType string, orderType string, ) (OrderPlacement, error)
NewOrderPlacement constructs a validated OrderPlacement. All value objects are checked for validity; transaction type must be BUY or SELL; and for non-MARKET orders the price must be positive (Money.IsPositive). MARKET / SL-M orders carry a zero-value Money by convention — those are accepted.
func (OrderPlacement) Instrument ¶
func (p OrderPlacement) Instrument() InstrumentKey
Instrument returns the validated instrument key.
func (OrderPlacement) Notional ¶
func (p OrderPlacement) Notional() Money
Notional returns the placement's price × quantity as a Money value in the price's currency. For MARKET / SL-M orders the price is zero-Money by convention; Notional in those cases returns a zero-Money in the SAME currency as the configured price (which is INR by default for unset prices). Callers that need a meaningful notional for MARKET orders must estimate from LTP separately — this aggregate intentionally does not reach for live market data.
func (OrderPlacement) OrderType ¶
func (p OrderPlacement) OrderType() string
OrderType returns MARKET, LIMIT, SL, or SL-M.
func (OrderPlacement) Price ¶
func (p OrderPlacement) Price() Money
Price returns the placement's price. May be zero-value Money for MARKET / SL-M orders — check with Price().IsPositive() before use.
func (OrderPlacement) Quantity ¶
func (p OrderPlacement) Quantity() Quantity
Quantity returns the validated quantity value object.
func (OrderPlacement) TransactionType ¶
func (p OrderPlacement) TransactionType() string
TransactionType returns BUY or SELL.
type OrderRejectedEvent ¶
type OrderRejectedEvent struct {
Email string
OrderID string // may be empty for place_order failures (no broker-assigned ID)
ToolName string // "place_order" / "modify_order" / "cancel_order"
Reason string // broker error message, best-effort
Timestamp time.Time
}
OrderRejectedEvent is emitted when the broker fails an order mutation — place, modify, or cancel — for any reason that surfaces from the Kite API (rate limit, exchange reject, insufficient margin, invalid symbol, etc.). Captures the user-visible failure path on the order pipeline so the audit/projection stream isn't silent on broker rejections; previously these errors were logged but not event-sourced, leaving a hole in the order aggregate's lifecycle.
Distinct from RiskLimitBreachedEvent (pre-broker, riskguard-internal): OrderRejected fires only AFTER riskguard allowed the call AND the broker round-trip returned an error. Together the two events cover the two failure surfaces — internal pre-trade gates and external post-broker rejections — so an auditor walking the user's stream sees every reason an order didn't reach a placed/modified/cancelled terminal state.
OrderID may be empty when the rejection is on place_order — the broker never assigned an ID before failing. For modify/cancel the caller-supplied OrderID is preserved so the rejection joins the existing order aggregate stream. ToolName tags the failure surface ("place_order", "modify_order", "cancel_order") so projector queries can partition rejections by mutation type without parsing Reason.
Reason is the broker error message (best-effort string); kept verbose so a forensic walk can distinguish "MARGIN_INSUFFICIENT" from "RATE_LIMIT" without re-querying the broker. Caller is responsible for stripping PII from the error before emit (Kite errors don't carry user identifiers in practice — but the contract is "don't ship plaintext credentials").
func (OrderRejectedEvent) EventType ¶
func (e OrderRejectedEvent) EventType() string
func (OrderRejectedEvent) OccurredAt ¶
func (e OrderRejectedEvent) OccurredAt() time.Time
type OrderSpec ¶
type OrderSpec struct {
QtySpec *QuantitySpec
PriceSpec *PriceSpec
// contains filtered or unexported fields
}
OrderSpec composes quantity and price specs into a single order-level check. Additional rules (valid exchange, valid order type) are included.
func NewOrderSpec ¶
func NewOrderSpec(qtySpec *QuantitySpec, priceSpec *PriceSpec) *OrderSpec
NewOrderSpec creates a composite order specification.
func (*OrderSpec) IsSatisfiedBy ¶
func (s *OrderSpec) IsSatisfiedBy(o OrderCandidate) bool
type PaperOrderRejectedEvent ¶
type PaperOrderRejectedEvent struct {
Email string
OrderID string
Reason string // human-readable rejection reason (cash shortage, LTP unavailable)
Source string // "place_market" / "place_limit" / "fill_immediate" / "fill_monitor"
Timestamp time.Time
}
PaperOrderRejectedEvent is emitted when the paper-trading engine rejects a virtual order. Sources:
- "place_market" — MARKET order rejected because LTP unavailable (no LTP provider, instrument not subscribed).
- "place_limit" — LIMIT BUY rejected at place time because the notional exceeds the user's cash balance.
- "fill_immediate" — fillOrder rejected a BUY because the snap-to- LTP price exceeded cash balance (covers the market path and marketable-LIMIT path).
- "fill_monitor" — background monitor rejected a queued BUY at fill time when cash dropped below required notional between place and fill.
Distinct event type from real OrderRejectedEvent so projector consumers (activity feeds, dashboards) can filter "real broker rejection" vs "virtual-account rejection" without parsing OrderID prefixes — paper IDs use "PAPER_<n>" but relying on prefix-sniffing for projection-side classification is fragile, so we surface the distinction at the event type itself.
Aggregate-ID rule: keyed by OrderID via PaperOrderAggregateID. Paper order IDs ("PAPER_<n>") are already process-unique via the atomic orderSeq counter in kc/papertrading/engine.go, so no email prefix is needed to disambiguate. Empty OrderID falls back to "paper:unknown" — never observed in practice (orderID is assigned before any rejection branch) but defence in depth keeps malformed dispatches out of real rows.
func (PaperOrderRejectedEvent) EventType ¶
func (e PaperOrderRejectedEvent) EventType() string
func (PaperOrderRejectedEvent) OccurredAt ¶
func (e PaperOrderRejectedEvent) OccurredAt() time.Time
type PaperTradingDisabledEvent ¶
PaperTradingDisabledEvent is emitted on successful deactivation of a user's paper-trading account. Pairs with PaperTradingEnabledEvent under the same email-keyed aggregate.
func (PaperTradingDisabledEvent) EventType ¶
func (e PaperTradingDisabledEvent) EventType() string
func (PaperTradingDisabledEvent) OccurredAt ¶
func (e PaperTradingDisabledEvent) OccurredAt() time.Time
type PaperTradingEnabledEvent ¶
PaperTradingEnabledEvent is emitted on successful activation of a user's paper-trading account. Captures InitialCash so a forensic walk can reconstruct the seed condition without re-querying the engine. Replaces the prior untyped appendAuxEvent("paper.enabled", ...) emit.
Aggregate-ID rule: keyed by email via PaperTradingAggregateID. The full enable -> N resets -> disable lifecycle replays under one stream. Disjoint from PaperOrderRejectedEvent (per-paper-order) which keys by OrderID.
func (PaperTradingEnabledEvent) EventType ¶
func (e PaperTradingEnabledEvent) EventType() string
func (PaperTradingEnabledEvent) OccurredAt ¶
func (e PaperTradingEnabledEvent) OccurredAt() time.Time
type PaperTradingResetEvent ¶
PaperTradingResetEvent is emitted on successful reset of the virtual portfolio (cash balance back to InitialCash, positions / holdings cleared). Pairs with PaperTradingEnabledEvent / PaperTradingDisabledEvent under the same email-keyed aggregate so a full lifecycle replays cleanly.
func (PaperTradingResetEvent) EventType ¶
func (e PaperTradingResetEvent) EventType() string
func (PaperTradingResetEvent) OccurredAt ¶
func (e PaperTradingResetEvent) OccurredAt() time.Time
type PluginRegisteredEvent ¶
PluginRegisteredEvent is emitted when a plugin binary is registered for hot-reload watching via WatchPluginBinary. Captures the (plugin-name, path) pair so the plugin-watcher aggregate stream lets auditors / read-side projectors reconstruct the full set of currently- watched binaries by replaying the (registered, unregistered) pairs.
PluginName is the logical plugin identifier (typically the basename of Path, derived inside WatchPluginBinary). Path is the absolute filesystem path that subscribed to fsnotify events. Both fields are required at emit time.
func (PluginRegisteredEvent) EventType ¶
func (e PluginRegisteredEvent) EventType() string
func (PluginRegisteredEvent) OccurredAt ¶
func (e PluginRegisteredEvent) OccurredAt() time.Time
type PluginReloadTriggeredEvent ¶
PluginReloadTriggeredEvent is emitted when the watcher debounce timer fires Close() on a registered BinaryReloadable, signaling that the plugin's subprocess will be relaunched on the next tool invocation. This is the workhorse event of the plugin-watcher aggregate: every dev-loop rebuild surfaces here, giving the audit log an explicit reload boundary that read-side projectors can use to compute "reloads/hour by plugin" without scanning fsnotify-level traces.
func (PluginReloadTriggeredEvent) EventType ¶
func (e PluginReloadTriggeredEvent) EventType() string
func (PluginReloadTriggeredEvent) OccurredAt ¶
func (e PluginReloadTriggeredEvent) OccurredAt() time.Time
type PluginUnregisteredEvent ¶
PluginUnregisteredEvent is emitted when a plugin path is removed from the watcher's registry — currently only via ClearPluginWatches (test path), but reserved for a future production unregister API. Pairs with PluginRegisteredEvent so the watcher aggregate stream is closed (registered + unregistered events have matching Path).
func (PluginUnregisteredEvent) EventType ¶
func (e PluginUnregisteredEvent) EventType() string
func (PluginUnregisteredEvent) OccurredAt ¶
func (e PluginUnregisteredEvent) OccurredAt() time.Time
type PluginWatcherStartedEvent ¶
PluginWatcherStartedEvent is emitted exactly once per StartPluginBinaryWatcher invocation that actually starts the watcher goroutine (idempotent re-Start calls are silent). Captures the fsnotify watcher coming online so the aggregate stream has explicit "watcher up" boundaries between reload bursts.
func (PluginWatcherStartedEvent) EventType ¶
func (e PluginWatcherStartedEvent) EventType() string
func (PluginWatcherStartedEvent) OccurredAt ¶
func (e PluginWatcherStartedEvent) OccurredAt() time.Time
type PluginWatcherStoppedEvent ¶
PluginWatcherStoppedEvent is emitted exactly once per StopPluginBinaryWatcher invocation that actually stops a running watcher (no-op stops on never-started watchers are silent). Pairs with PluginWatcherStartedEvent so the aggregate stream's lifecycle transitions are symmetric.
func (PluginWatcherStoppedEvent) EventType ¶
func (e PluginWatcherStoppedEvent) EventType() string
func (PluginWatcherStoppedEvent) OccurredAt ¶
func (e PluginWatcherStoppedEvent) OccurredAt() time.Time
type Position ¶
type Position struct {
// contains filtered or unexported fields
}
Position is the rich domain entity for a broker trading position. It wraps a broker.Position DTO and exposes behaviour that was previously duplicated: IsIntraday (product == "MIS"), Direction (LONG/SHORT/FLAT), RealizedPnL, UnrealizedPnL, and InstrumentKey projection.
The wrapped DTO is the source of truth; existing broker / persistence code continues to use broker.Position directly. Use ToDomainPosition or NewPositionFromBroker at the adapter boundary.
func NewPositionFromBroker ¶
func NewPositionFromBroker(b broker.Position) Position
NewPositionFromBroker lifts a broker.Position DTO into the rich domain entity.
func ToDomainPosition ¶
func ToDomainPosition(b broker.Position) Position
ToDomainPosition is a converter alias — identical to NewPositionFromBroker, named for ergonomic use at adapter boundaries.
func (Position) DTO ¶
func (p Position) DTO() broker.Position
DTO returns the underlying broker DTO for passthrough to code that still consumes broker.Position directly.
func (Position) Direction ¶
Direction returns "LONG" when quantity > 0, "SHORT" when quantity < 0, and "FLAT" when quantity == 0.
func (Position) InstrumentKey ¶
func (p Position) InstrumentKey() InstrumentKey
InstrumentKey returns the canonical (EXCHANGE:SYMBOL) identifier for this position's instrument. Useful when joining positions with LTP maps keyed by instrument key.
func (Position) IsIntraday ¶
IsIntraday reports whether the position is intraday-only (MIS product). MIS positions are force-squared-off at market close unless converted to NRML/CNC.
func (Position) IsOpen ¶
IsOpen reports whether the position still has non-zero quantity. A flat position (Quantity == 0) may still appear in the broker's position list because it carried realized P&L earlier in the session, but it is not "open" in the sense of carrying market exposure.
func (Position) PnL ¶
PnL returns the broker-reported net P&L as a Money value. As of Slice 6e c2, broker.Position.PnL is already Money — the adapter wraps gokiteconnect's INR float at the boundary. This accessor passes through.
Prefer UnrealizedPnL when you want to compute P&L at a specific LTP that may differ from the broker's last-seen LTP.
func (Position) UnrealizedPnL ¶
UnrealizedPnL computes the mark-to-market P&L at the given LTP as (ltp - averagePrice) * quantity, wrapped in a Money value.
For a long position (quantity > 0), profit accrues when ltp rises above averagePrice. For a short position (quantity < 0), the sign flips naturally because the quantity factor is negative — no special-casing required.
If ltp is in a different currency than INR, the result carries the LTP's currency. Callers may normalise via Money.Add error handling.
type PositionClosedEvent ¶
type PositionClosedEvent struct {
Email string
OrderID string // the closing order's ID
Instrument InstrumentKey
Product string // MIS, CNC, NRML — part of aggregate key
Qty Quantity
TransactionType string // opposite direction used to close
Timestamp time.Time
}
PositionClosedEvent is emitted after a position is closed via close_position. OrderID is the closing order (fresh from Kite), not the opening one — use PositionAggregateID() to join with the corresponding open event.
func (PositionClosedEvent) EventType ¶
func (e PositionClosedEvent) EventType() string
func (PositionClosedEvent) OccurredAt ¶
func (e PositionClosedEvent) OccurredAt() time.Time
type PositionConvertedEvent ¶
type PositionConvertedEvent struct {
Email string
Instrument InstrumentKey
TransactionType string // "BUY" or "SELL" (the position direction being converted)
Quantity int
OldProduct string // MIS, CNC, NRML — pre-conversion product
NewProduct string // MIS, CNC, NRML — post-conversion product
PositionType string // "day" or "overnight" — Kite Convert API param
Timestamp time.Time
}
PositionConvertedEvent is emitted when a user converts a position from one product type to another (e.g. CNC->MIS for intraday squaring, MIS->CNC to carry forward overnight). Replaces the prior untyped appendAuxEvent("position.converted", map[string]any{...}) emit in kc/usecases/convert_position.go so the audit stream uses a typed domain.Event with stable field names — projector consumers no longer need to type-assert against an opaque map[string]any payload.
Aggregate-ID rule: keyed by (email, exchange, tradingsymbol, OLD product) via PositionConvertedAggregateID. Using the OLD product (not the new) means a CNC->MIS->CNC sequence threads through a stable aggregate stream rooted on the original holding's product. Stable across reverse conversions, matches the pre-ES key shape so existing rows aren't orphaned by the migration.
PositionType ("day" / "overnight") is preserved verbatim from the Kite ConvertPosition API param so a forensic walk can distinguish intraday squaring from carry-forward conversions without re-querying position state.
func (PositionConvertedEvent) EventType ¶
func (e PositionConvertedEvent) EventType() string
func (PositionConvertedEvent) OccurredAt ¶
func (e PositionConvertedEvent) OccurredAt() time.Time
type PositionOpenedEvent ¶
type PositionOpenedEvent struct {
Email string
PositionID string // opening order ID (historical trace only)
Instrument InstrumentKey
Product string // MIS, CNC, NRML — part of aggregate key
Qty Quantity
AvgPrice Money
TransactionType string // "BUY" or "SELL"
Timestamp time.Time
}
PositionOpenedEvent is emitted when a new position is opened.
Aggregate-ID rule: positions do not have a broker-assigned unique ID — Kite's Position struct doesn't expose an "opening order" field — so we can't reliably join open and close events by ID. Instead, we key position events by the natural tuple (email, exchange, symbol, product) via PositionAggregateID() below. A single user-instrument-product aggregate may contain multiple open→close lifecycles across time; walking the event stream makes lifecycle boundaries visible.
Product is required for the aggregate ID. PositionID is kept for tracing — it equals the opening order ID from place_order — but is no longer the aggregate key.
func (PositionOpenedEvent) EventType ¶
func (e PositionOpenedEvent) EventType() string
func (PositionOpenedEvent) OccurredAt ¶
func (e PositionOpenedEvent) OccurredAt() time.Time
type PriceSpec ¶
type PriceSpec struct {
MaxPrice Money // zero-Money means no upper bound
// contains filtered or unexported fields
}
PriceSpec validates that a price is positive and within an optional ceiling. A zero-Money MaxPrice means no upper bound. Currency-aware: a candidate price in a different currency than the configured ceiling is rejected with an explanatory reason — never silently coerced.
func NewPriceSpec ¶
NewPriceSpec creates a price specification with the given ceiling. Pass a zero-Money (e.g. domain.Money{}) for no upper bound; the standard constructor for "no bound" is the literal zero value, which IsSatisfiedBy treats as "skip ceiling check" via IsZero.
func (*PriceSpec) IsSatisfiedBy ¶
IsSatisfiedBy reports whether the candidate price is positive and (when the spec has a non-zero ceiling) does not exceed MaxPrice. Cross-currency comparisons are rejected — a USD ceiling cannot validate an INR candidate, so the spec records a currency-mismatch reason rather than performing implicit conversion.
type Profile ¶
type Profile struct {
UserID string
UserName string
Email string
Broker string
Exchanges []string
Products []string
}
Profile is the domain-layer representation of a trading account's identity + capability surface. Matches the broker.Profile wire DTO field-for-field so NewProfileFromBroker is a pure refcopy, but adds business-meaning methods that live on the entity rather than being duplicated in every tool handler that needs them.
Scope:
- Exchange/product capability checks — used by pretrade validation, compliance reporting, and the admin user page to show "what can this user actually trade?". Previously callers would string-search profile.Exchanges[] inline (4 prod sites and counting) and do a case-sensitive comparison, producing subtle bugs when Kite returns "NSE" but config uses "nse".
Explicitly NOT modeled here — and NOT a DDD gap:
- IsKycComplete / AccountTenure / IsCorporateAccount — the Kite Profile response carries NONE of these fields (no kyc_status, no account_created_at, no account_type). Modeling them today would be speculative entities that hold no data. If Zerodha's API ever exposes these, add them to broker.Profile first, then the methods land here trivially.
func NewProfileFromBroker ¶
func NewProfileFromBroker(p broker.Profile) Profile
NewProfileFromBroker converts a broker.Profile DTO into the rich domain entity. Zero-value input returns a zero-value Profile — not an error — because Profile is a read-side snapshot; the "invalid profile" condition is the absence of one, not a malformed one.
func (Profile) HasCommodityAccess ¶
HasCommodityAccess reports whether the profile can trade commodity futures (MCX). Separate enablement from equity derivatives.
func (Profile) HasDerivativesAccess ¶
HasDerivativesAccess reports whether the profile can trade F&O (NFO, BFO). Segment-enabled brokers have explicit NFO activation; default retail accounts don't.
func (Profile) HasEquityAccess ¶
HasEquityAccess reports whether the profile can trade on any equity exchange (NSE or BSE). Convenience predicate used by the pretrade check to short-circuit stock orders for F&O-only accounts.
func (Profile) IsIntradayEligible ¶
IsIntradayEligible reports whether the account can place intraday orders. Accounts with MIS product access can trade intraday; pure delivery accounts cannot.
func (Profile) IsZerodhaAccount ¶
IsZerodhaAccount returns true if the broker name identifies a Zerodha Kite account. Useful for segment-specific code paths that depend on Kite-only features like ATO alerts.
func (Profile) SupportsExchange ¶
SupportsExchange reports whether the account can trade on the given exchange (NSE, BSE, NFO, BFO, MCX, ...). Case-insensitive because Kite responses are upper-case but user input + configs are mixed.
func (Profile) SupportsProduct ¶
SupportsProduct reports whether the account can place orders with the given product type (CNC, MIS, NRML, MTF, ...). Case-insensitive for the same reason as SupportsExchange.
type Quantity ¶
type Quantity struct {
// contains filtered or unexported fields
}
Quantity is a value object representing an order quantity. It is always a positive integer — zero and negative quantities are invalid.
func NewQuantity ¶
NewQuantity creates a validated Quantity. Returns an error if v <= 0.
func (Quantity) IsValid ¶
IsValid returns true if the quantity has a positive value. A zero-value Quantity (e.g., from var q Quantity) is invalid.
func (Quantity) MarshalJSON ¶
MarshalJSON encodes the quantity as a JSON number.
func (*Quantity) UnmarshalJSON ¶
UnmarshalJSON decodes a JSON number into a Quantity. Accepts zero for cases where the quantity is intentionally unset (e.g., command defaults).
type QuantitySpec ¶
QuantitySpec validates that an integer quantity falls within [Min, Max]. Min defaults to 1 if zero; Max of 0 means no upper bound.
func NewQuantitySpec ¶
func NewQuantitySpec(min, max int) *QuantitySpec
NewQuantitySpec creates a quantity specification with the given bounds.
func (*QuantitySpec) IsSatisfiedBy ¶
func (s *QuantitySpec) IsSatisfiedBy(qty int) bool
func (*QuantitySpec) Reason ¶
func (s *QuantitySpec) Reason() string
type RiskLimitBreachedEvent ¶
type RiskLimitBreachedEvent struct {
Email string
Reason string // matches riskguard.RejectionReason values
Message string
ToolName string
Timestamp time.Time
}
RiskLimitBreachedEvent is emitted when riskguard blocks an order.
func (RiskLimitBreachedEvent) EventType ¶
func (e RiskLimitBreachedEvent) EventType() string
func (RiskLimitBreachedEvent) OccurredAt ¶
func (e RiskLimitBreachedEvent) OccurredAt() time.Time
type RiskguardDailyCounterResetEvent ¶
RiskguardDailyCounterResetEvent is emitted when riskguard rolls a user's per-day counters (DailyOrderCount, DailyPlacedValue) on the 9:15 AM IST trading-day boundary. Mostly observable for forensic timeline replay — auditors who walk the counters aggregate stream see explicit reset boundaries rather than having to infer them from gaps in the event log.
Reason is currently always "trading_day_boundary"; reserved as a string so future paths (admin-forced reset, end-of-week rollover) can tag themselves without bumping the event schema.
func (RiskguardDailyCounterResetEvent) EventType ¶
func (e RiskguardDailyCounterResetEvent) EventType() string
func (RiskguardDailyCounterResetEvent) OccurredAt ¶
func (e RiskguardDailyCounterResetEvent) OccurredAt() time.Time
type RiskguardKillSwitchTrippedEvent ¶
type RiskguardKillSwitchTrippedEvent struct {
UserEmail string // empty for global kill-switch (the canonical case)
FrozenBy string
Reason string
Active bool // true=tripped (frozen), false=lifted (unfrozen)
Timestamp time.Time
}
RiskguardKillSwitchTrippedEvent is emitted when riskguard's global trading freeze (a.k.a. the "kill switch") is engaged at the riskguard layer — distinct from the admin-tool-level GlobalFreezeEvent which records the admin command. This event captures the riskguard state mutation itself (kill switch went 0→1), making the counters aggregate's freeze lifecycle fully reconstructable from the event stream.
Active is true when the kill switch was tripped (off→on); when an operator unfreezes globally, a separate event with Active=false is emitted so the on/off transitions are symmetric in the audit log.
FrozenBy carries the operator/system tag passed into FreezeGlobal — typically an admin email but may be a synthetic tag like "riskguard:auto" in future auto-trip scenarios.
func (RiskguardKillSwitchTrippedEvent) EventType ¶
func (e RiskguardKillSwitchTrippedEvent) EventType() string
func (RiskguardKillSwitchTrippedEvent) OccurredAt ¶
func (e RiskguardKillSwitchTrippedEvent) OccurredAt() time.Time
type RiskguardRejectionEvent ¶
RiskguardRejectionEvent is emitted when the circuit-breaker's sliding rejection window for a user is incremented (recordRejection called). Distinct from RiskLimitBreachedEvent which is emitted at the use-case layer when an order is blocked: this event captures the COUNTER mutation inside the riskguard aggregate, so the counters aggregate stream can be replayed to reconstruct the auto-freeze state machine without joining against the order pipeline.
Reason mirrors the RejectionReason that drove the recordRejection call (e.g. "order_value_limit", "daily_value_limit", "anomaly_high"). Used by future read-side projectors that count by reason to surface "user X is hitting the order_value cap repeatedly" without scanning the full audit log.
func (RiskguardRejectionEvent) EventType ¶
func (e RiskguardRejectionEvent) EventType() string
func (RiskguardRejectionEvent) OccurredAt ¶
func (e RiskguardRejectionEvent) OccurredAt() time.Time
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is the rich domain entity representing a user's Kite broker session — specifically, the authenticated access token and the metadata needed to reason about its validity.
Kite access tokens expire once per trading day at 06:00 IST, regardless of when they were issued. A token issued at 09:15 IST will therefore live for ~20h 45m, while a token issued at 05:59 IST will live for just 1 minute. The IsExpired / TokenAgeHours methods encapsulate this broker-specific rule so that callers (middleware, scheduled jobs, briefing, dashboards) do not need to re-derive the expiry calendar themselves.
This entity intentionally wraps a flat DTO (SessionData) rather than the kc.KiteTokenEntry struct, so that kc/domain retains zero upward deps. Converters live in kc (see kc.ToDomainSession) to map between the two.
func NewSessionFromData ¶
func NewSessionFromData(d SessionData) Session
NewSessionFromData constructs a Session from its flat DTO.
func ToDomainSession ¶
func ToDomainSession(d SessionData) Session
ToDomainSession is a converter alias — identical to NewSessionFromData, named for ergonomic use at adapter boundaries.
func (Session) AccessToken ¶
AccessToken returns the underlying Kite access token value.
func (Session) DTO ¶
func (s Session) DTO() SessionData
DTO returns the underlying session DTO for passthrough.
func (Session) HasToken ¶
HasToken reports whether the session carries a non-empty access token. Useful as a cheap pre-check before deciding whether re-auth is required.
func (Session) IsAuthenticated ¶
IsAuthenticated is the wall-clock variant of IsAuthenticatedAt.
func (Session) IsAuthenticatedAt ¶
IsAuthenticatedAt reports whether the session can be used to call the Kite API *at the given moment*: a token is present AND it has not crossed the daily 06:00 IST refresh boundary. Collapses the two-step `hasToken && !IsExpired` check that was previously duplicated in OAuth middleware and lifecycle tests.
func (Session) IsExpired ¶
IsExpired reports whether the session has passed the daily 06:00 IST refresh boundary. Compared against time.Now.
Rule: a token is expired if it was issued before today's 06:00 IST (or before yesterday's 06:00 IST if the current time is itself before today's 06:00 IST — in that case the "last expiry tick" was yesterday).
Mirrors kc.IsKiteTokenExpired but lives on the rich entity so consumers can write `session.IsExpired()` instead of threading the StoredAt timestamp through a package-level function.
func (Session) IsExpiredAt ¶
IsExpiredAt is the testable variant of IsExpired — takes "now" explicitly so unit tests don't need to monkey-patch time.Now. Production callers should prefer IsExpired().
func (Session) TokenAgeHours ¶
TokenAgeHours returns how many hours have elapsed since IssuedAt, computed against time.Now. Negative values (IssuedAt in the future) are clamped to 0. Used for display ("token is 3.2 hours old") and for alerting on unusually fresh/stale tokens in admin tools.
type SessionClearedEvent ¶
type SessionClearedEvent struct {
SessionID string
Reason string // "post_credential_register" / "profile_check_failed" / "admin_action"
Timestamp time.Time
}
SessionClearedEvent is emitted when the Kite session data attached to an MCP session is cleared (without terminating the session itself). Phase C ES: append-only audit record of the clear, keyed by session ID.
func (SessionClearedEvent) EventType ¶
func (e SessionClearedEvent) EventType() string
func (SessionClearedEvent) OccurredAt ¶
func (e SessionClearedEvent) OccurredAt() time.Time
type SessionCreatedEvent ¶
type SessionCreatedEvent struct {
Email string
SessionID string
Broker string // "zerodha", "angelone", etc.
Timestamp time.Time
}
SessionCreatedEvent is emitted when a new MCP session is established.
func (SessionCreatedEvent) EventType ¶
func (e SessionCreatedEvent) EventType() string
func (SessionCreatedEvent) OccurredAt ¶
func (e SessionCreatedEvent) OccurredAt() time.Time
type SessionData ¶
type SessionData struct {
// Email is the OAuth-authenticated email that owns the session.
Email string
// AccessToken is the Kite Connect access token value. The domain entity
// does not use it directly — included so callers that need to pass the
// session through to a broker client retain one-stop access.
AccessToken string
// IssuedAt is the wall-clock time the token was obtained / stored.
// Must be in a timezone that can be normalised to IST for the 06:00 rule.
IssuedAt time.Time
}
SessionData is the flat DTO view of a Kite session. Matches the relevant subset of kc.KiteTokenEntry; additional fields (UserID, UserName) are irrelevant to expiry reasoning and are omitted here.
type SessionID ¶
type SessionID struct {
// contains filtered or unexported fields
}
SessionID is the value object for an MCP session identifier. It is always non-empty — the NewSessionID constructor is the only way to produce a valid one. Callers that receive a zero-value SessionID (e.g., var id SessionID) can detect it via IsValid().
func NewSessionID ¶
NewSessionID constructs a validated SessionID. Empty IDs are rejected so that downstream services don't have to re-check the invariant.
type SessionInvalidatedEvent ¶
type SessionInvalidatedEvent struct {
SessionID string
Reason string // "expired" / "admin_action" / "logout"
Timestamp time.Time
}
SessionInvalidatedEvent is emitted when an MCP session is ended (the SessionRegistry entry is evicted, cleanup hooks run). Distinct from SessionClearedEvent which keeps the session alive without broker data.
func (SessionInvalidatedEvent) EventType ¶
func (e SessionInvalidatedEvent) EventType() string
func (SessionInvalidatedEvent) OccurredAt ¶
func (e SessionInvalidatedEvent) OccurredAt() time.Time
type Spec ¶
type Spec[T any] interface { // IsSatisfiedBy returns true if the candidate meets this specification. IsSatisfiedBy(candidate T) bool // Reason returns a human-readable explanation when IsSatisfiedBy returns false. // Callers may use this to build rejection messages. Reason() string }
Spec is a predicate over a value of type T. Implementations encode a single business rule ("quantity is within lot limits", "price is positive", "order passes all pre-trade checks").
type TelegramChatBoundEvent ¶
type TelegramChatBoundEvent struct {
UserEmail string
OldChatID int64
NewChatID int64
Timestamp time.Time
}
TelegramChatBoundEvent is emitted when an existing Telegram subscriber re-binds to a different chat ID (rotation) — e.g. the user lost access to the old chat or moved to a new device. OldChatID and NewChatID are both captured so projector consumers can render the rotation history without joining against a separate snapshot.
Stays silent for no-op writes (same chat ID in, same chat ID out) so the event log reflects real state transitions, not redundant setup-tool replays. Pattern mirrors TierChangedEvent's "real transitions only" contract.
Aggregate-ID rule: same as TelegramSubscribedEvent — keyed by TelegramSubscriptionAggregateID(UserEmail). The full chat-bind lifecycle for a user lives under one aggregate stream regardless of how many times the chat ID rotates.
func (TelegramChatBoundEvent) EventType ¶
func (e TelegramChatBoundEvent) EventType() string
func (TelegramChatBoundEvent) OccurredAt ¶
func (e TelegramChatBoundEvent) OccurredAt() time.Time
type TelegramSubscribedEvent ¶
TelegramSubscribedEvent is emitted the first time a user binds a Telegram chat ID to their account (no prior chat-ID mapping in the alerts.Store). Distinct from TelegramChatBoundEvent so auditors can tell first-time onboarding from re-binding without walking the full Telegram-subscription history.
Aggregate-ID rule: keyed by TelegramSubscriptionAggregateID(UserEmail) — one logical Telegram subscription per user. ChatID can change over time (re-bind to a new chat, e.g. user lost device); the email is the stable aggregate identifier.
func (TelegramSubscribedEvent) EventType ¶
func (e TelegramSubscribedEvent) EventType() string
func (TelegramSubscribedEvent) OccurredAt ¶
func (e TelegramSubscribedEvent) OccurredAt() time.Time
type TierChangedEvent ¶
type TierChangedEvent struct {
UserEmail string
FromTier int
ToTier int
Amount Money
Reason string
Timestamp time.Time
}
TierChangedEvent is emitted when a user's billing subscription tier transitions — free→paid (upgrade), paid→free (cancellation), or paid→paid (cross-grade between Pro/Premium/SoloPro). Emitted from the billing.Store on every successful SetSubscription call where the effective tier differs from the prior persisted tier. Stays silent for no-op writes (same tier in, same tier out) so the audit log reflects real state transitions, not redundant webhook replays.
FromTier and ToTier are integer codes matching billing.Tier (0=Free, 1=Pro, 2=Premium, 3=SoloPro). Stored as int rather than the typed billing.Tier to keep kc/domain free of an upward dependency on kc/billing — the convention mirrors UserFrozenEvent.FrozenBy / Reason which carry semantic strings without importing their producer.
Reason tags the lifecycle narrative ("stripe_checkout", "stripe_subscription_updated", "stripe_subscription_deleted", "admin_set_billing_tier") so auditors can distinguish webhook-driven changes from operator-driven changes without joining against another table.
Amount (Money VO Slice 4) is the to-tier's canonical monthly INR amount — the same value billing.TierMonthlyINR(ToTier) returns. Free transitions surface zero Money (IsZero() = true), paid tiers surface the published list price. Audit consumers compute MRR delta by summing Amount across emitted events without a join into billing. Stays a Money value rather than a bare float64 so a future multi- currency split (USD enterprise, INR retail) trips a currency-mismatch error in any downstream Add/GreaterThan call rather than silently corrupting the figure.
func (TierChangedEvent) EventType ¶
func (e TierChangedEvent) EventType() string
func (TierChangedEvent) OccurredAt ¶
func (e TierChangedEvent) OccurredAt() time.Time
type TrailingStopCancelledEvent ¶
TrailingStopCancelledEvent is emitted on successful deactivation of a trailing stop. Pairs with TrailingStopSetEvent under the same TrailingStopID for full lifecycle replay.
func (TrailingStopCancelledEvent) EventType ¶
func (e TrailingStopCancelledEvent) EventType() string
func (TrailingStopCancelledEvent) OccurredAt ¶
func (e TrailingStopCancelledEvent) OccurredAt() time.Time
type TrailingStopSetEvent ¶
type TrailingStopSetEvent struct {
Email string
TrailingStopID string
Instrument InstrumentKey
OrderID string // the SL order being trailed
Variety string // "regular" / "co" / etc.
Direction string // "long" or "short"
TrailAmount float64 // absolute trail in rupees
TrailPct float64 // percentage trail
CurrentStop float64 // initial SL trigger price
ReferencePrice float64 // HWM at activation
Timestamp time.Time
}
TrailingStopSetEvent is emitted on successful creation of a new trailing stop. Replaces the prior untyped appendAuxEvent payload with a stable typed schema. ReferencePrice is the HighWaterMark at activation (the price the trailing window anchors on); both TrailAmount and TrailPct are preserved verbatim because the user may set either form (the manager picks the one it sees as positive in evaluateOne).
Aggregate-ID rule: keyed by TrailingStopID via TrailingStopAggregateID — pairs with TrailingStopTriggeredEvent (per-trigger event) and TrailingStopCancelledEvent under the same key, giving the trailing stop's full set->triggers->cancel lifecycle a coherent aggregate stream.
func (TrailingStopSetEvent) EventType ¶
func (e TrailingStopSetEvent) EventType() string
func (TrailingStopSetEvent) OccurredAt ¶
func (e TrailingStopSetEvent) OccurredAt() time.Time
type TrailingStopTriggeredEvent ¶
type TrailingStopTriggeredEvent struct {
Email string
TrailingStopID string
OrderID string // the SL order being modified
Instrument InstrumentKey
Direction string // "long" or "short"
OldStop float64
NewStop float64
HighWaterMark float64 // best price seen since activation (post-update)
ModifyCount int // total modifications fired so far (post-increment)
Timestamp time.Time
}
TrailingStopTriggeredEvent is emitted when a trailing-stop fires — i.e. evaluateOne in kc/alerts/trailing.go decides the SL order's trigger price needs to move (long: HWM rose enough; short: HWM fell enough) AND the post-modify Kite call succeeded. Captures the full state transition (oldStop -> newStop, ModifyCount) plus the underlying SL OrderID so a forensic walk of the SL order ID sees trailing-stop modifications inline with place/modify/cancel events.
Failures of the underlying ModifyOrder broker call don't fire this event (the trailing stop's CurrentStop wasn't actually moved on the broker side). A future TrailingStopRejectedEvent could close that loop — flagged as follow-up.
Aggregate-ID rule: keyed by TrailingStopID via TrailingStopAggregateID. The trailing-stop ID is uuid-derived (8-char prefix from kc/alerts/trailing.go Add()), stable across the trailing-stop's full lifecycle (set -> N triggers -> cancel), so projector consumers see the trigger sequence under one aggregate.
func (TrailingStopTriggeredEvent) EventType ¶
func (e TrailingStopTriggeredEvent) EventType() string
func (TrailingStopTriggeredEvent) OccurredAt ¶
func (e TrailingStopTriggeredEvent) OccurredAt() time.Time
type UserFrozenEvent ¶
type UserFrozenEvent struct {
Email string
FrozenBy string // "admin", "riskguard:circuit-breaker"
Reason string
Timestamp time.Time
}
UserFrozenEvent is emitted when a user's trading is frozen (manual or auto).
func (UserFrozenEvent) EventType ¶
func (e UserFrozenEvent) EventType() string
func (UserFrozenEvent) OccurredAt ¶
func (e UserFrozenEvent) OccurredAt() time.Time
type UserSuspendedEvent ¶
type UserSuspendedEvent struct {
Email string
By string // admin email
Reason string
Timestamp time.Time
}
UserSuspendedEvent is emitted when an admin suspends a user account.
func (UserSuspendedEvent) EventType ¶
func (e UserSuspendedEvent) EventType() string
func (UserSuspendedEvent) OccurredAt ¶
func (e UserSuspendedEvent) OccurredAt() time.Time
type WatchlistCreatedEvent ¶
type WatchlistCreatedEvent struct {
Email string
WatchlistID string
Name string
Timestamp time.Time
}
WatchlistCreatedEvent is emitted when a new watchlist is created.
func (WatchlistCreatedEvent) EventType ¶
func (e WatchlistCreatedEvent) EventType() string
func (WatchlistCreatedEvent) OccurredAt ¶
func (e WatchlistCreatedEvent) OccurredAt() time.Time
type WatchlistDeletedEvent ¶
type WatchlistDeletedEvent struct {
Email string
WatchlistID string
Name string // captured before deletion for audit trail
ItemCount int // captured before deletion so auditors see the scope
Timestamp time.Time
}
WatchlistDeletedEvent is emitted when a watchlist is deleted.
func (WatchlistDeletedEvent) EventType ¶
func (e WatchlistDeletedEvent) EventType() string
func (WatchlistDeletedEvent) OccurredAt ¶
func (e WatchlistDeletedEvent) OccurredAt() time.Time
type WatchlistItemAddedEvent ¶
type WatchlistItemAddedEvent struct {
Email string
WatchlistID string
Instrument InstrumentKey
Timestamp time.Time
}
WatchlistItemAddedEvent is emitted when an instrument is added to a watchlist.
func (WatchlistItemAddedEvent) EventType ¶
func (e WatchlistItemAddedEvent) EventType() string
func (WatchlistItemAddedEvent) OccurredAt ¶
func (e WatchlistItemAddedEvent) OccurredAt() time.Time
type WatchlistItemRemovedEvent ¶
type WatchlistItemRemovedEvent struct {
Email string
WatchlistID string
ItemID string
Timestamp time.Time
}
WatchlistItemRemovedEvent is emitted when an instrument is removed from a watchlist.
func (WatchlistItemRemovedEvent) EventType ¶
func (e WatchlistItemRemovedEvent) EventType() string
func (WatchlistItemRemovedEvent) OccurredAt ¶
func (e WatchlistItemRemovedEvent) OccurredAt() time.Time