papertrading

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: May 16, 2026 License: MIT Imports: 15 Imported by: 0

README

kite-mcp-papertrading

Go Reference

Paper trading virtual portfolio engine for the algo2go ecosystem. Provides middleware interception of order placement to redirect to an in-memory portfolio with ₹1 crore default cash, background monitor for LIMIT fills, store with foreign-key integrity, leak-sentinel for goroutine cleanup, and integration with kite-mcp-riskguard for the same pre-trade safety checks as live trading.

Used by Sundeepg98/kite-mcp-server across kc/manager_, app/, kc/ops/* (paper handlers), kc/telegram/* (paper-mode commands), mcp/* (paper tool group, helpers test).

Why a separate module?

Paper trading is an end-user feature applicable to any algo2go consumer that wants risk-free strategy testing without committing real capital. Hosting as its own module:

  • Centralizes the Engine + Store + Monitor + Middleware contracts
  • Lets virtual-portfolio policies (default cash, slippage rules, fill-watcher cadence) version independently
  • Decouples paper-mode middleware from any one MCP server runtime

Stability promise

v0.x — unstable. Pin v0.1.0 deliberately.

Install

go get github.com/algo2go/kite-mcp-papertrading@v0.1.0

Public API

  • Engine — orchestrates order placement, fills, P&L, cash accounting
  • Store — in-memory portfolio + orders + positions with foreign-key integrity
  • Monitor — background LIMIT fill watcher; configurable cadence
  • Middleware — middleware-mode interception of place_order tool calls; routes to engine instead of broker
  • Riskguard integration — same 8+ safety checks as live trading

Dependencies

  • github.com/algo2go/kite-mcp-alerts v0.1.0
  • github.com/algo2go/kite-mcp-broker v0.1.0 (incl. /mock subpkg)
  • github.com/algo2go/kite-mcp-domain v0.1.0
  • github.com/algo2go/kite-mcp-logger v0.1.0
  • github.com/algo2go/kite-mcp-oauth v0.1.0
  • github.com/algo2go/kite-mcp-riskguard v0.1.0
  • github.com/stretchr/testify v1.10.0

All algo2go deps published; no upstream replace directives needed.

Reference consumer

Sundeepg98/kite-mcp-server — consumed across 18 .go files: kc/manager_, kc/interfaces.go, kc/broker_services.go, kc/ops/ (paper handlers, dashboard credentials, admin dashboard, api handlers), kc/telegram/* (handler, commands, bot edge), app/wire.go, app/app.go, mcp/helpers_test.go, mcp/tools_session_test.go.

License

MIT — see LICENSE.

Authors

Original design: Sundeepg98 (Zerodha Tech). Multi-module promotion (2026-05-10): algo2go contributors.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Middleware

func Middleware(engine *PaperEngine) server.ToolHandlerMiddleware

Middleware returns MCP tool handler middleware that intercepts order and portfolio tools when the user has paper trading enabled. Non-paper users and unrecognised tools pass through to the real handler.

Types

type Account

type Account struct {
	Email       string
	Enabled     bool
	InitialCash domain.Money
	CashBalance domain.Money
	CreatedAt   time.Time
	ResetAt     time.Time
}

Account represents a paper trading account for a user.

InitialCash and CashBalance use the domain.Money value object so the engine fails fast on cross-currency comparisons rather than silently coercing — same Slice 1 pattern as UserLimits.Max*INR. The zero Money (Amount=0, Currency="") is the "empty / not yet funded" sentinel; once EnableAccount runs, both fields hold an INR Money. SQLite REAL columns stay; we bind via .Float64() and rehydrate via domain.NewINR(scanned) on Scan so the wire / persistence shape is unchanged.

type Holding

type Holding struct {
	Email         string
	Exchange      string
	Tradingsymbol string
	Quantity      int
	AveragePrice  domain.Money
	LastPrice     domain.Money
	PnL           domain.Money
}

Holding represents a paper trading CNC holding.

All monetary fields are domain.Money (Slice 6a). Same semantic as Position; weighted-average on additional buys via the Money pipeline.

type LTPProvider

type LTPProvider interface {
	GetLTP(instruments ...string) (map[string]float64, error)
}

LTPProvider fetches last-traded prices for instruments. Instrument format: "EXCHANGE:TRADINGSYMBOL" (e.g. "NSE:RELIANCE").

type Monitor

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

Monitor polls for OPEN paper orders and fills them when the LTP crosses the limit/trigger price. It runs as a background goroutine.

Wave D Phase 3 Package 3 (Logger sweep): logger is typed as the kc/logger.Logger port. The goroutine uses context.Background() at each log call site — Monitor is a long-lived service with no request ctx; future ctx threading would tie it to a parent app ctx captured by Start.

func NewMonitor

func NewMonitor(engine *PaperEngine, interval time.Duration, logger *slog.Logger) *Monitor

NewMonitor creates a new background monitor for the paper trading engine. Accepts *slog.Logger for caller compatibility (app/wire.go:712); the logger is converted to the kc/logger.Logger port at the boundary.

func (*Monitor) Start

func (m *Monitor) Start()

Start launches the monitor goroutine. Calling Start more than once is a programming error; the redundant goroutine leaks. Callers should create a new Monitor instead.

func (*Monitor) Stop

func (m *Monitor) Stop()

Stop signals the monitor goroutine to exit and waits for it to terminate. It is safe to call Stop multiple times — only the first call closes the stop channel and waits for the loop; subsequent calls are no-ops (sync.Once guard).

If Start was never called, Stop is a pure no-op and returns immediately.

type Order

type Order struct {
	OrderID         string
	Email           string
	Exchange        string
	Tradingsymbol   string
	TransactionType string // BUY/SELL
	OrderType       string // MARKET/LIMIT/SL/SL-M
	Product         string // CNC/MIS/NRML
	Variety         string
	Quantity        int
	Price           domain.Money
	TriggerPrice    domain.Money
	Status          string // OPEN/COMPLETE/CANCELLED/REJECTED
	FilledQuantity  int
	AveragePrice    domain.Money
	PlacedAt        time.Time
	FilledAt        time.Time
	Tag             string
}

Order represents a paper trading order.

All monetary fields use domain.Money (Slice 6b complete):

  • Price: user-supplied LIMIT/SL limit price; zero Money is the "MARKET / no price set" sentinel (Slice 2 OrderCheckRequest.Price pattern).
  • TriggerPrice: SL / SL-M trigger threshold; zero Money is the "no trigger set" sentinel (LIMIT / MARKET orders never set it).
  • AveragePrice: broker-reported fill price; zero Money is the "unfilled" sentinel matching the Slice 6a Position.LastPrice pre-refresh shape.

SQLite REAL columns unchanged; bind via .Float64(), scan via domain.NewINR. JSON wire format unchanged at the orderToMap + trades-list seams (.Float64() at the boundary).

type PaperEngine

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

PaperEngine orchestrates virtual trading logic against a Store.

Wave D Phase 3 Package 3 (Logger sweep): logger is typed as the kc/logger.Logger port. NewEngine accepts *slog.Logger for caller compatibility (app/wire.go:482) and converts at the boundary via logport.NewSlog. Internal log calls use the ctx-aware port API with context.Background() — paper-engine methods don't carry a request ctx today, but the port abstraction positions the package for ctx threading in a later sweep.

func NewEngine

func NewEngine(store *Store, logger *slog.Logger) *PaperEngine

NewEngine creates a new PaperEngine.

func (*PaperEngine) CancelOrder

func (e *PaperEngine) CancelOrder(email, orderID string) (map[string]any, error)

CancelOrder cancels an open paper order.

func (*PaperEngine) Disable

func (e *PaperEngine) Disable(email string) error

Disable deactivates paper trading for the given email.

func (*PaperEngine) Enable

func (e *PaperEngine) Enable(email string, initialCash float64) error

Enable activates paper trading for the given email with the specified initial cash.

func (*PaperEngine) GetHoldings

func (e *PaperEngine) GetHoldings(email string) (any, error)

GetHoldings returns all paper holdings in Kite API format.

func (*PaperEngine) GetMargins

func (e *PaperEngine) GetMargins(email string) (any, error)

GetMargins returns paper margin info in Kite API format.

func (*PaperEngine) GetOrders

func (e *PaperEngine) GetOrders(email string) (any, error)

GetOrders returns all paper orders in Kite API format.

func (*PaperEngine) GetPositions

func (e *PaperEngine) GetPositions(email string) (any, error)

GetPositions returns all paper positions in Kite API format.

func (*PaperEngine) IsEnabled

func (e *PaperEngine) IsEnabled(email string) bool

IsEnabled returns whether paper trading is enabled for the given email.

func (*PaperEngine) ModifyOrder

func (e *PaperEngine) ModifyOrder(email, orderID string, params map[string]any) (map[string]any, error)

ModifyOrder modifies an open paper order's price, quantity, or order type.

func (*PaperEngine) PlaceOrder

func (e *PaperEngine) PlaceOrder(email string, params map[string]any) (map[string]any, error)

PlaceOrder places a paper order. Returns a Kite-compatible response map.

func (*PaperEngine) Reset

func (e *PaperEngine) Reset(email string) error

Reset clears all paper trading data and resets cash to the initial amount.

func (*PaperEngine) SetDispatcher

func (e *PaperEngine) SetDispatcher(d *domain.EventDispatcher)

SetDispatcher wires the paper engine to the shared domain event dispatcher so paper fills emit OrderPlacedEvent + OrderFilledEvent + PositionOpenedEvent into the same pipeline as live trades. Safe to leave nil in tests that don't need the audit trail.

func (*PaperEngine) SetLTPProvider

func (e *PaperEngine) SetLTPProvider(p LTPProvider)

SetLTPProvider sets the LTP provider used for market price lookups.

func (*PaperEngine) Status

func (e *PaperEngine) Status(email string) (map[string]any, error)

Status returns a summary of the paper trading account.

type Position

type Position struct {
	Email         string
	Exchange      string
	Tradingsymbol string
	Product       string
	Quantity      int // positive=long, negative=short
	AveragePrice  domain.Money
	LastPrice     domain.Money
	PnL           domain.Money
}

Position represents an open paper trading position.

All monetary fields are domain.Money (Slice 6a complete):

  • AveragePrice (commit 2): weighted-average via Money.Multiply / Add chains; resets to fillPrice on side-flip.
  • LastPrice (commit 1): LTP refreshed from the LTPProvider.
  • PnL (commit 3): derived value = Quantity * (LastPrice - AveragePrice). Negative for losing trades; sign preserved through Money.Sub + Money.Multiply.

SQLite REAL columns unchanged; bind via .Float64(), scan via domain.NewINR.

type Store

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

Store provides SQLite persistence for paper trading data.

Wave D Phase 3 Package 3: logger is typed as the kc/logger.Logger port. NewStore takes *slog.Logger for caller compatibility (app/wire.go:478) and converts via logport.NewSlog at the boundary.

func NewStore

func NewStore(db *alerts.DB, logger *slog.Logger) *Store

NewStore creates a new paper trading store backed by the given alerts DB.

func (*Store) DeletePosition

func (s *Store) DeletePosition(email, exchange, symbol, product string) error

DeletePosition removes a paper position.

func (*Store) DisableAccount

func (s *Store) DisableAccount(email string) error

DisableAccount disables paper trading for the given email.

func (*Store) EnableAccount

func (s *Store) EnableAccount(email string, initialCash float64) error

EnableAccount creates or updates a paper trading account with the given initial cash.

func (*Store) GetAccount

func (s *Store) GetAccount(email string) (*Account, error)

GetAccount retrieves the paper trading account for the given email. Returns nil, nil if no account exists.

SQLite REAL → domain.Money rehydration: the underlying column type is REAL (float64); we scan into local floats and wrap with domain.NewINR so the returned Account presents Money values to engine code. Boundary pattern matches kc/riskguard/limits.go LoadLimits (Slice 1).

func (*Store) GetAllOpenOrders

func (s *Store) GetAllOpenOrders() ([]*Order, error)

GetAllOpenOrders returns all OPEN paper orders across all users.

func (*Store) GetHoldings

func (s *Store) GetHoldings(email string) ([]*Holding, error)

GetHoldings returns all paper holdings for the given email.

SQLite REAL → domain.Money rehydration mirrors GetPositions.

func (*Store) GetOpenOrders

func (s *Store) GetOpenOrders(email string) ([]*Order, error)

GetOpenOrders returns all OPEN paper orders for the given email.

func (*Store) GetOrder

func (s *Store) GetOrder(orderID string) (*Order, error)

GetOrder returns a single order by ID.

func (*Store) GetOrders

func (s *Store) GetOrders(email string) ([]*Order, error)

GetOrders returns all paper orders for the given email, most recent first.

func (*Store) GetPositions

func (s *Store) GetPositions(email string) ([]*Position, error)

GetPositions returns all paper positions for the given email.

SQLite REAL → domain.Money rehydration for AveragePrice, LastPrice, PnL. Mirrors the GetAccount pattern from Slice 5.

func (*Store) InitTables

func (s *Store) InitTables() error

InitTables creates the paper trading tables if they don't already exist.

Foreign-key invariant (DDD aggregate boundary): paper_orders, paper_positions, and paper_holdings are children of the paper_accounts aggregate root. Each child row's email MUST reference an existing paper_accounts(email). The constraint is enforced by SQLite when foreign_keys=ON (set per-connection via the DSN _pragma=foreign_keys(1) — see kc/alerts/db.go:dsnWithFKPragma).

ON DELETE CASCADE: deleting an account row purges all dependent orders / positions / holdings atomically. In practice the application never hard- deletes paper_accounts (DeleteMyAccountUseCase calls Reset+Disable, which preserves the row with enabled=0); the cascade is a defence-in-depth invariant against schema-bypassing DELETEs.

CREATE TABLE IF NOT EXISTS is idempotent only on a fresh schema — existing pre-FK databases (Fly.io prod) keep their old constraint-less tables. New environments and all tests get the FK-enforced schema. Schema-as-truth going forward.

func (*Store) InsertOrder

func (s *Store) InsertOrder(o *Order) error

InsertOrder inserts a new paper trading order.

SQLite REAL boundary: Order.Price + Order.AveragePrice are domain.Money; bind via .Float64() at the SQL parameter sites so column types stay REAL.

func (*Store) ResetAccount

func (s *Store) ResetAccount(email string) error

ResetAccount deletes all orders, positions, and holdings for the given email and resets the cash balance to the initial amount.

func (*Store) UpdateCashBalance

func (s *Store) UpdateCashBalance(email string, balance float64) error

UpdateCashBalance updates the cash balance for the given account.

func (*Store) UpdateOrderStatus

func (s *Store) UpdateOrderStatus(orderID, status string, filledQty int, avgPrice float64) error

UpdateOrderStatus updates the status, filled quantity, and average price of an order.

func (*Store) UpsertHolding

func (s *Store) UpsertHolding(h *Holding) error

UpsertHolding inserts or updates a paper holding.

SQLite REAL boundary: AveragePrice, LastPrice, PnL are all domain.Money; bind via .Float64() at the SQL parameter sites.

func (*Store) UpsertPosition

func (s *Store) UpsertPosition(p *Position) error

UpsertPosition inserts or updates a paper position.

SQLite REAL boundary: AveragePrice, LastPrice, PnL are all domain.Money; bind via .Float64() at the SQL parameter sites so column types stay REAL.

Jump to

Keyboard shortcuts

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