ctgexchange

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

CTG.EXCHANGE Go SDK

Go client for the CTG.EXCHANGE exchange API — a thin, typed wrapper over the public REST + WebSocket interface.

Install

go get github.com/cryptorg-io/ctg-exchange-go

Requires Go 1.23+.

Quick start

Public market data — no key needed
ctx := context.Background()
c := ctg-exchange.NewClient(ctg-exchange.Config{})

symbols, err := c.GetSymbols(ctx)
// ...
book, err := c.GetOrderBook(ctx, "CTGUSDT")
Private account & trading

API keys are created in the CTG.EXCHANGE web app (Account → API keys). Read them from the environment — never hard-code them.

c := ctg-exchange.NewClient(ctg-exchange.Config{
    APIKey:    os.Getenv("CTG_EXCHANGE_API_KEY"),
    APISecret: os.Getenv("CTG_EXCHANGE_API_SECRET"),
})

balances, err := c.GetBalances(ctx)

res, err := c.PlaceOrder(ctx, "CTGUSDT", ctg-exchange.PlaceOrderParams{
    Side: "buy", Type: "limit", Price: "100.00", Qty: "1",
})
// res.Order, res.Trades
WebSocket streams
stream := ctg-exchange.NewMarketDataStream(ctg-exchange.StreamConfig{
    Channels: []string{"trades@CTGUSDT"},
})
defer stream.Close()

for {
    msg, err := stream.Next(ctx)
    if err != nil {
        break
    }
    fmt.Println(msg.Channel, msg.Type, string(msg.Data))
}

NewUserStream opens the private stream, authenticating in-band with a signed first frame. Subscribe to any of orders, trades, balances:

stream, err := ctg-exchange.NewUserStream(
    os.Getenv("CTG_EXCHANGE_API_KEY"),
    os.Getenv("CTG_EXCHANGE_API_SECRET"),
    ctg-exchange.StreamConfig{Channels: []string{"orders", "balances"}},
)
if err != nil {
    log.Fatal(err)
}
defer stream.Close()

for {
    msg, err := stream.Next(ctx)
    if err != nil {
        break
    }
    fmt.Println(msg.Channel, msg.Type, string(msg.Data))
}

Both streams auto-reconnect and re-subscribe on a dropped socket. StreamMessage.Data is a json.RawMessage — unmarshal it into the shape you need.

The decimal contract

Every monetary value — Price, Qty, Volume, fee amounts — is a string ("3500.55"), never a float. That is lossless; parse it with a big-decimal library if you need arithmetic.

Errors

A non-2xx response is returned as an *APIError. Use errors.As and the Is* helpers:

var apiErr *ctg-exchange.APIError
if errors.As(err, &apiErr) && apiErr.IsRateLimited() {
    time.Sleep(time.Duration(apiErr.RetryAfter) * time.Second)
}

APIError carries StatusCode, Code, Message and RequestID. Set Config.MaxRetries to auto-retry 429s.

What this SDK does not do

Withdrawals are not part of the CTG.EXCHANGE API and not in this SDK — they require a wallet signature and happen only in the web app.

Development

go vet ./...
go test ./...   # offline tests run with no credentials

Integration tests run only when CTG_EXCHANGE_API_KEY / CTG_EXCHANGE_API_SECRET are set, and are read-only — they never place orders.

License

Apache-2.0

Documentation

Overview

Package ctgexchange is a Go client for the CTG.EXCHANGE exchange API.

CTG.EXCHANGE is a hybrid crypto exchange (off-chain matcher, on-chain custody on BNB Smart Chain). This package is a thin, typed client over its public REST + WebSocket API.

c := ctgexchange.NewClient(ctgexchange.Config{APIKey: key, APISecret: secret})
balances, err := c.GetBalances(ctx)

