godex

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Aug 2, 2026 License: MIT Imports: 6 Imported by: 0

README

godex

Go trading integration layer for perpetual DEXes — Lighter (zkLighter), dYdX v4, and Hyperliquid. godex owns authenticated order placement, cancellation, account-state observation, and venue-specific signing behind a small, safety-oriented contract. Strategy and risk logic depend only on the normalized types and events; venue protocol details never leak out.

This is not a generic exchange SDK. The contract intentionally supports exactly what a post-only maker / IOC taker strategy needs. See docs/pre-implementation.md for the design boundary and safety rules.

Status: pre-release. All three adapters are implemented with full unit suites, and all three have passed the full testnet adoption-gate run.

Install

go get github.com/DaisukeYoda/godex

The contract

type VenueExecutor interface {
    VenueID() VenueID
    Connect(ctx context.Context) (ExecutionMetadata, error)
    PlaceOrder(ctx context.Context, order NewOrder) (OrderAck, error)
    CancelOrder(ctx context.Context, id OrderID) error
    AccountEvents() <-chan AccountEvent
    Close() error
}

Design invariants every adapter upholds:

  • Maker orders are post-only. A taker-crossing rejection is a normal-path outcome: PlaceOrder returns AckRejected (plus an OrderRejectedEvent), never an error.
  • Fills come only from the authenticated account stream. Adapters never infer executions or positions from order-book state.
  • Strict wire validation. Unexpected REST/WS payload shapes abort the connection instead of being guessed at; reconnection re-subscribes and re-converges from a verified snapshot.
  • Ambiguous submissions halt, never retry blindly. When an outcome is unknown (e.g. timeout), the adapter latches a fault: the transaction is never resent, later submissions fail with ErrTxOutcomeUnknown, and the adapter reconciles with venue state before resuming.
  • Event ordering. ConnectedEvent/DisconnectedEvent alternate (including across internal reconnects); all other events are emitted only in between. The events channel is buffered; when full, the adapter blocks rather than drops — consume promptly. The channel closes only after Close.
  • Quantization follows the venue's own rule. Hyperliquid prices are not a fixed tick: they carry at most five significant figures and at most 6 - szDecimals decimals, so the increment is recomputed per order from the price's magnitude.
  • Risk metadata errs toward the account. Where a venue's maintenance margin varies with position size, the single normalized fraction reports the strictest requirement, never the most permissive.
  • Venue lifetimes are made explicit. dYdX short-term orders expire on their own after roughly fifteen blocks; the adapter reports that as an OrderRejectedEvent rather than letting a strategy believe a quote is still live.
  • An order finishes exactly once. Every end of an order — crossed, expired, cancelled — is one OrderRejectedEvent, whichever path observed it first. An order that ended by a cancel the caller asked for carries godex.ReasonCanceledByRequest on every venue, and an adapter reports it only once the venue says the order ended: accepting a cancel means the request was valid, not that it applied, and one accepted as the order filled applied to nothing. Lighter cannot observe a plain cancellation at all — see its package comment.
  • Money is fixed-point. All prices/sizes use decimal.Decimal (big-int mantissa + scale, string-only construction, round half away from zero). No floats anywhere near order flow.

Market data (public, read-only)

Alongside execution, godex normalizes the public market data a maker/taker strategy consumes — order books and funding — behind two contracts, one streamed and one polled:

type MarketStream interface {          // WS order-book stream
    VenueID() VenueID
    Start(ctx context.Context) error
    Events() <-chan MarketEvent        // BookSnapshot | MarketConnected | MarketDisconnected
    Close() error
}

type MarketDataClient interface {      // REST polls
    VenueID() VenueID
    FundingRate(ctx context.Context) (FundingRate, error)
    MarketStats(ctx context.Context) (MarketStats, error)
}

Like executors, one stream or client serves one market. Both are available for Lighter (lighter.NewMarketStream / lighter.NewMarketData) and dYdX (dydx.NewMarketStream / dydx.NewMarketData); they need no credentials.

Invariants:

  • A crossed book is never emitted. Delta-driven crossings on dYdX are uncrossed by treating the later update as the freshest state (the official client's interpretation); a crossed snapshot resubscribes the market, and on Lighter a crossed book suppresses emits and rebuilds the connection past a short grace.
  • Sequence integrity is proven, not assumed. dYdX message_id is tracked through a contiguous watermark that tolerates cross-channel reordering but aborts on true gaps and duplicates; Lighter updates must chain begin_noncenonce exactly. Either failure rebuilds the connection and the book — no gap is ever papered over.
  • Snapshot/delta reassembly is internal. Consumers always receive full, sorted book snapshots.
  • Funding rates are normalized to a plain per-interval decimal at FundingRateScale with the venue's sign convention folded in (positive = longs pay shorts), so rates from different venues subtract cleanly.

dYdX also exposes dydx.FetchFundingPayments (the Indexer's per-account settled funding history, public REST) and dydx.KeyFromMnemonic, which derives the account key at the Cosmos HD path m/44'/118'/0'/0/0 exactly as the official clients do — pinned in tests against @dydxprotocol/v4-client-js.

To watch live market data without touching execution:

go run ./cmd/godex-smoke -market-watch -venue dydx -network mainnet \
  -ticker SOL-USD -symbol SOL-PERP -price-scale 4 -size-scale 3 -watch-duration 30s

Quickstart (Lighter, testnet)

package main

import (
    "context"
    "log"
    "os"

    "github.com/DaisukeYoda/godex"
    "github.com/DaisukeYoda/godex/decimal"
    "github.com/DaisukeYoda/godex/lighter"
)

func main() {
    exec, err := lighter.New(lighter.Config{
        Credentials: lighter.Credentials{
            AccountIndex:  48,
            APIKeyIndex:   2,
            APIPrivateKey: os.Getenv("LIGHTER_API_PRIVATE_KEY"),
        },
        Symbol:   "SOL-PERP",
        MarketID: 2, // SOL on testnet
        Network:  lighter.Testnet,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer exec.Close()

    go func() {
        for event := range exec.AccountEvents() {
            log.Printf("%#v", event)
        }
    }()

    ctx := context.Background()
    meta, err := exec.Connect(ctx)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("sizeStep=%s mmf=%s", meta.SizeStep, meta.MaintenanceMarginFraction)

    ack, err := exec.PlaceOrder(ctx, godex.NewOrder{
        Symbol: "SOL-PERP",
        Side:   godex.SideBuy,
        Price:  decimal.MustFromString("80.000", 3), // rounded to tick by the adapter
        Size:   decimal.MustFromString("0.200", 3),
        Intent: godex.IntentPostOnly,
    })
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("ack: %+v", ack)
}

Quickstart (dYdX v4, testnet)

exec, err := dydx.New(dydx.Config{
    Credentials: dydx.Credentials{
        PrivateKeyHex: os.Getenv("DYDX_PRIVATE_KEY_HEX"),
        Address:       os.Getenv("DYDX_ADDRESS"), // dydx1... — the account orders belong to
        SubaccountNumber: 0,
        // Optional: the on-chain authenticator scoping this key to trading only.
        AuthenticatorID: nil,
    },
    Symbol:  "ETH-PERP",
    Ticker:  "ETH-USD", // venue market ticker
    Network: dydx.Testnet,
})

The dYdX adapter places short-term orders only — gas-free, matched synchronously in CheckTx (so a crossing post-only comes back as AckRejected in the same call), and valid for at most ~20 blocks. It talks to exactly two hosts: the Indexer (market metadata, account stream) and a validator's CometBFT RPC (block height, account lookup, broadcast). Transactions are built and signed in-process from a small vendored protobuf set (dydx/internal/pb) rather than importing the dYdX chain module, whose forked cosmos-sdk pins do not resolve transitively.

Quickstart (Hyperliquid, testnet)

exec, err := hyperliquid.New(hyperliquid.Config{
    Credentials: hyperliquid.Credentials{
        // The account that holds the position — the master account when an
        // API wallet signs for it.
        AccountAddress: os.Getenv("HYPERLIQUID_ACCOUNT_ADDRESS"), // 0x...
        APIPrivateKey:  os.Getenv("HYPERLIQUID_API_PRIVATE_KEY"), // API (agent) wallet key
        // Optional: route orders to a vault or subaccount instead.
        VaultAddress: "",
    },
    Symbol:  "ETH-PERP",
    Coin:    "ETH", // venue perp name
    Network: hyperliquid.Testnet,
})

Orders are signed as L1 actions: the action is MessagePack-encoded, framed with the nonce and vault address, hashed, and signed as EIP-712 typed data under the venue's fixed Exchange domain. Signing is implemented in-process and pinned to the reference implementation's published test vectors (hyperliquid/signer_test.go); only MessagePack comes from a third-party module, with keccak and secp256k1 taken from dependencies godex already uses.

Post-only maps to the venue's Alo time-in-force, and a crossing maker is refused synchronously, so it returns AckRejected in the same call. Every order is submitted under a client order id minted before dispatch, which is also what a cancel is keyed by and what an ambiguous submission is reconciled against — an order the venue turns out to be holding is cancelled, since the caller never received an id it could close it with. Fills come from the userFills stream; position and margin are read from clearinghouse snapshots — at connect, on every fill, after a reconnect, and on a periodic backstop poll. Tracked orders are re-checked after a reconnect too, because order updates are pushed and never replayed.

Maintenance margin on Hyperliquid is tiered — a perp advertising 25x drops to 5x above $50k of notional — so MaintenanceMarginFraction, which is a single ratio, carries the strictest tier rather than the headline one.

Testnet smoke test (adoption gates)

An adapter is adopted only after the full gate scenario passes on testnet: connect + verified snapshot → far post-only + cancel → crossing post-only rejected (normal path) → IOC fill + position → optional forced reconnect with convergence and duplicate-fill checks → reduce-only close to flat.

LIGHTER_ACCOUNT_INDEX=... LIGHTER_API_KEY_INDEX=... LIGHTER_API_PRIVATE_KEY=... \
  go run ./cmd/godex-smoke -venue lighter -network testnet \
  -market-id 2 -symbol SOL-PERP -size 0.200 -reconnect-check \
  [-wait-fill] [-record data/lighter-account.jsonl]
DYDX_PRIVATE_KEY_HEX=... DYDX_ADDRESS=dydx1... [DYDX_SUBACCOUNT_NUMBER=0] \
  go run ./cmd/godex-smoke -venue dydx -network testnet \
  -ticker ETH-USD -symbol ETH-PERP -size 0.010 -reconnect-check
HYPERLIQUID_ACCOUNT_ADDRESS=0x... HYPERLIQUID_API_PRIVATE_KEY=0x... \
  go run ./cmd/godex-smoke -venue hyperliquid -network testnet \
  -coin ETH -symbol ETH-PERP -size 0.010 -reconnect-check

Each gate logs PASS/FAIL; any failure exits non-zero. -record streams raw account WS frames to JSONL for fixture refresh (Lighter only).

Security

  • Credentials are passed in as struct fields; the library never reads environment variables or files. cmd/godex-smoke reads env vars and fails fast when they are missing.
  • Use venue-scoped, trading-only API keys. Withdrawal-capable master keys (L1 wallets) must never reach a trading process. dYdX has no separate API key, so register a dedicated key as a scoped on-chain authenticator (accountplus) and name it in Credentials.AuthenticatorID. The chain takes one authenticator per message, so compose several restrictions into a single AllOf authenticator rather than listing them. On Hyperliquid, use an API (agent) wallet: it can place and cancel orders but cannot withdraw or transfer.
  • Use testnet keys for the smoke test. .env* and *.jsonl are gitignored; never commit key material or account recordings.

Changelog

See CHANGELOG.md.

License

MIT

Documentation

Overview

Package godex is a trading integration layer for perpetual DEXes. It owns authenticated order placement, cancellation, account-state observation, and venue-specific signing behind a small, safety-oriented contract. Strategy and risk logic depend only on the normalized types and events in this package; venue adapters live in subpackages (lighter, ...).

This is not a generic exchange SDK: the contract intentionally supports only what a post-only maker / IOC taker strategy needs.

Index

Constants

View Source
const DefaultAccountEventBuffer = 1024

DefaultAccountEventBuffer is the AccountEvents channel capacity. It absorbs transient consumer stalls; when it fills, producers block instead of dropping (a dropped fill would silently corrupt position state), which eventually stalls the WebSocket read loop and surfaces loudly as a venue idle-disconnect plus reconnect.

View Source
const DefaultMarketEventBuffer = 1024

DefaultMarketEventBuffer is the MarketStream Events channel capacity. Like the account stream, when it fills the adapter blocks rather than dropping: a consumer acting on a silently stale book would quote against prices that no longer exist. The stall eventually surfaces as a venue idle-disconnect plus reconnect.

View Source
const FundingRateScale = 8

FundingRateScale is the decimal scale of normalized funding rates. Venues report rates at unpredictable native precision (dYdX has been observed sending 20 fractional digits); adapters round half away from zero to this scale so rates from different venues subtract cleanly.

View Source
const MarginUsageScale = 4

MarginUsageScale is the decimal scale of normalized margin usage ratios (0.6200 = 62%).

View Source
const ReasonCanceledByRequest = "canceled by request"

ReasonCanceledByRequest is the reason an adapter reports for an order that ended by a cancel the caller asked for. Every other reason is the venue's own wording, passed through; this one is the adapter's, so such a cancel reads the same on every venue.

It is reported when the venue says the order ended, not when it accepts the cancel — accepting one says the request was valid, not that it applied. A cancel accepted in the same instant the order filled applied to nothing, and that order is reported as filled, never under this reason.

It follows that an order whose end the venue never reports is never reported here either. Where an adapter can ask outright it does, and each reconnect re-checks every order still believed live. Lighter is the exception: its account stream reports only post-only cancellations and it has no order-status query, so a caller's cancel of a resting order there produces no event at all. See the lighter package comment.

View Source
const USDNotionalScale = 2

USDNotionalScale is the decimal scale of normalized USD notionals in market statistics (open interest, volume). Statistics are reference values, not order inputs, so cent precision is enough.

Variables

View Source
var (
	// ErrNotConnected is returned when an operation requires a connected
	// executor.
	ErrNotConnected = errors.New("godex: executor not connected")
	// ErrClosed is returned after Close.
	ErrClosed = errors.New("godex: executor closed")
	// ErrUnknownOrder is returned by CancelOrder for an ID the executor is
	// not tracking.
	ErrUnknownOrder = errors.New("godex: unknown order id")
	// ErrTxOutcomeUnknown reports that a submission's outcome could not be
	// determined (e.g. timeout). The executor latches this fault and blocks
	// further submissions until it reconciles with venue state; callers must
	// never blindly retry.
	ErrTxOutcomeUnknown = errors.New("godex: transaction outcome unknown")
)

Sentinel errors shared by all venue adapters.

Functions

func ComputeMarginUsage

func ComputeMarginUsage(total, available string) (decimal.Decimal, error)

ComputeMarginUsage returns (total - available) / total at MarginUsageScale. Venue adapters map their native wire fields onto (total, available) — e.g. equity/freeCollateral or collateral/availableBalance. Zero total (an unfunded account) is zero usage.

func QuantizeReduceOnlySize

func QuantizeReduceOnlySize(size, step decimal.Decimal) (decimal.Decimal, error)

QuantizeReduceOnlySize ceils size to a multiple of step. Reduce-only orders cannot flip the position, so dust may be ceiled up to fully close.

func QuantizeSize

func QuantizeSize(size, step, minSize decimal.Decimal) (decimal.Decimal, error)

QuantizeSize floors size to a multiple of step. If the result is zero or below minSize, it returns an error rather than silently rounding up.

func RoundPriceToTick

func RoundPriceToTick(price, tick decimal.Decimal, side Side) (decimal.Decimal, error)

RoundPriceToTick rounds price to a multiple of tick: buy floors, sell ceils. The result carries tick's scale.

func SizeForNotional

func SizeForNotional(notional, price, step decimal.Decimal) (decimal.Decimal, error)

SizeForNotional returns the largest step multiple not exceeding notional / price, computed without floating point. The result may be zero.

Types

type AccountEvent

type AccountEvent interface {
	// contains filtered or unexported methods
}

AccountEvent is the sealed union of account-stream events. Consumers type-switch over the concrete types below; treat unknown variants in the default branch as a programming error (fail fast), mirroring strict discriminator validation.

type AckStatus

type AckStatus string

AckStatus is the submission outcome reported by OrderAck.

const (
	// AckSubmitted means the venue accepted the submission. It does not mean
	// the order filled.
	AckSubmitted AckStatus = "submitted"
	// AckRejected means the venue (or the adapter's pre-check) rejected the
	// order — e.g. a post-only order that would cross. A normal-path outcome.
	AckRejected AckStatus = "rejected"
)

Ack statuses.

type BookLevel added in v0.4.0

type BookLevel struct {
	Price decimal.Decimal
	Size  decimal.Decimal
}

BookLevel is one price level of an order book.

type BookSnapshotEvent added in v0.4.0

type BookSnapshotEvent struct {
	Book OrderBook
}

BookSnapshotEvent carries a normalized full book snapshot. Adapters rebuild the book internally from the venue's snapshot/delta wire protocol; the difference never leaks to consumers.

type ConnectedEvent

type ConnectedEvent struct {
	VenueID VenueID
}

ConnectedEvent reports that the account stream is up and the initial (or post-reconnect) snapshot follows.

type DisconnectedEvent

type DisconnectedEvent struct {
	VenueID VenueID
}

DisconnectedEvent reports that the account stream is down; state events pause until the next ConnectedEvent.

type ExecutionMetadata

type ExecutionMetadata struct {
	// SizeStep is the venue's order size increment.
	SizeStep decimal.Decimal
	// MaintenanceMarginFraction is normalized to a decimal ratio. Venues
	// define it differently (decimal vs 1/10000 integer); each adapter
	// converts to a plain ratio.
	MaintenanceMarginFraction decimal.Decimal
}

ExecutionMetadata is venue market metadata resolved during Connect.

type FillEvent

type FillEvent struct {
	OrderID OrderID
	Side    Side
	Price   decimal.Decimal
	Size    decimal.Decimal
	Time    time.Time
}

FillEvent reports an execution from the authenticated account stream — the only source of truth for fills.

type FundingRate added in v0.4.0

type FundingRate struct {
	VenueID VenueID
	Symbol  Symbol
	// Rate is the funding rate per interval at FundingRateScale, signed the
	// way perp venues quote it: positive means longs pay shorts.
	Rate decimal.Decimal
	// IntervalHours is the venue's funding interval (1 for hourly venues).
	IntervalHours int
	// NextFundingTime is the next application time, nil when the venue's API
	// does not report one.
	NextFundingTime *time.Time
}

FundingRate is a venue's current funding rate observation for one market.

type MarginEvent

type MarginEvent struct {
	// UsageRatio is at MarginUsageScale; see ComputeMarginUsage.
	UsageRatio decimal.Decimal
	EquityUSD  decimal.Decimal
	Time       time.Time
}

MarginEvent reports account margin state.

type MarketConnectedEvent added in v0.4.0

type MarketConnectedEvent struct {
	VenueID VenueID
}

MarketConnectedEvent reports that the market stream is up and subscribed.

type MarketDataClient added in v0.4.0

type MarketDataClient interface {
	// VenueID identifies the venue this client queries.
	VenueID() VenueID

	// FundingRate returns the venue's current funding rate for the
	// configured market.
	FundingRate(ctx context.Context) (FundingRate, error)

	// MarketStats returns venue statistics for the configured market.
	MarketStats(ctx context.Context) (MarketStats, error)
}

MarketDataClient is the normalized polled market-data contract (REST). Like executors, one client serves one market. Methods are safe for concurrent use.

type MarketDisconnectedEvent added in v0.4.0

type MarketDisconnectedEvent struct {
	VenueID VenueID
}

MarketDisconnectedEvent reports that the market stream is down. Book snapshots pause until the next MarketConnectedEvent; consumers must treat the last snapshot as stale, not current.

type MarketEvent added in v0.4.0

type MarketEvent interface {
	// contains filtered or unexported methods
}

MarketEvent is the sealed union of market-stream events. Consumers type-switch over the concrete types below; treat unknown variants in the default branch as a programming error (fail fast).

type MarketStats added in v0.4.0

type MarketStats struct {
	VenueID VenueID
	Symbol  Symbol
	// OpenInterestUSD is the open interest at USDNotionalScale. Venues that
	// report OI in base-asset units are converted with the venue's own
	// reference price, rounding once at the product.
	OpenInterestUSD decimal.Decimal
	// Volume24hUSD is the 24-hour volume at USDNotionalScale.
	Volume24hUSD decimal.Decimal
}

MarketStats are venue market statistics. Reference values only — never order inputs.

type MarketStream added in v0.4.0

type MarketStream interface {
	// VenueID identifies the venue this stream observes.
	VenueID() VenueID

	// Start dials the venue and subscribes. A first-connect failure is
	// returned and the reconnect loop is not entered (fail fast).
	Start(ctx context.Context) error

	// Events returns the stream's single event channel. The channel is
	// buffered (DefaultMarketEventBuffer); when it fills the adapter blocks
	// rather than dropping. Consume promptly. The channel is closed only
	// after Close completes.
	Events() <-chan MarketEvent

	// Close tears the stream down and closes the event channel. Close is
	// terminal; observing again means constructing a new stream.
	Close() error
}

MarketStream is the normalized market-data streaming contract. Like executors, one stream serves one market: N markets are N streams.

Design invariants:

  • A crossed book is never emitted. Sequence gaps, duplicate sequence numbers, and unparseable payloads abort the connection instead of being guessed at (fail fast); the stream reconnects and resubscribes.
  • Snapshot/delta reassembly is internal; consumers always receive full snapshots.

Unlike VenueExecutor.Close, a MarketStream keeps itself alive across connection drops between Start and Close: drops emit MarketDisconnectedEvent, reconnects emit MarketConnectedEvent and resubscribe.

Event ordering contract (Events):

  • MarketConnectedEvent and MarketDisconnectedEvent alternate, including across internal reconnects.
  • BookSnapshotEvent is emitted only between a MarketConnectedEvent and the following MarketDisconnectedEvent.

type NewOrder

type NewOrder struct {
	Symbol Symbol
	Side   Side
	// Price is the desired price before rounding; the executor rounds it to
	// the venue tick (buy floor / sell ceil).
	Price decimal.Decimal
	// Size is the desired size before rounding; the executor quantizes it to
	// the venue step.
	Size       decimal.Decimal
	Intent     OrderIntent
	ReduceOnly bool
}

NewOrder is a normalized order intent.

type OrderAck

type OrderAck struct {
	OrderID OrderID
	VenueID VenueID
	Status  AckStatus
	Time    time.Time
}

OrderAck is the normalized submission acknowledgement.

type OrderBook added in v0.4.0

type OrderBook struct {
	VenueID VenueID
	Symbol  Symbol
	Bids    []BookLevel
	Asks    []BookLevel
	// ReceivedAt is the local receive time of the update that produced this
	// snapshot. Consumers use it for staleness decisions.
	ReceivedAt time.Time
}

OrderBook is a normalized full order-book snapshot. Bids are sorted best (highest) first, asks best (lowest) first. A book is never emitted crossed; see MarketStream.

type OrderID

type OrderID string

OrderID is an executor-scoped order identifier. The mapping to venue-native IDs is kept inside each adapter.

type OrderIntent

type OrderIntent string

OrderIntent is the normalized execution intent. There is no GTC: the contract supports exactly what a maker/taker strategy needs.

const (
	// IntentPostOnly quotes maker-only; an order that would cross the book
	// is rejected by the venue (a normal-path outcome).
	IntentPostOnly OrderIntent = "post_only"
	// IntentIOC executes immediately up to the price cap; any remainder is
	// canceled.
	IntentIOC OrderIntent = "ioc"
)

Order intents.

type OrderRejectedEvent

type OrderRejectedEvent struct {
	OrderID OrderID
	Reason  string
}

OrderRejectedEvent reports that an order is finished without having filled in full — a post-only order that would cross, an IOC remainder the venue cancelled, a short-term order that reached its expiry block. A normal-path event, not an error.

It means no further fills are coming for this order. It does not mean the order did nothing: an IOC that filled part of its size and had the rest cancelled produces both a FillEvent and this. Consumers must treat it as closing the order, not as voiding it.

It can also arrive before a fill it accounts for, when the venue reports the removal in an earlier message than the execution. Adapters emit fills first within a single venue message, but they do not reorder across messages — buffering the account stream to tidy this would cost latency on the one signal that must not have any. Attribute fills by OrderID rather than assuming a rejection is the last word on an order.

type Position

type Position struct {
	VenueID VenueID
	Symbol  Symbol
	// Size is signed: long positive, short negative, zero flat.
	Size          decimal.Decimal
	EntryPrice    decimal.Decimal
	UnrealizedPnL decimal.Decimal
	// Time is the venue observation timestamp.
	Time time.Time
}

Position is a venue position observation.

type PositionEvent

type PositionEvent struct {
	Position Position
}

PositionEvent reports a position observation.

type ReconnectConfig

type ReconnectConfig struct {
	// InitialDelay is the backoff delay after an unexpected drop; it resets
	// on every successful open.
	InitialDelay time.Duration
	// MaxDelay caps the exponential backoff.
	MaxDelay time.Duration
	// Multiplier scales the delay after each failed reconnect attempt.
	Multiplier float64
	// IdleTimeout is the maximum inbound silence (messages, pings, pongs)
	// before a connection is treated as half-open (e.g. a sleep/wake or
	// network partition where no close frame arrives) and force-reconnected.
	IdleTimeout time.Duration
}

ReconnectConfig tunes WebSocket reconnect behavior shared by all venue adapters: exponential backoff plus half-open detection.

func DefaultReconnectConfig

func DefaultReconnectConfig() ReconnectConfig

DefaultReconnectConfig returns the reference tuning used in production.

func (ReconnectConfig) IsZero

func (c ReconnectConfig) IsZero() bool

IsZero reports whether c is the zero value.

func (ReconnectConfig) Validate

func (c ReconnectConfig) Validate() error

Validate rejects partially specified configs. Use the zero value (adapters substitute DefaultReconnectConfig) or specify every field.

type Side

type Side string

Side is the order side.

const (
	SideBuy  Side = "buy"
	SideSell Side = "sell"
)

Order sides.

type Symbol

type Symbol string

Symbol is the normalized instrument label, e.g. "SOL-PERP". The symbol universe is application configuration, not library contract; adapters map it to venue-native market identifiers.

type VenueExecutor

type VenueExecutor interface {
	// VenueID identifies the venue this executor trades on.
	VenueID() VenueID

	// Connect loads venue market metadata, validates credentials, starts the
	// authenticated account stream, and emits a verified initial snapshot
	// (Connected, Position, Margin) before returning. Unsupported positions
	// or incomplete account state fail Connect.
	Connect(ctx context.Context) (ExecutionMetadata, error)

	// PlaceOrder rounds, signs, and submits the order.
	//
	// ctx cancellation is honored only until the transaction is dispatched;
	// once submission has started, PlaceOrder waits for the venue outcome
	// under the adapter's own request timeout — canceling mid-flight would
	// leave the submission ambiguous or orphan a live order.
	//
	// If a submission outcome is unknown, the adapter latches a fault: the
	// affected transaction is never retried and subsequent submissions fail
	// with ErrTxOutcomeUnknown until the adapter reconciles with venue state.
	PlaceOrder(ctx context.Context, order NewOrder) (OrderAck, error)

	// CancelOrder cancels a previously placed order by its executor-scoped
	// ID. Returns ErrUnknownOrder for IDs the executor is not tracking.
	CancelOrder(ctx context.Context, id OrderID) error

	// AccountEvents returns the executor's single account-event stream. The
	// channel is buffered (DefaultAccountEventBuffer); when it fills the
	// adapter blocks rather than dropping — a dropped fill would silently
	// corrupt position state. Consume promptly. The channel is closed only
	// after Close completes, so range termination means the executor is
	// terminal.
	AccountEvents() <-chan AccountEvent

	// Close tears the executor down: a final DisconnectedEvent is emitted
	// (if connected), then the event channel is closed. Close is terminal;
	// reconnecting means constructing a new executor.
	Close() error
}

VenueExecutor is the normalized execution contract every venue adapter implements.

Design invariants:

  • Maker orders are always post-only. A taker-crossing rejection is a normal-path outcome — PlaceOrder returns AckRejected (plus an OrderRejectedEvent); it is never an error.
  • REST and WebSocket payloads are validated strictly. Unexpected shapes abort the connection instead of being guessed at (fail fast).
  • Authenticated account-stream fills are the only source of truth for executions. Adapters never infer fills or positions from book state.
  • Price tick and size step rounding are the adapter's responsibility.

Concurrency: Connect must return before any other method is called — it resolves the market metadata and signer the other methods read, and nothing else establishes that ordering. Afterwards PlaceOrder, CancelOrder, and AccountEvents are safe to use concurrently, and Close is terminal.

Event ordering contract (AccountEvents):

  • ConnectedEvent and DisconnectedEvent alternate, including across internal reconnects.
  • Other events are emitted only between a ConnectedEvent and the following DisconnectedEvent.

type VenueID

type VenueID string

VenueID identifies a supported venue.

const (
	// VenueLighter is the Lighter (zkLighter) venue.
	VenueLighter VenueID = "lighter"
	// VenueDydx is the dYdX v4 venue.
	VenueDydx VenueID = "dydx"
	// VenueHyperliquid is the Hyperliquid venue.
	VenueHyperliquid VenueID = "hyperliquid"
)

Supported venues.

Directories

Path Synopsis
cmd
godex-smoke command
godex-smoke runs the adoption-gate smoke test against a live venue (normally testnet) with real credentials taken from the environment.
godex-smoke runs the adoption-gate smoke test against a live venue (normally testnet) with real credentials taken from the environment.
Package decimal provides an immutable fixed-point decimal: value = mantissa / 10^scale.
Package decimal provides an immutable fixed-point decimal: value = mantissa / 10^scale.
Package dydx implements godex.VenueExecutor for dYdX v4.
Package dydx implements godex.VenueExecutor for dYdX v4.
internal/pb
Package pb holds the generated protobuf types the dydx adapter needs to build, sign, and broadcast dYdX v4 (Cosmos SDK) transactions.
Package pb holds the generated protobuf types the dydx adapter needs to build, sign, and broadcast dYdX v4 (Cosmos SDK) transactions.
Package hyperliquid implements godex.VenueExecutor for Hyperliquid.
Package hyperliquid implements godex.VenueExecutor for Hyperliquid.
internal
book
Package book implements shared order-book reassembly for snapshot+delta market-data feeds.
Package book implements shared order-book reassembly for snapshot+delta market-data feeds.
dedupe
Package dedupe bounds the "have I already reported this?" state an adapter needs to keep an at-most-once event contract.
Package dedupe bounds the "have I already reported this?" state an adapter needs to keep an at-most-once event contract.
ws
Package ws implements the shared WebSocket connection lifecycle used by venue adapters: exponential-backoff reconnect and half-open detection.
Package ws implements the shared WebSocket connection lifecycle used by venue adapters: exponential-backoff reconnect and half-open detection.
Package lighter implements godex.VenueExecutor for Lighter (zkLighter).
Package lighter implements godex.VenueExecutor for Lighter (zkLighter).
Package smoketest runs the venue-agnostic adoption-gate scenario against a live VenueExecutor (normally on testnet).
Package smoketest runs the venue-agnostic adoption-gate scenario against a live VenueExecutor (normally on testnet).

Jump to

Keyboard shortcuts

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