kalshi

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 20 Imported by: 0

README

kalshi-go

Go Reference CI

Unofficial Go client library for the Kalshi exchange API (trade-api v2). Not affiliated with Kalshi.

This client was extracted from a production trading system that runs it against the live exchange daily, including the unglamorous edges (cursor escaping, fractional counterparty fills, malformed-record degradation). Several of the design notes below were paid for with live incidents.

Status

Beta. The REST surface below has accumulated live production usage, but the public API of this module may still shift until v0.1.0 is tagged. WebSocket streams are not implemented yet — see ROADMAP.md.

Install

go get github.com/jaracah/kalshi-go

Requires Go 1.23+. Zero dependencies outside the standard library.

Full API documentation is on pkg.go.dev.

Public market data (no credentials)

import kalshi "github.com/jaracah/kalshi-go"

c := kalshi.NewClient(nil)

markets, err := c.DiscoverActive(ctx, "KXBTCD")      // open markets in a series
market, err := c.FetchMarket(ctx, "KXBTCD-26JUL1612-T110000")

yesBid, yesAsk, err := c.FetchOrderbook(ctx, ticker)   // live top-of-book, cents
yes, no, err := c.FetchOrderbookDepth(ctx, ticker)     // full depth

trades, err := c.FetchTrades(ctx, ticker)              // complete public tape
settled, err := c.FetchSettled(ctx, "KXBTCD", 500)   // settled history, cursor-paged
candles, err := c.FetchCandlesticks(ctx, series, ticker, startTS, endTS, 1)

A client built with NewClient is strictly read-only: it holds no credentials and never touches a trading endpoint.

Note one live-vs-cached subtlety: the market summary endpoint (FetchMarket, DiscoverActive) is CloudFront-cached and its quote fields freeze at the market's open time. The orderbook endpoint is not cached — use FetchOrderbook/FetchOrderbookDepth for live quotes.

Trading (authenticated)

Generate an API key in your Kalshi account settings. You get a key ID and an RSA private key (PEM); requests are signed with KALSHI-ACCESS-* headers automatically per the published spec.

signer, err := kalshi.NewSigner(os.Getenv("KALSHI_KEY_ID"), os.Getenv("KALSHI_PRIVATE_KEY"))
c := kalshi.NewAuthedClient(nil, signer, "") // "" = production; kalshi.DemoBaseURL = demo

// Cheapest authenticated call — a good startup credentials check
bal, err := c.Balance(ctx) // cents

// Take: buy 10 YES at 55¢, immediate-or-cancel
res, err := c.CreateOrder(ctx, kalshi.Order{
    Ticker:        "KXBTCD-26JUL1612-T110000",
    Side:          kalshi.SideBid,
    Count:         10,
    PriceC:        55,
    ClientOrderID: newUUID(), // required; makes retries safe
})
fmt.Println(res.FillCount, res.AvgPriceC, res.TotalFeeC)

// Make: rest a post-only quote (sell YES at 95¢ == buy NO at 5¢)
res, err = c.CreateOrder(ctx, kalshi.Order{
    Ticker:        "KXBTCD-26JUL1612-T110000",
    Side:          kalshi.SideAsk,
    Count:         5,
    PriceC:        95,
    TimeInForce:   kalshi.TIFGoodTillCanceled,
    PostOnly:      true, // reject instead of crossing (ErrPostOnlyCross)
    ClientOrderID: newUUID(),
})

open, err := c.OpenOrders(ctx)                       // every resting order
cres, err := c.CancelOrder(ctx, res.OrderID)         // 404 → ErrOrderNotFound
fills, err := c.Fills(ctx, since)                    // async fill discovery
positions, err := c.Positions(ctx)                   // cursor-paged, whole book

Orders use the V2 single-book vocabulary: SideBid buys YES, SideAsk sells YES, and prices always quote the YES side (a NO bid at q¢ is the same order as a YES ask at 100−q¢).