Every monetary value is a decimal string (the API's decimal contract), never a float — parse it with a big-decimal library if you need arithmetic. Withdrawals are intentionally not part of the API.

Index

Constants

View Source
const DefaultBaseURL = "https://api.ctg.exchange"

DefaultBaseURL is the production REST base URL.

View Source
const DefaultWSBaseURL = "wss://api.ctg.exchange"

DefaultWSBaseURL is the production WebSocket base URL.

View Source
const Version = "0.1.0"

Version is the SDK version.

Variables

View Source
var ErrMissingCredentials = errors.New(
	"ctgexchange: api key and secret are required for private endpoints")

ErrMissingCredentials is returned when a private endpoint is called without an API key and secret configured.

View Source
var ErrStreamClosed = errors.New("ctgexchange: stream closed")

ErrStreamClosed is returned by Stream.Next once the stream has stopped and will deliver no more messages.

Functions

func RESTCanonicalString

func RESTCanonicalString(ts int64, method, requestURI, body string) string

RESTCanonicalString builds the four-field canonical string a REST signature covers:

<ts>\n<METHOD>\n<request-uri>\n<hex sha256 of body>

requestURI is the path plus query string exactly as sent on the request line, e.g. /api/v1/me/orders/CTGUSDT?limit=50.

func RESTHeaders

func RESTHeaders(keyID, secret, method, requestURI, body string, ts int64) map[string]string

RESTHeaders returns the three signed headers a private REST request must carry. ts is Unix seconds; the server rejects timestamps outside its signature window (default 30s), so keep the local clock in sync.

func SHA256Hex

func SHA256Hex(body string) string

SHA256Hex returns the hex SHA-256 of a request body. SHA256Hex("") is the hash used for every body-less request.

func SignREST

func SignREST(secret string, ts int64, method, requestURI, body string) string

SignREST returns the lowercase-hex HMAC-SHA256 of the REST canonical string, keyed by the API key secret.

func SignWSAuth

func SignWSAuth(secret string, ts int64) string

SignWSAuth returns the lowercase-hex HMAC-SHA256 over "ws-auth\n<ts>".

func WSAuthMessage

func WSAuthMessage(keyID, secret string, ts int64) map[string]any

WSAuthMessage builds the signed "auth" frame to send as the first frame on the private WebSocket stream.

Types

type APIError

type APIError struct {
	// StatusCode is the HTTP status of the response.
	StatusCode int
	// Code is the API's machine-readable "error" field.
	Code string
	// Message is the API's human-readable "message" field, if any.
	Message string
	// RequestID is the API's "request_id" — quote it when reporting.
	RequestID string
	// RetryAfter is the Retry-After header value in seconds on a 429,
	// or 0 when the server did not send one.
	RetryAfter int
}

APIError is a non-2xx HTTP response from the API. Inspect StatusCode, or use the Is* helpers, to branch on the kind of failure:

var apiErr *ctgexchange.APIError
if errors.As(err, &apiErr) && apiErr.IsRateLimited() {
        time.Sleep(time.Duration(apiErr.RetryAfter) * time.Second)
}

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) IsBadRequest

func (e *APIError) IsBadRequest() bool

IsBadRequest reports whether the request was malformed (400).

func (*APIError) IsForbidden

func (e *APIError) IsForbidden() bool

IsForbidden reports a wrong-scope key or off-allowlist IP (403).

func (*APIError) IsNotFound

func (e *APIError) IsNotFound() bool

IsNotFound reports an unknown symbol or order (404).

func (*APIError) IsRateLimited

func (e *APIError) IsRateLimited() bool

IsRateLimited reports a rate-limit response (429); see RetryAfter.

func (*APIError) IsUnauthorized

func (e *APIError) IsUnauthorized() bool

IsUnauthorized reports a missing/expired/invalid API key (401).

type Balance

type Balance struct {
	Asset     string `json:"asset"`
	Available string `json:"available"`
	Reserved  string `json:"reserved"`
}

Balance is a per-asset balance.

type BookLevel

type BookLevel struct {
	Price string `json:"price"`
	Qty   string `json:"qty"`
}

BookLevel is one price level of an order book.

type Candle

type Candle struct {
	Open      string `json:"open"`
	High      string `json:"high"`
	Low       string `json:"low"`
	Close     string `json:"close"`
	Volume    string `json:"volume"`
	OpenTime  int64  `json:"open_time"`
	CloseTime int64  `json:"close_time"`
	Trades    int    `json:"trades"`
}

Candle is a single OHLC candle.

type CandleOptions

type CandleOptions struct {
	Limit int
	From  int64
	To    int64
}

CandleOptions are the optional query parameters for GetCandles. Leave a field zero to omit it. Omit From/To for the latest Limit candles; set them (Unix ms) for a historical window [From, To).

type Client

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

Client is a REST client for the CTG.EXCHANGE API. It is safe for concurrent use.

func NewClient

func NewClient(cfg Config) *Client

NewClient builds a Client from cfg.

func (*Client) CancelAllOrders

func (c *Client) CancelAllOrders(ctx context.Context, symbol string) ([]Order, error)

CancelAllOrders cancels every open order for symbol (trade scope).

func (*Client) CancelOrder

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

CancelOrder cancels one order (trade scope).

func (*Client) GetBalances

func (c *Client) GetBalances(ctx context.Context) ([]Balance, error)

GetBalances returns the API key owner's per-asset balances (read scope).

func (*Client) GetCandles

func (c *Client) GetCandles(
	ctx context.Context, symbol, interval string, opts CandleOptions,
) ([]Candle, error)

GetCandles returns candles for symbol at interval (1m/5m/15m/1h/4h/1d; defaults to 1m when empty). See CandleOptions for windowed queries.

func (*Client) GetFees

func (c *Client) GetFees(ctx context.Context) (UserFees, error)

GetFees returns the API key owner's fee/rebate snapshot (read scope).

func (*Client) GetMyTrades

func (c *Client) GetMyTrades(
	ctx context.Context, symbol string, q TradeQuery,
) ([]Trade, error)

GetMyTrades returns the owner's trade history for symbol (read scope).

func (*Client) GetOpenOrders

func (c *Client) GetOpenOrders(ctx context.Context) ([]Order, error)

GetOpenOrders returns every open order across all symbols (read scope). Each Order carries its own Symbol; decimal fields are converted per that order's scales. For closed-order history use GetOrders per symbol.

func (*Client) GetOrder

func (c *Client) GetOrder(ctx context.Context, symbol, orderID string) (Order, error)

GetOrder returns one order by its canonical server id (read scope).

func (*Client) GetOrderBook

func (c *Client) GetOrderBook(ctx context.Context, symbol string) (OrderBook, error)

GetOrderBook returns the current order book for symbol.

func (*Client) GetOrders

func (c *Client) GetOrders(
	ctx context.Context, symbol string, q OrderQuery,
) ([]Order, error)

GetOrders lists the owner's orders for symbol (read scope).

func (*Client) GetSymbols

func (c *Client) GetSymbols(ctx context.Context) ([]Symbol, error)

GetSymbols returns all trading symbols with their order filters.

func (*Client) GetTicker

func (c *Client) GetTicker(ctx context.Context, symbol string) (Ticker, error)

GetTicker returns the 24h ticker for symbol.

func (*Client) GetTickers

func (c *Client) GetTickers(ctx context.Context) ([]Ticker, error)

GetTickers returns the 24h ticker for every symbol.

func (*Client) GetTrades

func (c *Client) GetTrades(ctx context.Context, symbol string, limit int) ([]Trade, error)

GetTrades returns recent public trade prints for symbol. A limit of 0 uses the server default.

func (*Client) ModifyOrder

func (c *Client) ModifyOrder(
	ctx context.Context, symbol, orderID string, p ModifyOrderParams,
) (Order, error)

ModifyOrder modifies a resting order's price and quantity (trade scope). The API expects the full new state — set both fields.

func (*Client) PlaceOrder

func (c *Client) PlaceOrder(
	ctx context.Context, symbol string, p PlaceOrderParams,
) (OrderResult, error)

PlaceOrder places an order (trade scope). A ClientOrderID is generated when p.ClientOrderID is empty, so retries de-dup safely.

type Config

type Config struct {
	APIKey     string
	APISecret  string
	BaseURL    string        // default DefaultBaseURL
	Timeout    time.Duration // default 10s; ignored when HTTPClient is set
	MaxRetries int           // retry 429s, honouring Retry-After; default 0
	HTTPClient *http.Client  // optional custom HTTP client
}

Config configures a Client. APIKey and APISecret are required only for the private (/api/v1/me/...) endpoints; public market data works without them.

