Skip to content
tiennm99devPublic

About

Unofficial Go client for the TCBS iFlash Open API

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tcbs-api

Unofficial Go client for the TCBS iFlash Open API; not affiliated with or endorsed by Techcom Securities.

The iFlash Open API lets a TCBS customer trade stocks and derivatives, query accounts, move money and stream market data over REST and WebSocket. This module has two clients for it:

The API moves real money. PlaceOrder, TransferMoney, the derivative order calls and the margin deposit/withdraw calls act on a live account. Try them against the SIT environment (tcbs.SITBaseURL) first.

TCBS, TCInvest and iFlash are names of Techcom Securities. The API specs belong to TCBS and are not kept in the current tree (older commits still contain copies); go generate downloads the one it needs from the TCBS developer portal.

Install

go get github.com/tiennm99dev/tcbs-api/tcbs     # hand-written client
go get github.com/tiennm99dev/tcbs-api/tcbsgen  # generated client

The module is pure Go (CGO_ENABLED=0 builds) and needs the Go version in go.mod.

Authentication

  1. Register an API key in TCInvest, under Utilities (Tiện ích), iFlash Open API: https://tcinvest.tcbs.com.vn/iflash-open-api.
  2. Exchange the key plus the current Smart OTP (iOTP) from the TCInvest app for a JWT at POST /gaia/v1/oauth2/openapi/token. The token is valid for at most 8 hours from the iOTP confirmation; request a new one when it expires.
  3. Send the JWT as Authorization: Bearer <token> on every other call.

Two key-management calls take the API key in the body instead of a JWT: POST /gaia/v1/oauth2/openapi/apikey/info (key only) returns the key's expiry date, and POST /gaia/v1/oauth2/openapi/apikey/extend (key and iOTP) extends it. Read the key from the environment or a secret store; never commit it. Details: docs/authentication.md.

Quick start (tcbs)

package main

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

    "github.com/tiennm99dev/tcbs-api/tcbs"
)

func main() {
    client := tcbs.NewClient() // production; tcbs.WithBaseURL(tcbs.SITBaseURL) for SIT
    ctx := context.Background()

    // Exchange the API key and iOTP for a JWT; the client keeps the token.
    if _, err := client.GetToken(ctx, os.Getenv("TCBS_API_KEY"), os.Getenv("TCBS_OTP")); err != nil {
        log.Fatal(err)
    }

    prices, err := client.GetStockPrices(ctx, []string{"FPT", "VNM"})
    if err != nil {
        log.Fatal(err)
    }
    for _, p := range prices {
        fmt.Printf("%s: %.0f\n", p.Symbol, p.MatchPrice.Float())
    }
}

A longer walk-through is in tcbs/example. It reads its credentials and account numbers from environment variables and points at SIT, because one step places an order.

TCBS sometimes sends numbers as strings ("1000", "0E-10"). tcbs.Number accepts both, but only some tcbs models use it: holdings (StockHoldingInfo), cash (CashInvestment, IAInfo), REST stock prices (MarketStockInfo) and the margin risk models. Every other numeric field, including all derivative, money-transfer and WebSocket models, is a plain float64/int, and a string there makes the call fail to decode, as in tcbsgen. Error responses come back as *tcbs.APIError, with Code and Message filled from a {code,message} body.

Hand-written client (tcbs)

Configuration

// Production (default)
client := tcbs.NewClient()

// SIT environment
client := tcbs.NewClient(tcbs.WithBaseURL(tcbs.SITBaseURL))

// Custom HTTP client
client := tcbs.NewClient(tcbs.WithHTTPClient(&http.Client{Timeout: 60 * time.Second}))

// Pre-set token
client := tcbs.NewClient(tcbs.WithToken("your-jwt-token"))

API coverage

Authentication

Method Description
GetToken Exchange API Key + OTP for JWT token

Account

Method Description
GetSubAccountInfo Get sub-account profile information
ListAccounts List the sub-accounts under a custody code