Design notes

  • Prices parse to integer cents (PriceC, YesBid, …) because binary contracts trade in 1–99¢ and integer math avoids float drift in P&L.
  • Counts come in two views. Orders placed through this client are whole contracts, but Kalshi counterparties trade fractional contracts, so every count the exchange reports about your orders can come back fractional (a resting 5-lot really does report "4.82"). Fields carry both a whole-contract view (Remaining is the ceiling of the exact count, Filled its floor — together they reproduce a whole-contract ledger exactly) and the exchange's exact value (RemainingFP, FilledFP, NetYesFP, …).
  • ClientOrderID is required and is what makes retries safe: the exchange dedupes on it, so a 429 or an ambiguous transport failure can be retried without double-filling. All requests retry 429s with linear backoff.
  • Strict where money is counted, tolerant where it isn't. A filled order whose average_fill_price is unusable is an error, never a 0¢ cost basis. Account-wide reads (OpenOrders, Positions, Fills) instead degrade per item: an unparseable record reaches you flagged via its Malformed field rather than failing the page, because those reads back reconciliation and emergency-stop paths that must keep working even when one record is poison.
  • Typed sentinel errors for the two ambiguous outcomes a trading loop must branch on: ErrPostOnlyCross (benign — the quote didn't rest) and ErrOrderNotFound (the order is gone; resolve fill-vs-cancel through Fills, never by assuming).
  • Cursors are URL-escaped. Kalshi cursors can contain +, / and =; unescaped, a + decodes server-side as a space and pagination silently derails.

Contributing

Issues and pull requests are welcome — see CONTRIBUTING.md for the checks to run and the invariants to preserve. The test suite is fully hermetic (go test -race ./... needs no network access or credentials).

Disclaimer

This is an unofficial client, not affiliated with or endorsed by Kalshi. Trading involves risk of loss; use at your own risk, and test against small orders before automating anything with real money.

License

MIT

Documentation

Overview

Package kalshi is an unofficial Go client for Kalshi's trade-api v2 (api.elections.kalshi.com). It is not affiliated with Kalshi.

Without a Signer the client is strictly read-only public market data (NewClient — no credentials, nothing is ever sent to trading endpoints). With one (NewAuthedClient) the trading endpoints become available:

c := kalshi.NewClient(nil)
yesBid, yesAsk, err := c.FetchOrderbook(ctx, "KXBTCD-26JUL1612-T110000")

signer, err := kalshi.NewSigner(keyID, pemKey)
c = kalshi.NewAuthedClient(nil, signer, "")
res, err := c.CreateOrder(ctx, kalshi.Order{...})

Requests are signed per Kalshi's published API-key spec: RSA-PSS (SHA-256) over timestamp+method+path, sent in KALSHI-ACCESS-* headers.

Prices and counts

Prices cross the wire as fixed-point dollar strings ("0.5600"). This package parses them to integer cents (PriceC, YesBid, ...) because binary contracts trade in 1–99¢ and integer math avoids float drift in P&L. Contract counts are subtler: orders placed through this client are whole contracts, but Kalshi counterparties trade fractional contracts, so every count the exchange reports about your orders — fills, remaining, positions — can come back fractional. Such fields carry both a whole-contract view (Remaining is the ceiling of the exact count, Filled its floor; together they reproduce a whole-contract ledger exactly) and the exchange's exact fixed-point value (RemainingFP, FilledFP, NetYesFP, ...).

Sides

Orders use the V2 single-book vocabulary: SideBid buys YES, SideAsk sells YES. A NO bid at q¢ is the same order as a YES ask at 100−q¢; prices always quote the YES side.

Retries and idempotency

Every order requires a ClientOrderID — it is what makes retries safe on the exchange side — and CreateOrder refuses to place one without it. All requests retry 429s with linear backoff. Canceling an order the exchange no longer knows returns ErrOrderNotFound (wrapped); the caller resolves whether it filled or was already canceled through Fills, never by assuming.

Demo environment

Kalshi's demo exchange speaks the same protocol at a different host. Pass DemoBaseURL as NewAuthedClient's baseURL, with a key pair generated in a demo account (demo and production credentials are separate):

c := kalshi.NewAuthedClient(nil, signer, kalshi.DemoBaseURL)

Passing a nil signer with a base URL override yields an unauthenticated, read-only client against that host — the same mechanism the package's tests use to point at httptest servers.

Malformed data

Fields that feed money math parse strictly: a filled order whose average price is unusable is an error, never a 0¢ cost basis. Account-wide reads (OpenOrders, Positions, Fills) instead degrade per item — an unparseable order, position, or fill reaches the caller flagged via its Malformed field rather than failing the page, because those reads back reconciliation and emergency-stop paths that must keep working even when one record is poison.

Index

Examples

Constants

View Source
const (
	SideBid = "bid" // buy YES
	SideAsk = "ask" // sell YES (== buy NO at 100−price)
)

Order sides in the V2 single-book vocabulary.

View Source
const (
	TIFImmediateOrCancel = "immediate_or_cancel"
	TIFGoodTillCanceled  = "good_till_canceled"
)

Time-in-force values (verified against the V2 create-order spec 2026-07-22; the API also offers fill_or_kill, which nothing here needs).

View Source
const DefaultBaseURL = "https://api.elections.kalshi.com/trade-api/v2"

DefaultBaseURL is Kalshi's public trade-api v2 root.

View Source
const DemoBaseURL = "https://demo-api.kalshi.co/trade-api/v2"

DemoBaseURL is the trade-api v2 root of Kalshi's demo exchange, for use as NewAuthedClient's baseURL. Demo accounts issue their own API keys; production credentials do not work against demo.

Variables

View Source
var ErrOrderNotFound = errors.New("kalshi: order not found")

ErrOrderNotFound marks a cancel whose order the exchange no longer knows as resting (already fully filled or already canceled). The caller must resolve the ambiguity through the fills endpoint, never by assuming.

View Source
var ErrPostOnlyCross = errors.New("kalshi: post-only order would cross")

ErrPostOnlyCross marks a post-only order the exchange refused because it would have crossed the book and taken — the join-only invariant enforced server-side. Benign and self-limiting: the quote simply does not rest.

Functions

func NormalizeTrades

func NormalizeTrades(trades []Trade)

NormalizeTrades fills the derived fields on trades decoded from a cached tape (which holds only the wire strings) and sorts them oldest-first.

Types

type CancelResult

type CancelResult struct {
	OrderID       string
	ClientOrderID string
	// ReducedBy is how many whole contracts the cancel removed — the
	// remaining count at cancellation time, rounded the same way
	// OpenOrder.Remaining is. Anything below the caller's believed remaining
	// filled (or was reduced) before the cancel landed: that difference is a
	// real position, discovered via Fills, never retried away.
	ReducedBy int
	// ReducedByFP is the exact reduced_by. It goes fractional when a
	// counterparty took part of the lot before the cancel landed; the
	// caller compares against it to notice pieces too small to move
	// ReducedBy.
	ReducedByFP float64
	TSMs        int64
}

CancelResult is the exchange's answer to an order cancel.

type Candle

type Candle struct {
	EndTS  int64 // unix seconds, period end
	YesBid int   // close, cents (0 if no bid)
	YesAsk int   // close, cents (0 if no ask)
}

Candle is one period's quote, reduced to closing top-of-book in cents.

type Client

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

Client reads public market data, and — when built by NewAuthedClient — places orders and reads the portfolio. The zero value is not usable.

func NewAuthedClient

func NewAuthedClient(hc *http.Client, signer *Signer, baseURL string) *Client

NewAuthedClient returns a Client that signs every request with signer, unlocking the trading endpoints. baseURL "" means production; tests and the demo environment pass their own.

func NewClient

func NewClient(hc *http.Client) *Client

NewClient returns a read-only Client using hc (or a sane default if hc is nil).

Example

Read public market data with no credentials.

package main

import (
	"context"
	"fmt"
	"log"

	kalshi "github.com/jaracah/kalshi-go"
)

func main() {
	c := kalshi.NewClient(nil)

	yesBid, yesAsk, err := c.FetchOrderbook(context.Background(), "KXBTCD-26JUL1612-T110000")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("yes %d¢ bid / %d¢ ask\n", yesBid, yesAsk)
}

func (*Client) Balance

func (c *Client) Balance(ctx context.Context) (int64, error)

Balance returns the member's available balance in cents. Doubling as the cheapest authenticated call, it makes a good startup auth check.

func (*Client) CancelOrder

func (c *Client) CancelOrder(ctx context.Context, orderID string) (CancelResult, error)

CancelOrder cancels a resting order by id. A 404 returns ErrOrderNotFound (wrapped): the order is gone from the book — filled or already canceled — and the caller owes a fills poll to learn which.

func (*Client) CreateOrder

func (c *Client) CreateOrder(ctx context.Context, o Order) (OrderResult, error)

CreateOrder places o and reports what executed synchronously (everything, for IOC; any immediate crossing fill, for GTC). self_trade_prevention stays "taker_at_cross" on every order: on the taker side it can only cancel our own taker order, and on the maker side it is the resting order a self-cross cancels against — never a fabricated self-fill.

Example

Place a limit order with an authenticated client.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	kalshi "github.com/jaracah/kalshi-go"
)

func main() {
	signer, err := kalshi.NewSigner(
		os.Getenv("KALSHI_KEY_ID"),
		os.Getenv("KALSHI_PRIVATE_KEY"), // PEM, PKCS#1 or PKCS#8
	)
	if err != nil {
		log.Fatal(err)
	}
	c := kalshi.NewAuthedClient(nil, signer, "") // "" = production

	res, err := c.CreateOrder(context.Background(), kalshi.Order{
		Ticker: "KXBTCD-26JUL1612-T110000",
		Side:   kalshi.SideBid, // buy YES
		Count:  10,
		PriceC: 55, // 55¢ limit; zero TimeInForce = immediate-or-cancel
		// Required: it makes retrying a 429 or ambiguous transport failure
		// safe — the exchange dedupes on it. Unique per logical order.
		ClientOrderID: "d5f4f4a2-1f5e-4c3a-9b6d-8f2a7c1e0b42",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("order %s: filled %d @ %d¢, %d resting\n",
		res.OrderID, res.FillCount, res.AvgPriceC, res.Remaining)
}

func (*Client) DiscoverActive

func (c *Client) DiscoverActive(ctx context.Context, series string) ([]Market, error)

DiscoverActive returns open markets in a series, soonest close_time first.

func (*Client) FetchCandlesticks

func (c *Client) FetchCandlesticks(ctx context.Context, series, ticker string, startTS, endTS int64, periodMin int) ([]Candle, error)

FetchCandlesticks returns the historical quote series for a market over [startTS, endTS] (unix seconds) at periodMin-minute resolution. Quotes are the period close (the open carries occasional pre-trade artifacts).

func (*Client) FetchMarket

func (c *Client) FetchMarket(ctx context.Context, ticker string) (Market, error)

FetchMarket returns the current summary for one market.

func (*Client) FetchOrderbook

func (c *Client) FetchOrderbook(ctx context.Context, ticker string) (yesBid, yesAsk int, err error)

FetchOrderbook returns live top-of-book in cents: yesBid is the best price you could SELL yes at (highest yes bid); yesAsk is the best price you could BUY yes at, derived as 100 - bestNoBid (a No bid at q is a Yes offer at 100-q). Either is 0 when that side of the book is empty.

func (*Client) FetchOrderbookDepth

func (c *Client) FetchOrderbookDepth(ctx context.Context, ticker string) (yes, no []Level, err error)

FetchOrderbookDepth returns the live book's full depth, best-priced level first on each side. The yes side holds resting bids to buy YES; the no side holds resting bids to buy NO (a NO bid at q is a YES offer at 100−q). The best NO level's size is the queue a joining maker rests behind. Like FetchOrderbook this endpoint is not CloudFront-cached.

func (*Client) FetchSettled

func (c *Client) FetchSettled(ctx context.Context, series string, limit int) ([]Market, error)

FetchSettled returns up to limit recently-settled markets in a series, newest first (each carries a Result of "yes"/"no" and its open/close times). It pages with the API cursor so limit may exceed the 100-per-request cap — letting callers reach older history.

func (*Client) FetchTrades

func (c *Client) FetchTrades(ctx context.Context, ticker string) ([]Trade, error)

FetchTrades returns the complete public tape for ticker, oldest first, paging with the API cursor (1000 trades per page, brief pause between pages). The page cap is a runaway guard far above any real single-market tape; hitting it is an error — a silently truncated tape would corrupt downstream analysis.

func (*Client) FetchTradesSince

func (c *Client) FetchTradesSince(ctx context.Context, ticker string, since time.Time) ([]Trade, error)

FetchTradesSince returns the ticker's prints strictly after since, oldest first — the incremental form a fill-polling loop wants (refetching whole tapes every pass would hammer the API). The page cap bounds one poll; a capped read returns what it has with an error so the caller can treat the tape as possibly-incomplete rather than silently truncated.

func (*Client) Fills

func (c *Client) Fills(ctx context.Context, since time.Time) ([]Fill, error)

Fills returns portfolio fills at or after since, oldest first, cursor- paged. min_ts is second-granular and treated inclusively; callers dedupe by FillID across polls (a restart's re-read makes duplicates normal).

Example

Rest a post-only maker quote, then discover its fills asynchronously.

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"time"

	kalshi "github.com/jaracah/kalshi-go"
)

func main() {
	signer, err := kalshi.NewSigner(
		os.Getenv("KALSHI_KEY_ID"),
		os.Getenv("KALSHI_PRIVATE_KEY"),
	)
	if err != nil {
		log.Fatal(err)
	}
	c := kalshi.NewAuthedClient(nil, signer, "")
	ctx := context.Background()

	res, err := c.CreateOrder(ctx, kalshi.Order{
		Ticker:        "KXBTCD-26JUL1612-T110000",
		Side:          kalshi.SideAsk, // sell YES == buy NO at 100−price
		Count:         5,
		PriceC:        95,
		TimeInForce:   kalshi.TIFGoodTillCanceled,
		PostOnly:      true, // reject instead of crossing
		ClientOrderID: "0b7c9d1e-2f3a-4b5c-8d9e-6f1a2b3c4d5e",
	})
	if errors.Is(err, kalshi.ErrPostOnlyCross) {
		return // would have taken; the quote simply does not rest
	} else if err != nil {
		log.Fatal(err)
	}

	fills, err := c.Fills(ctx, time.Now().Add(-time.Minute))
	if err != nil {
		log.Fatal(err)
	}
	for _, f := range fills {
		if f.OrderID != res.OrderID {
			continue
		}
		if f.Malformed != "" {
			log.Printf("unbookable fill %s: %s", f.FillID, f.Malformed)
			continue
		}
		// CountFP can be fractional even though the order was whole:
		// counterparties trade fractional contracts.
		fmt.Printf("filled %.2f @ %d¢ (fee %d¢)\n", f.CountFP, f.YesPriceC, f.FeeC)
	}
}

func (*Client) OpenOrders

func (c *Client) OpenOrders(ctx context.Context) ([]OpenOrder, error)

OpenOrders returns every resting order, cursor-paged — the exchange's order-side truth for adoption and reconciliation.

func (*Client) Positions

func (c *Client) Positions(ctx context.Context) ([]Position, error)

Positions returns every market with a non-zero position. It pages through the cursor so the caller always sees the whole book.

type Fill

type Fill struct {
	FillID   string
	OrderID  string
	Ticker   string
	BookSide string // SideBid | SideAsk
	IsTaker  bool
	// Count is the whole-contract count when the exchange reported an
	// integral fill, else 0. CountFP is always the exact reported count:
	// counterparties trade fractional contracts, so YOUR integral order can
	// fill in fractional pieces (observed live 2026-07-24: a 1-lot ask
	// filled as 0.95 + 0.05). Callers that book whole contracts accumulate
	// CountFP per order and book as wholes complete.
	Count     int
	CountFP   float64
	YesPriceC int
	FeeC      int
	Time      time.Time
	// Malformed is non-empty when the exchange reported the fill in a shape
	// that cannot be booked at all (missing count, unusable price). Such a
	// fill still flows to the caller — erroring the whole poll would wedge
	// the fills cursor on one poison fill forever (observed live
	// 2026-07-24) — but its numbers are not trustworthy and the caller must
	// treat the account as diverged.
	Malformed string
}

Fill is one portfolio fill — the async discovery record carrying the exchange-reported price and fee.

type Level

type Level struct {
	PriceC int // cents
	Size   int // displayed contracts
}

Level is one resting orderbook level, in this package's native units.

type Market

type Market struct {
	Ticker string `json:"ticker"`
	Title  string `json:"title"`
	// This endpoint reports quotes as dollar-denominated strings ("0.2400"),
	// not integer-cent fields. NOTE: every summary field here — including these
	// quotes and volume_fp — freezes at the window's open_time (the response is
	// CloudFront-cached and the object isn't refreshed intra-window). For a live
	// quote use FetchOrderbook; treat VolumeFP as open-time only.
	YesBidDollars string `json:"yes_bid_dollars"`
	YesAskDollars string `json:"yes_ask_dollars"`
	VolumeFP      string `json:"volume_fp"`
	Status        string `json:"status"`
	Result        string `json:"result"`     // "yes" | "no" | ""
	OpenTime      string `json:"open_time"`  // ISO-8601, window start
	CloseTime     string `json:"close_time"` // ISO-8601, window end

	// Derived from the *_dollars strings by normalize(); callers work in cents.
	YesBid int // cents, 0-100
	YesAsk int // cents, 0-100
}

Market is one market, with the fields this client decodes.

func PickLive

func PickLive(markets []Market, now time.Time, buffer time.Duration) (m Market, ok bool)

PickLive returns the soonest-closing market that is actually tradeable right now: its window has opened and it is not within buffer of its close. markets is assumed sorted by close_time ascending (as DiscoverActive returns them). Markets with unparseable times are skipped. ok is false when nothing is currently in-window.

type OpenOrder

type OpenOrder struct {
	OrderID       string
	ClientOrderID string
	Ticker        string
	BookSide      string // SideBid | SideAsk (the V2 book vocabulary)
	YesPriceC     int    // YES-side limit price, cents
	Remaining     int    // WHOLE contracts that can still fill from this order
	Filled        int    // whole contracts filled so far
	CreatedTime   string // ISO-8601, exchange-reported
	// RemainingFP and FilledFP are the exchange's exact counts, fractional
	// whenever a counterparty has taken part of a lot. Remaining is their
	// ceiling and Filled their floor, which together reproduce a
	// whole-contract caller's view exactly: a 5-lot part-filled by 0.18
	// rests at 4.82 == 5 wholes still to book, 0 booked so far.
	RemainingFP float64
	FilledFP    float64
	// Malformed is non-empty when a count could not be parsed at all. The
	// order still reaches the caller — its id is enough to cancel it — but
	// its counts are zero and must not be trusted.
	Malformed string
}

OpenOrder is one resting order, carrying both whole-contract and exact fixed-point counts.

type Order

type Order struct {
	Ticker string
	Side   string // SideBid | SideAsk
	Count  int    // whole contracts, > 0
	PriceC int    // YES-side limit price in cents, 1..99
	// TimeInForce: "" (= TIFImmediateOrCancel) or TIFGoodTillCanceled.
	TimeInForce string
	// PostOnly, on a resting order, makes the exchange reject a placement
	// that would cross instead of taking — a maker's join-only invariant
	// enforced exchange-side (verified in the V2 spec 2026-07-22).
	PostOnly bool
	// ClientOrderID makes retries idempotent on the exchange side: a 429 or
	// transport error can be retried with the same id without double-filling.
	ClientOrderID string
}

Order is a limit order in this package's native units (integer cents and whole contracts). The zero TimeInForce is IOC: a taker order either fills at its snapshot price or misses — if the book moved, missing the fill is usually the correct outcome. Pass TIFGoodTillCanceled to rest on the book.

type OrderResult

type OrderResult struct {
	OrderID string
	// FillCount is WHOLE contracts filled immediately (0 = book moved, no
	// fill — or a purely fractional fill, which FillCountFP shows).
	FillCount int
	// Remaining is whole contracts still resting for a GTC order, or
	// canceled back for an IOC one.
	Remaining int
	// FillCountFP and RemainingFP are the exchange's exact counts. They
	// differ from the whole-contract fields when a fractional counterparty
	// took part of the lot; callers that book wholes use the ints, callers
	// that must agree with the exchange's own arithmetic use these.
	FillCountFP float64
	RemainingFP float64
	AvgPriceC   int // volume-weighted average fill price, cents (0 when no fill)
	// TotalFeeC is the fee for the whole fill in cents, derived from the
	// exchange's per-contract average (average_fee_paid × the exact
	// fill_count, rounded to the nearest cent) — the closest the V2
	// response gets to an exact total.
	TotalFeeC int
	TSMs      int64 // matching-engine timestamp, epoch ms
}

OrderResult is the exchange's synchronous answer to an IOC order.

type Position

type Position struct {
	Ticker string
	// NetYes is the whole-contract net position: positive = YES contracts
	// held, negative = NO contracts held (the exchange's position_fp uses
	// the same sign). Rounded toward zero when the position is fractional —
	// NetYesFP carries the exact value.
	NetYes int
	// NetYesFP is position_fp verbatim. A fractional position is ROUTINE,
	// not corruption: it is exactly what the account holds while a
	// whole-contract order is part-filled by a fractional counterparty, and
	// it persists until that order completes, is canceled, or settles.
	// Callers reconciling against a whole-contract ledger must compare
	// against this plus their unbooked piece remainders, never treat the
	// fraction itself as divergence (observed live 2026-07-27).
	NetYesFP float64
	// ExposureC is the exchange's market_exposure in cents — the cost basis
	// of the aggregate position, usable to approximate an average entry
	// price when adopting positions at startup.
	ExposureC int64
	// Malformed is non-empty when position_fp could not be parsed at all —
	// genuine API drift, not a fraction. Per-position, never a page error: a
	// page error here would break startup position adoption and blind every
	// reconcile pass for as long as the position exists (observed live
	// 2026-07-24).
	Malformed string
}

Position is one market's exchange-side position.

type Signer

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

Signer holds an API key id and its RSA private key, and signs requests in place. Safe for concurrent use.

func NewSigner

func NewSigner(keyID, pemKey string) (*Signer, error)

NewSigner parses pemKey (PKCS#1 "RSA PRIVATE KEY" or PKCS#8 "PRIVATE KEY") and returns a Signer for keyID. The key material never leaves the process; callers typically load it from the environment or a secrets store.

type Trade

type Trade struct {
	TradeID         string `json:"trade_id"`
	CreatedTime     string `json:"created_time"`
	TakerSide       string `json:"taker_side"` // "yes" | "no"
	CountFP         string `json:"count_fp"`
	YesPriceDollars string `json:"yes_price_dollars"`

	// Derived from the wire strings by normalize; callers use these. Excluded
	// from JSON so cached tapes round-trip through the wire fields only.
	Time     time.Time `json:"-"`
	Count    float64   `json:"-"` // contracts; the API reports fractions ("935.19")
	YesCents int       `json:"-"`
}

Trade is one print from the public trade tape (GET /markets/trades). taker_side is only "which side crossed the spread" — the tape does not say whether the taker was informed.

Jump to

Keyboard shortcuts

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