type FeeRebate

type FeeRebate struct {
	AccountID   string `json:"account_id"`   // referrer wallet address
	FractionBps int    `json:"fraction_bps"` // share of the fee in bps
}

FeeRebate is the fraction of a charged fee credited to a referrer. An empty object (zero fields) when no rebate applies.

type ModifyOrderParams

type ModifyOrderParams struct {
	NewPrice string
	NewQty   string
}

ModifyOrderParams are the parameters for modifying an order. Send both fields — the API expects the full new state.

type Order

type Order struct {
	ID                string     `json:"id"`
	ClientOrderID     string     `json:"client_order_id"`
	UID               string     `json:"uid"` // owner wallet address
	Symbol            string     `json:"symbol"`
	Side              string     `json:"side"` // buy / sell
	Type              string     `json:"type"` // limit / market
	TimeInForce       string     `json:"time_in_force"`
	Status            string     `json:"status"`
	Price             string     `json:"price"`
	AvgExecutionPrice string     `json:"avg_execution_price"`
	Qty               string     `json:"qty"`
	FilledQty         string     `json:"filled_qty"`
	RemainingQty      string     `json:"remaining_qty"`
	FeeAmount         string     `json:"fee_amount"` // decimal string in fee_asset scale
	FeeAsset          string     `json:"fee_asset"`
	TakerFeeBps       int        `json:"taker_fee_bps"` // per-order override; 0 = default
	MakerFeeBps       int        `json:"maker_fee_bps"`
	Rebate            *FeeRebate `json:"rebate"`
	CreatedAt         string     `json:"created_at"`
	UpdatedAt         string     `json:"updated_at"`
}

Order is an order as returned by the API.

type OrderBook

type OrderBook struct {
	Symbol       string      `json:"symbol"`
	LastUpdateID int64       `json:"last_update_id"`
	Bids         []BookLevel `json:"bids"`
	Asks         []BookLevel `json:"asks"`
}

OrderBook is a full order book snapshot.

type OrderQuery

type OrderQuery struct {
	Status string // open / partially_filled / filled / canceled
	Limit  int
	Offset int
}

OrderQuery are the optional filters for GetOrders.

type OrderResult

type OrderResult struct {
	Order  Order   `json:"order"`
	Trades []Trade `json:"trades"`
}

OrderResult is the response to placing or modifying an order: the order plus any fills that happened immediately.

type PlaceOrderParams

type PlaceOrderParams struct {
	Side          string // buy / sell
	Type          string // limit / market
	Price         string
	Qty           string
	ClientOrderID string
}

PlaceOrderParams are the parameters for placing an order. Price and Qty are decimal strings. ClientOrderID is generated when empty.

type Rebate

type Rebate struct {
	ReferrerAccount string `json:"referrer_account"`
	FractionBps     int    `json:"fraction_bps"`
}

Rebate is the referral-rebate entry inside a UserFees snapshot.

type Stream

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

Stream is a WebSocket stream. Call Next in a loop to consume messages; it connects on the first call and, unless reconnect is disabled, transparently reconnects and re-subscribes on a drop. A Stream is not safe for concurrent Next calls.

func NewMarketDataStream

func NewMarketDataStream(cfg StreamConfig) *Stream

NewMarketDataStream opens the public market-data stream — channels orderbook / ticker / candles / trades. No authentication.

func NewUserStream

func NewUserStream(apiKey, apiSecret string, cfg StreamConfig) (*Stream, error)

NewUserStream opens the private stream — the caller's orders / trades / balances — authenticated in-band with a signed first frame.

func (*Stream) Close

func (s *Stream) Close()

Close stops the stream and any reconnection. After Close, Next returns ErrStreamClosed.

func (*Stream) Next

func (s *Stream) Next(ctx context.Context) (StreamMessage, error)

Next returns the next message. It blocks until one arrives, ctx is cancelled, or the stream closes (then it returns ErrStreamClosed or the failure that ended the stream).

func (*Stream) Subscribe

func (s *Stream) Subscribe(channels ...string)

Subscribe adds channels (e.g. "orderbook@CTGUSDT"). They are kept across reconnects.

func (*Stream) Unsubscribe

func (s *Stream) Unsubscribe(channels ...string)

Unsubscribe drops channels.

type StreamConfig

type StreamConfig struct {
	BaseURL          string        // default DefaultWSBaseURL
	Channels         []string      // (re)subscribed on every connect
	DisableReconnect bool          // turn off auto-reconnect
	ReconnectDelay   time.Duration // default 2s
}

StreamConfig configures a WebSocket stream.

type StreamMessage

type StreamMessage struct {
	Type    string          // "snapshot" or "update"
	Channel string          // e.g. "orderbook"
	Symbol  string          // e.g. "CTGUSDT"; empty for private channels
	Data    json.RawMessage // channel payload — unmarshal as needed
}

StreamMessage is one server WebSocket message: a snapshot or update.

type Symbol

type Symbol struct {
	Symbol          string `json:"symbol"`
	BaseAsset       string `json:"base_asset"`
	QuoteAsset      string `json:"quote_asset"`
	PriceScale      int    `json:"price_scale"`
	QtyScale        int    `json:"qty_scale"`
	QuoteAssetScale int    `json:"quote_asset_scale"`
	TickSize        string `json:"tick_size"`
	StepSize        string `json:"step_size"`
	MinPrice        string `json:"min_price"`
	MaxPrice        string `json:"max_price"`
	MinQty          string `json:"min_qty"`
	MaxQty          string `json:"max_qty"`
	MinNotional     string `json:"min_notional"`
}

Symbol is a trading pair plus its order filters.

type Ticker

type Ticker struct {
	Symbol    string `json:"symbol"`
	Price     string `json:"price"`
	Open      string `json:"open"`
	High      string `json:"high"`
	Low       string `json:"low"`
	Close     string `json:"close"`
	Volume    string `json:"volume"` // base-asset volume
	Trades    int    `json:"trades"`
	ChangeBps int    `json:"change_bps"`
	Ts        int64  `json:"ts"` // epoch
}

Ticker is a 24h rolling ticker.

type Trade

type Trade struct {
	ID            string     `json:"id"`
	Symbol        string     `json:"symbol"`
	Price         string     `json:"price"`
	Qty           string     `json:"qty"`
	BuyOrderID    string     `json:"buy_order_id"`
	SellOrderID   string     `json:"sell_order_id"`
	BuyUID        string     `json:"buy_uid"`  // buyer wallet address
	SellUID       string     `json:"sell_uid"` // seller wallet address
	AggressorSide string     `json:"aggressor_side"`
	MakerOrderID  string     `json:"maker_order_id"`
	TakerOrderID  string     `json:"taker_order_id"`
	BuyFeeBps     int        `json:"buy_fee_bps"`  // omitted when 0
	SellFeeBps    int        `json:"sell_fee_bps"` // omitted when 0
	BuyRebate     *FeeRebate `json:"buy_rebate"`
	SellRebate    *FeeRebate `json:"sell_rebate"`
	BuyFeeAmount  string     `json:"buy_fee_amount"`
	BuyFeeAsset   string     `json:"buy_fee_asset"`
	SellFeeAmount string     `json:"sell_fee_amount"`
	SellFeeAsset  string     `json:"sell_fee_asset"`
	CreatedAt     string     `json:"created_at"`
}

Trade is a matched trade. Both sides' addresses, order ids and fees are exposed — see "matcher transparency".

type TradeQuery

type TradeQuery struct {
	Limit  int
	Offset int
}

TradeQuery are the optional filters for GetMyTrades.

type UserFees

type UserFees struct {
	TakerFeeBps *int    `json:"taker_fee_bps"`
	MakerFeeBps *int    `json:"maker_fee_bps"`
	Rebate      *Rebate `json:"rebate"`
}

UserFees is a fee/rebate snapshot. A nil pointer integer means no override applies — the platform default is then in effect.

Directories

Path Synopsis
examples
account command
Private account access and order placement.
Private account access and order placement.
marketdata command
Public market data — no API key needed.
Public market data — no API key needed.
stream command
Streaming market data and private account updates over WebSocket.
Streaming market data and private account updates over WebSocket.

Jump to

Keyboard shortcuts

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