Stock Orders

Method Description
PlaceOrder Place a stock order
UpdateOrder Modify an existing order
CancelOrder Cancel existing orders

Stock Queries

Method Description
GetOrders Get order book
GetOrderByID Get specific order by ID
GetMatchingDetails Get order matching details
GetPurchasingPower Get purchasing power
GetPurchasingPowerBySymbol Get purchasing power for symbol
GetPurchasingPowerBySymbolPrice Get purchasing power for symbol at price
GetMarginQuota Get margin quota
GetMarginAccountInfo Get margin account risk info
GetSupplementaryLoanPackages Get loan package details
GetLoans Get loan list
GetStockAssets Get stock holdings
GetCashBalance Get cash balance
GetCashStatements Get cash statement history
GetMarginInfo Get debt inquiry info

Market Data

Method Description
GetStockPrices Get stock ticker pricing
GetForeignRoom Get foreign investor room info
GetPutThroughInfo Get put-through match info
GetIntradayHistory Get intraday price history
GetSupplyDemand Get 15-min supply/demand data
GetSupplyDemandExt Get extended supply/demand data
GetSupplyDemandMonth Get monthly supply/demand data

Money Management

Method Description
TransferMoney Transfer between sub-accounts
DepositMargin Deposit margin for derivatives
WithdrawMargin Withdraw margin for derivatives

Derivatives

Method Description
GetDerivativeCashStatus Get derivative cash/margin overview
GetDerivativeClosedPositions Get closed positions
GetDerivativeOpenPositions Get open positions
GetDerivativeNormalOrders List normal orders
GetDerivativeConditionOrders List conditional orders
PlaceDerivativeNormalOrder Place normal order
PlaceDerivativeConditionOrder Place conditional order
ChangeDerivativeNormalOrder Modify normal order
ChangeDerivativeConditionOrder Modify conditional order
CancelDerivativeNormalOrder Cancel normal order
CancelDerivativeConditionOrder Cancel conditional order
GetDerivativeMarketInfo Get derivative contract pricing

WebSocket Streams

Method Endpoint Portal page
ConnectStockPrice /ws/thesis/v1/stream/normal stock/ws-price-board
ConnectDerivativePrice /ws/thesis/v1/stream/derivative derivative/ws-price-board
ConnectCenter /ws/ouranos/v1/stream stock/ws-history
ConnectStockMatch /ws/aither stock/ws-order-matching
ConnectDerivativeMatch /ws/nesoi derivative/ws-order-matching

WebSocket usage

Connect* only opens the socket (sending the stored token as an Authorization: Bearer dial header) and passes every received frame to your handler. It does not authenticate, subscribe or keep the connection alive; the caller sends those frames with ws.Send. The streams use pipe-delimited text frames, not JSON. The portal pages linked above are the protocol reference.

The price board on /ws/thesis/v1/stream/normal:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

authOK := make(chan bool, 1)
ws, err := client.ConnectStockPrice(ctx, func(_ websocket.MessageType, data []byte) {
    msg := string(data)
    if strings.HasPrefix(msg, "d|0|") { // auth result
        select {
        case authOK <- strings.Contains(msg, `"success":true`):
        default:
        }
        return
    }
    fmt.Println(msg)
})
if err != nil {
    log.Fatal(err)
}
defer ws.Close()

// 1. Authenticate in-band with the base64-encoded JWT.
auth := "d|a|||" + base64.StdEncoding.EncodeToString([]byte(tok.Token))
if err := ws.Send(ctx, []byte(auth)); err != nil {
    log.Fatal(err)
}

// 2. Wait for d|0|{"success":true,...} before subscribing.
select {
case ok := <-authOK:
    if !ok {
        log.Fatal("websocket auth rejected")
    }
case <-time.After(10 * time.Second):
    log.Fatal("no websocket auth response")
}

// 3. Keep the connection alive: a text-frame heartbeat every 2s
//    (the server announces a 15s timeout with d|33|15).
go func() {
    t := time.NewTicker(2 * time.Second)
    defer t.Stop()
    for {
        select {
        case <-ctx.Done():
            return
        case <-t.C:
            if err := ws.Send(ctx, []byte("d|p|||")); err != nil {
                return
            }
        }
    }
}()

// 4. Subscribe to ticker updates.
if err := ws.Send(ctx, []byte("d|s|tk|bp+bi+tm+mp+op+fe|FPT")); err != nil {
    log.Fatal(err)
}

Here tok is the *tcbs.TokenResponse returned by GetToken. The handler runs on the read goroutine, so keep it short.

The frames differ per stream:

Stream Auth frame Server success reply Subscribe Heartbeat (text frame, every 2s) Server timeout
/ws/thesis/v1/stream/normal, /ws/thesis/v1/stream/derivative d|a|||<base64(JWT)> d|0|{"success":true,...} d|s|tk|bp+bi+tm+mp+op+fe|<SYMBOLS> d|p||| 15s (d|33|15)
/ws/ouranos/v1/stream d|a|||<base64(JWT)> d|0|{"success":true,...} d|st|C001+C002S60+C002S900|<SYMBOLS> d|po 15s (d|33|15)
/ws/aither authenticate|<base64({"jwt":"<JWT>"})> authenticate|{"success":true,...} subscribe|<base64({"topic":"STOCK_ORDER"})> ping|1 7s (pingTimeout|7)
/ws/nesoi authenticate|<base64({"jwt":"<JWT>"})> authenticate|{"success":true,...} subscribe|<base64({"topic":"DE_ORDER"})>, also DE_CONDITIONAL_ORDER, DE_OPEN_POSITION ping|1 45s

Use only text-frame heartbeats; the portal says the server rejects RFC 6455 ping frames. The client used here (nhooyr.io/websocket) sends none on its own.

Generated client (tcbsgen)

tcbsgen covers every operation in the portal spec, including the API-key calls the hand-written client lacks. Each operation is named after its portal docs page (stock/asset becomes StockAsset).

import "github.com/tiennm99dev/tcbs-api/tcbsgen"

c, err := tcbsgen.NewClientWithResponses("https://openapi.tcbs.com.vn",
    tcbsgen.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
        req.Header.Set("Authorization", "Bearer "+token)
        return nil
    }))
if err != nil {
    log.Fatal(err)
}
resp, err := c.StockAssetWithResponse(ctx, accountNo)
if err != nil {
    log.Fatal(err)
}
if resp.JSON200 != nil && resp.JSON200.Assets != nil {
    for _, a := range *resp.JSON200.Assets {
        fmt.Println(*a.Symbol, *a.Quantity)
    }
}

Generated models use plain int64/float64, so a number TCBS sends as a string makes the whole ...WithResponse call return an error. The plain Client methods (c.ClientInterface.StockAsset, for example) return the raw *http.Response if you need to decode such a body yourself. See docs/spec-and-codegen.md for this and the other known spec gaps.

To regenerate:

go generate ./tcbsgen

go generate downloads the portal spec, checks it against the SHA-256 pinned in tcbsgen/generate.go, repairs it with tools/specfix into tcbsgen/openapi.json (gitignored), and runs oapi-codegen. If TCBS publishes a changed spec, the checksum check fails: review the change, update the pinned checksum, and regenerate. Do not edit tcbsgen/client.gen.go by hand. specfix also fails if an endpoint is added or removed without a matching entry in tcbsgen/operation-ids.json.

Development

go vet ./...
go test ./...
CGO_ENABLED=0 go build ./...

Tests make no network calls; the clients run against httptest servers.

Documentation

License

The code is licensed under the Apache License 2.0; see LICENSE. The TCBS API specs are not part of the current tree; copies in older commits remain TCBS's and are not covered by this license.

About

Unofficial Go client for the TCBS iFlash Open API

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages