Initial functional commit

This commit is contained in:
Options Vamp
2024-05-14 21:50:57 -04:00
parent 70888184fa
commit 461b8cef7b
23 changed files with 2704 additions and 1 deletions

View File

@@ -1,2 +1,60 @@
# tastytrade
# Go Tastytrade Open API Wrapper
Go API wrapper for the Tastytrade Open API
![Build](https://github.com/optionsvamp/tastytrade/actions/workflows/build.yaml/badge.svg)
## Table of Contents
1. [Introduction](#introduction)
2. [Installation](#installation)
3. [Usage](#usage)
4. [Testing](#testing)
5. [Contributing](#contributing)
6. [License](#license)
## Introduction
This project provides a Go wrapper for the Tastytrade Open API. It allows developers to interact with Tastytrade's financial data and services in a more Go-idiomatic way, abstracting away the details of direct HTTP requests and responses.
## Installation
To install this project, you can use `go get`:
```bash
go get github.com/optionsvamp/tastytrade
```
Then, import it in your Go code:
```
import "github.com/optionsvamp/tastytrade"
```
## Usage
Here's a basic example of how to use this wrapper to get account balances:
```
api := NewTastytradeAPI("your-api-key")
balances, err := api.GetAccountBalances("your-account-number")
if err != nil {
log.Fatal(err)
}
fmt.Println(balances)
```
## Testing
To run the tests for this project, you can use go test:
```bash
go test ./...
```
## Contributing
Contributions to this project are welcome! Please submit a pull request or open an issue on GitHub.
## License
This project is released into the public domain under the Unlicense. For more information, please refer to the LICENSE file or visit https://unlicense.org.

86
api.go Normal file
View File

@@ -0,0 +1,86 @@
package tastytrade
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
baseURL = "https://api.tastytrade.com"
)
// TastytradeAPI represents the Tastytrade API client
type TastytradeAPI struct {
httpClient *http.Client
authToken string
host string
}
// NewTastytradeAPI creates a new instance of TastytradeAPI
func NewTastytradeAPI(hosts ...string) *TastytradeAPI {
host := baseURL
if len(hosts) > 0 {
host = hosts[0]
}
return &TastytradeAPI{
httpClient: &http.Client{Timeout: 10 * time.Second},
host: host,
}
}
// fetchData sends a GET request to the specified URL with authorization
func (api *TastytradeAPI) fetchData(url string) (map[string]interface{}, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", api.authToken)
resp, err := api.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
return nil, fmt.Errorf("client error occurred: status code %d", resp.StatusCode)
} else if resp.StatusCode >= 500 {
return nil, fmt.Errorf("server error occurred: status code %d", resp.StatusCode)
}
var data map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
return data, nil
}
// Helper function to fetch and unmarshal data
func (api *TastytradeAPI) fetchDataAndUnmarshal(urlVal string, v interface{}) error {
req, err := http.NewRequest("GET", urlVal, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", api.authToken)
resp, err := api.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
return fmt.Errorf("client error occurred: status code %d", resp.StatusCode)
} else if resp.StatusCode >= 500 {
return fmt.Errorf("server error occurred: status code %d", resp.StatusCode)
}
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return err
}
return nil
}

44
api_test.go Normal file
View File

@@ -0,0 +1,44 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestFetchData(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"data": "test"}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.fetchData(server.URL)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp["data"] != "test" {
t.Errorf("expected %s, got %s", "test", resp["data"])
}
}
func TestFetchDataAndUnmarshal(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"data": "test"}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
var v map[string]interface{}
err := api.fetchDataAndUnmarshal(server.URL, &v)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if v["data"] != "test" {
t.Errorf("expected %s, got %s", "test", v["data"])
}
}

118
balances.go Normal file
View File

@@ -0,0 +1,118 @@
package tastytrade
import (
"fmt"
)
type BalanceData struct {
AccountNumber string `json:"account-number"`
CashBalance float64 `json:"cash-balance,string"`
LongEquityValue float64 `json:"long-equity-value,string"`
ShortEquityValue float64 `json:"short-equity-value,string"`
LongDerivativeValue float64 `json:"long-derivative-value,string"`
ShortDerivativeValue float64 `json:"short-derivative-value,string"`
LongFuturesValue float64 `json:"long-futures-value,string"`
ShortFuturesValue float64 `json:"short-futures-value,string"`
LongFuturesDerivativeValue float64 `json:"long-futures-derivative-value,string"`
ShortFuturesDerivativeValue float64 `json:"short-futures-derivative-value,string"`
LongMargineableValue float64 `json:"long-margineable-value,string"`
ShortMargineableValue float64 `json:"short-margineable-value,string"`
MarginEquity float64 `json:"margin-equity,string"`
EquityBuyingPower float64 `json:"equity-buying-power,string"`
DerivativeBuyingPower float64 `json:"derivative-buying-power,string"`
DayTradingBuyingPower float64 `json:"day-trading-buying-power,string"`
FuturesMarginRequirement float64 `json:"futures-margin-requirement,string"`
AvailableTradingFunds float64 `json:"available-trading-funds,string"`
MaintenanceRequirement float64 `json:"maintenance-requirement,string"`
MaintenanceCallValue float64 `json:"maintenance-call-value,string"`
RegTCallValue float64 `json:"reg-t-call-value,string"`
DayTradingCallValue float64 `json:"day-trading-call-value,string"`
DayEquityCallValue float64 `json:"day-equity-call-value,string"`
NetLiquidatingValue float64 `json:"net-liquidating-value,string"`
CashAvailableToWithdraw float64 `json:"cash-available-to-withdraw,string"`
DayTradeExcess float64 `json:"day-trade-excess,string"`
PendingCash float64 `json:"pending-cash,string"`
PendingCashEffect string `json:"pending-cash-effect"`
LongCryptocurrencyValue float64 `json:"long-cryptocurrency-value,string"`
ShortCryptocurrencyValue float64 `json:"short-cryptocurrency-value,string"`
CryptocurrencyMarginRequirement float64 `json:"cryptocurrency-margin-requirement,string"`
UnsettledCryptocurrencyFiatAmount float64 `json:"unsettled-cryptocurrency-fiat-amount,string"`
UnsettledCryptocurrencyFiatEffect string `json:"unsettled-cryptocurrency-fiat-effect"`
ClosedLoopAvailableBalance float64 `json:"closed-loop-available-balance,string"`
EquityOfferingMarginRequirement float64 `json:"equity-offering-margin-requirement,string"`
LongBondValue float64 `json:"long-bond-value,string"`
BondMarginRequirement float64 `json:"bond-margin-requirement,string"`
UsedDerivativeBuyingPower float64 `json:"used-derivative-buying-power,string"`
SnapshotDate string `json:"snapshot-date"`
RegTMarginRequirement float64 `json:"reg-t-margin-requirement,string"`
FuturesOvernightMarginRequirement float64 `json:"futures-overnight-margin-requirement,string"`
FuturesIntradayMarginRequirement float64 `json:"futures-intraday-margin-requirement,string"`
MaintenanceExcess float64 `json:"maintenance-excess,string"`
PendingMarginInterest float64 `json:"pending-margin-interest,string"`
EffectiveCryptocurrencyBuyingPower float64 `json:"effective-cryptocurrency-buying-power,string"`
UpdatedAt string `json:"updated-at"`
}
type BalanceResponse struct {
Data BalanceData `json:"data"`
Context string `json:"context"`
}
type AccountBalanceSnapshot struct {
AccountNumber string `json:"account-number"`
CashBalance float64 `json:"cash-balance,string"`
LongEquityValue float64 `json:"long-equity-value,string"`
ShortEquityValue float64 `json:"short-equity-value,string"`
LongDerivativeValue float64 `json:"long-derivative-value,string"`
ShortDerivativeValue float64 `json:"short-derivative-value,string"`
LongFuturesValue float64 `json:"long-futures-value,string"`
ShortFuturesValue float64 `json:"short-futures-value,string"`
LongMargineableValue float64 `json:"long-margineable-value,string"`
ShortMargineableValue float64 `json:"short-margineable-value,string"`
MarginEquity float64 `json:"margin-equity,string"`
EquityBuyingPower float64 `json:"equity-buying-power,string"`
DerivativeBuyingPower float64 `json:"derivative-buying-power,string"`
DayTradingBuyingPower float64 `json:"day-trading-buying-power,string"`
FuturesMarginRequirement float64 `json:"futures-margin-requirement,string"`
AvailableTradingFunds float64 `json:"available-trading-funds,string"`
MaintenanceRequirement float64 `json:"maintenance-requirement,string"`
MaintenanceCallValue float64 `json:"maintenance-call-value,string"`
RegTCallValue float64 `json:"reg-t-call-value,string"`
DayTradingCallValue float64 `json:"day-trading-call-value,string"`
DayEquityCallValue float64 `json:"day-equity-call-value,string"`
NetLiquidatingValue float64 `json:"net-liquidating-value,string"`
DayTradeExcess float64 `json:"day-trade-excess,string"`
PendingCash float64 `json:"pending-cash,string"`
PendingCashEffect string `json:"pending-cash-effect"`
SnapshotDate string `json:"snapshot-date"`
TimeOfDay string `json:"time-of-day"`
}
type AccountBalanceSnapshotResponse struct {
Data struct {
Items []AccountBalanceSnapshot `json:"items"`
} `json:"data"`
Context string `json:"context"`
}
// GetAccountBalances retrieves balances for a specific account
func (api *TastytradeAPI) GetAccountBalances(accountNumber string) (BalanceResponse, error) {
url := fmt.Sprintf("%s/accounts/%s/balances", api.host, accountNumber)
var response BalanceResponse
err := api.fetchDataAndUnmarshal(url, &response)
if err != nil {
return BalanceResponse{}, err
}
return response, nil
}
// GetAccountBalanceSnapshots retrieves balance snapshots for a specific account
func (api *TastytradeAPI) GetAccountBalanceSnapshots(accountNumber string, snapshotDate string, timeOfDay string) (AccountBalanceSnapshotResponse, error) {
url := fmt.Sprintf("%s/accounts/%s/balance-snapshots?snapshot-date=%s&time-of-day=%s", api.host, accountNumber, snapshotDate, timeOfDay)
var response AccountBalanceSnapshotResponse
err := api.fetchDataAndUnmarshal(url, &response)
if err != nil {
return AccountBalanceSnapshotResponse{}, err
}
return response, nil
}

43
balances_test.go Normal file
View File

@@ -0,0 +1,43 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetAccountBalances(t *testing.T) {
// Create a mock HTTP server
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
// Test request parameters
if req.URL.String() != "/accounts/123/balances" {
t.Errorf("got: %s, want: /accounts/123/balances", req.URL.String())
}
// Send response to be tested
rw.Write([]byte(`{"data": {"account-number": "123", "cash-balance": "1000"}, "context": "test"}`))
}))
// Close the server when test finishes
defer server.Close()
// Initialize a new TastytradeAPI instance
api := NewTastytradeAPI(server.URL)
// Invoke the method to be tested
resp, err := api.GetAccountBalances("123")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Data.AccountNumber != "123" {
t.Errorf("expected %s, got %s", "123", resp.Data.AccountNumber)
}
if resp.Data.CashBalance != 1000 {
t.Errorf("expected %f, got %f", 1000.0, resp.Data.CashBalance)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
}

188
customer.go Normal file
View File

@@ -0,0 +1,188 @@
package tastytrade
import (
"encoding/json"
"fmt"
)
type Address struct {
StreetOne string `json:"street-one"`
City string `json:"city"`
StateRegion string `json:"state-region"`
PostalCode string `json:"postal-code"`
Country string `json:"country"`
IsForeign bool `json:"is-foreign"`
IsDomestic bool `json:"is-domestic"`
}
type CustomerSuitability struct {
ID int `json:"id"`
MaritalStatus string `json:"marital-status"`
NumberOfDependents int `json:"number-of-dependents"`
EmploymentStatus string `json:"employment-status"`
Occupation string `json:"occupation"`
EmployerName string `json:"employer-name"`
JobTitle string `json:"job-title"`
AnnualNetIncome int `json:"annual-net-income"`
NetWorth int `json:"net-worth"`
LiquidNetWorth int `json:"liquid-net-worth"`
StockTradingExperience string `json:"stock-trading-experience"`
CoveredOptionsTradingExperience string `json:"covered-options-trading-experience"`
UncoveredOptionsTradingExperience string `json:"uncovered-options-trading-experience"`
FuturesTradingExperience string `json:"futures-trading-experience"`
}
type Person struct {
ExternalID string `json:"external-id"`
FirstName string `json:"first-name"`
LastName string `json:"last-name"`
BirthDate string `json:"birth-date"`
CitizenshipCountry string `json:"citizenship-country"`
USACitizenshipType string `json:"usa-citizenship-type"`
MaritalStatus string `json:"marital-status"`
NumberOfDependents int `json:"number-of-dependents"`
EmploymentStatus string `json:"employment-status"`
Occupation string `json:"occupation"`
EmployerName string `json:"employer-name"`
JobTitle string `json:"job-title"`
}
type CustomerData struct {
ID string `json:"id"`
FirstName string `json:"first-name"`
LastName string `json:"last-name"`
Address Address `json:"address"`
MailingAddress Address `json:"mailing-address"`
CustomerSuitability CustomerSuitability `json:"customer-suitability"`
USACitizenshipType string `json:"usa-citizenship-type"`
IsForeign bool `json:"is-foreign"`
MobilePhoneNumber string `json:"mobile-phone-number"`
Email string `json:"email"`
TaxNumberType string `json:"tax-number-type"`
TaxNumber string `json:"tax-number"`
BirthDate string `json:"birth-date"`
ExternalID string `json:"external-id"`
CitizenshipCountry string `json:"citizenship-country"`
SubjectToTaxWithholding bool `json:"subject-to-tax-withholding"`
AgreedToMargining bool `json:"agreed-to-margining"`
AgreedToTerms bool `json:"agreed-to-terms"`
HasIndustryAffiliation bool `json:"has-industry-affiliation"`
HasPoliticalAffiliation bool `json:"has-political-affiliation"`
HasListedAffiliation bool `json:"has-listed-affiliation"`
IsProfessional bool `json:"is-professional"`
HasDelayedQuotes bool `json:"has-delayed-quotes"`
HasPendingOrApprovedApplication bool `json:"has-pending-or-approved-application"`
IdentifiableType string `json:"identifiable-type"`
Person Person `json:"person"`
}
type CustomerResponse struct {
Context string `json:"context"`
Data CustomerData `json:"data"`
}
type Account struct {
AccountNumber string `json:"account-number"`
ExternalID string `json:"external-id"`
OpenedAt string `json:"opened-at"`
Nickname string `json:"nickname"`
AccountTypeName string `json:"account-type-name"`
DayTraderStatus bool `json:"day-trader-status"`
IsClosed bool `json:"is-closed"`
IsFirmError bool `json:"is-firm-error"`
IsFirmProprietary bool `json:"is-firm-proprietary"`
IsFuturesApproved bool `json:"is-futures-approved"`
IsTestDrive bool `json:"is-test-drive"`
MarginOrCash string `json:"margin-or-cash"`
IsForeign bool `json:"is-foreign"`
FundingDate string `json:"funding-date"`
InvestmentObjective string `json:"investment-objective"`
FuturesAccountPurpose string `json:"futures-account-purpose"`
SuitableOptionsLevel string `json:"suitable-options-level"`
CreatedAt string `json:"created-at"`
}
type AccountContainer struct {
Account Account `json:"account"`
AuthorityLevel string `json:"authority-level"`
}
type AccountData struct {
Items []AccountContainer `json:"items"`
}
type AccountResponse struct {
Context string `json:"context"`
Data Account `json:"data"`
}
type AccountsResponse struct {
Context string `json:"context"`
Data AccountData `json:"data"`
}
// GetCustomerInfo retrieves customer information
func (api *TastytradeAPI) GetCustomerInfo() (CustomerResponse, error) {
url := fmt.Sprintf("%s/customers/me", api.host)
data, err := api.fetchData(url)
if err != nil {
return CustomerResponse{}, err
}
var response CustomerResponse
jsonData, err := json.Marshal(data)
if err != nil {
return CustomerResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return CustomerResponse{}, err
}
return response, nil
}
// ListCustomerAccounts retrieves a list of customer accounts
func (api *TastytradeAPI) ListCustomerAccounts() (AccountsResponse, error) {
url := fmt.Sprintf("%s/customers/me/accounts", api.host)
data, err := api.fetchData(url)
if err != nil {
return AccountsResponse{}, err
}
var response AccountsResponse
jsonData, err := json.Marshal(data)
if err != nil {
return AccountsResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return AccountsResponse{}, err
}
return response, nil
}
// GetAccount retrieves account information for a specific account
func (api *TastytradeAPI) GetAccount(accountNumber string) (AccountResponse, error) {
url := fmt.Sprintf("%s/customers/me/accounts/%s", api.host, accountNumber)
data, err := api.fetchData(url)
if err != nil {
return AccountResponse{}, err
}
var response AccountResponse
jsonData, err := json.Marshal(data)
if err != nil {
return AccountResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return AccountResponse{}, err
}
return response, nil
}

77
customer_test.go Normal file
View File

@@ -0,0 +1,77 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetCustomerInfo(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"id": "123", "first-name": "John", "last-name": "Doe"}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetCustomerInfo()
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if resp.Data.ID != "123" {
t.Errorf("expected %s, got %s", "123", resp.Data.ID)
}
}
func TestListCustomerAccounts(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"items": [{"account": {"account-number": "123"}}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.ListCustomerAccounts()
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].Account.AccountNumber != "123" {
t.Errorf("expected %s, got %s", "123", resp.Data.Items[0].Account.AccountNumber)
}
}
func TestGetAccount(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"account-number": "123"}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetAccount("123")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if resp.Data.AccountNumber != "123" {
t.Errorf("expected %s, got %s", "123", resp.Data.AccountNumber)
}
}

159
equity.go Normal file
View File

@@ -0,0 +1,159 @@
package tastytrade
import (
"encoding/json"
"fmt"
"net/url"
)
type EquityResponse struct {
Context string `json:"context"`
Data EquityData `json:"data"`
}
type Tick struct {
Threshold string `json:"threshold,omitempty"`
Value string `json:"value"`
}
type EquityData struct {
Active bool `json:"active"`
BorrowRate string `json:"borrow-rate"`
Cusip string `json:"cusip"`
Description string `json:"description"`
ID int `json:"id"`
InstrumentType string `json:"instrument-type"`
IsClosingOnly bool `json:"is-closing-only"`
IsETF bool `json:"is-etf"`
IsFractionalQuantityEligible bool `json:"is-fractional-quantity-eligible"`
IsIlliquid bool `json:"is-illiquid"`
IsIndex bool `json:"is-index"`
IsOptionsClosingOnly bool `json:"is-options-closing-only"`
Lendability string `json:"lendability"`
ListedMarket string `json:"listed-market"`
MarketTimeInstrumentCollection string `json:"market-time-instrument-collection"`
OptionTickSizes []Tick `json:"option-tick-sizes"`
ShortDescription string `json:"short-description"`
StreamerSymbol string `json:"streamer-symbol"`
Symbol string `json:"symbol"`
TickSizes []Tick `json:"tick-sizes"`
}
type EquityListResponse struct {
Context string `json:"context"`
Data struct {
Items []EquityData `json:"items"`
} `json:"data"`
}
type EquityQueryParams struct {
Symbol []string `json:"symbol"`
Lendability string `json:"lendability"`
IsIndex *bool `json:"is-index"`
IsETF *bool `json:"is-etf"`
}
type ActiveEquityQueryParams struct {
Lendability string `json:"lendability"`
PerPage int `json:"per-page"`
PageOffset int `json:"page-offset"`
}
// GetEquityData retrieves data for a specific equity symbol
func (api *TastytradeAPI) GetEquityData(symbol string) (EquityResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/equities/%s", api.host, symbol)
data, err := api.fetchData(urlVal)
if err != nil {
return EquityResponse{}, err
}
var response EquityResponse
jsonData, err := json.Marshal(data)
if err != nil {
return EquityResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return EquityResponse{}, err
}
return response, nil
}
// ListEquities retrieves a list of all equities
func (api *TastytradeAPI) ListEquities(params *EquityQueryParams) (EquityListResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/equities", api.host)
if params != nil {
queryParams := url.Values{}
for _, symbol := range params.Symbol {
queryParams.Add("symbol[]", symbol)
}
if params.Lendability != "" {
queryParams.Add("lendability", params.Lendability)
}
if params.IsIndex != nil {
queryParams.Add("is-index", fmt.Sprintf("%t", *params.IsIndex))
}
if params.IsETF != nil {
queryParams.Add("is-etf", fmt.Sprintf("%t", *params.IsETF))
}
urlVal = fmt.Sprintf("%s?%s", urlVal, queryParams.Encode())
}
data, err := api.fetchData(urlVal)
if err != nil {
return EquityListResponse{}, err
}
var response EquityListResponse
jsonData, err := json.Marshal(data)
if err != nil {
return EquityListResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return EquityListResponse{}, err
}
return response, nil
}
// ListActiveEquities retrieves a list of all active equities
func (api *TastytradeAPI) ListActiveEquities(params *ActiveEquityQueryParams) (EquityListResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/equities/active", api.host)
if params != nil {
queryParams := url.Values{}
if params.Lendability != "" {
queryParams.Add("lendability", params.Lendability)
}
if params.PerPage != 0 {
queryParams.Add("per-page", fmt.Sprintf("%d", params.PerPage))
}
if params.PageOffset != 0 {
queryParams.Add("page-offset", fmt.Sprintf("%d", params.PageOffset))
}
urlVal = fmt.Sprintf("%s?%s", urlVal, queryParams.Encode())
}
data, err := api.fetchData(urlVal)
if err != nil {
return EquityListResponse{}, err
}
var response EquityListResponse
jsonData, err := json.Marshal(data)
if err != nil {
return EquityListResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return EquityListResponse{}, err
}
return response, nil
}

73
equity_test.go Normal file
View File

@@ -0,0 +1,73 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetEquityData(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"symbol": "AAPL"}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetEquityData("AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if resp.Data.Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Symbol)
}
}
func TestListEquities(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"items": [{"symbol": "AAPL"}, {"symbol": "GOOG"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.ListEquities(nil)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 2 {
t.Errorf("expected %d, got %d", 2, len(resp.Data.Items))
}
}
func TestListActiveEquities(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"items": [{"symbol": "AAPL"}, {"symbol": "GOOG"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.ListActiveEquities(nil)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 2 {
t.Errorf("expected %d, got %d", 2, len(resp.Data.Items))
}
}

98
example/main.go Normal file
View File

@@ -0,0 +1,98 @@
package main
import (
"fmt"
"github.com/optionsvamp/tastytrade"
"os"
)
func main() {
// Example usage:
api := tastytrade.NewTastytradeAPI("")
// Authenticate with Tastytrade API
err := api.Authenticate(os.Getenv("USER"), os.Getenv("PWD"))
if err != nil {
fmt.Println("Authentication failed:", err)
return
}
//customerInfo, err := api.GetCustomerInfo()
//if err != nil {
// fmt.Println("Error fetching customer data:", err)
// return
//}
//fmt.Println("Customer data:", customerInfo)
// Get a list of customer accounts
customerAccounts, err := api.ListCustomerAccounts()
if err != nil {
fmt.Println("Error fetching customer accounts:", err)
return
}
//fmt.Println("Customer accounts:", customerAccounts)
// Get account trading status for the first account in the previous response
status, err := api.GetAccountTradingStatus(customerAccounts.Data.Items[0].Account.AccountNumber)
if err != nil {
fmt.Println("Error fetching customer account status:", err)
return
}
fmt.Println("Customer account status:", status)
// Get account
acct, err := api.GetAccount(customerAccounts.Data.Items[0].Account.AccountNumber)
if err != nil {
fmt.Println("Error fetching customer account:", err)
return
}
fmt.Println("Customer account:", acct)
// Get account balance
bal, err := api.GetAccountBalances(customerAccounts.Data.Items[0].Account.AccountNumber)
if err != nil {
fmt.Println("Error fetching account balance:", err)
return
}
fmt.Println("Account balance:", bal)
snap, err := api.GetAccountBalanceSnapshots(customerAccounts.Data.Items[0].Account.AccountNumber, "2024-01-01", "BOD")
if err != nil {
fmt.Println("Error fetching account balance snapshot:", err)
return
}
fmt.Println("Account balance snapshot:", snap)
// Get symbol data
symbolData, err := api.GetEquityData("AAPL")
if err != nil {
fmt.Println("Error fetching symbol data:", err)
return
}
fmt.Println("Symbol data:", symbolData)
transactions, err := api.GetTransactions(customerAccounts.Data.Items[2].Account.AccountNumber, &tastytrade.TransactionQueryParams{
Symbol: "/MCLN4",
InstrumentType: "Future",
})
if err != nil {
fmt.Println("Error fetching transactions data:", err)
return
}
fmt.Println("Transactions data:", transactions)
symbols, err := api.ListEquities(nil)
if err != nil {
fmt.Println("Error fetching symbol data:", err)
return
}
_ = symbols
// Get option chain
optionChain, err := api.ListOptionsChainsDetailed("AAPL")
if err != nil {
fmt.Println("Error fetching option chain:", err)
return
}
fmt.Println("Option chain:", optionChain)
}

492
futures.go Normal file
View File

@@ -0,0 +1,492 @@
package tastytrade
import (
"encoding/json"
"fmt"
"net/url"
)
type FutureETFEquivalent struct {
Symbol string `json:"symbol"`
ShareQuantity int `json:"share-quantity"`
}
type FutureProduct struct {
RootSymbol string `json:"root-symbol"`
Code string `json:"code"`
Description string `json:"description"`
ClearingCode string `json:"clearing-code"`
ClearingExchangeCode string `json:"clearing-exchange-code"`
ClearportCode string `json:"clearport-code"`
LegacyCode string `json:"legacy-code"`
Exchange string `json:"exchange"`
LegacyExchangeCode string `json:"legacy-exchange-code"`
ProductType string `json:"product-type"`
ListedMonths []string `json:"listed-months"`
ActiveMonths []string `json:"active-months"`
NotionalMultiplier string `json:"notional-multiplier"`
TickSize string `json:"tick-size"`
DisplayFactor string `json:"display-factor"`
StreamerExchangeCode string `json:"streamer-exchange-code"`
SmallNotional bool `json:"small-notional"`
BackMonthFirstCalendarSymbol bool `json:"back-month-first-calendar-symbol"`
FirstNotice bool `json:"first-notice"`
CashSettled bool `json:"cash-settled"`
SecurityGroup string `json:"security-group"`
MarketSector string `json:"market-sector"`
Roll struct {
Name string `json:"name"`
ActiveCount int `json:"active-count"`
CashSettled bool `json:"cash-settled"`
BusinessDaysOffset int `json:"business-days-offset"`
FirstNotice bool `json:"first-notice"`
} `json:"roll"`
}
type TickSize struct {
Value string `json:"value"`
Threshold string `json:"threshold,omitempty"`
Symbol string `json:"symbol,omitempty"`
}
type Future struct {
Symbol string `json:"symbol"`
ProductCode string `json:"product-code"`
ContractSize string `json:"contract-size"`
TickSize string `json:"tick-size"`
NotionalMultiplier string `json:"notional-multiplier"`
MainFraction string `json:"main-fraction"`
SubFraction string `json:"sub-fraction"`
DisplayFactor string `json:"display-factor"`
LastTradeDate string `json:"last-trade-date"`
ExpirationDate string `json:"expiration-date"`
ClosingOnlyDate string `json:"closing-only-date"`
Active bool `json:"active"`
ActiveMonth bool `json:"active-month"`
NextActiveMonth bool `json:"next-active-month"`
IsClosingOnly bool `json:"is-closing-only"`
StopsTradingAt string `json:"stops-trading-at"`
ExpiresAt string `json:"expires-at"`
ProductGroup string `json:"product-group"`
Exchange string `json:"exchange"`
RollTargetSymbol string `json:"roll-target-symbol"`
StreamerExchangeCode string `json:"streamer-exchange-code"`
StreamerSymbol string `json:"streamer-symbol"`
BackMonthFirstCalendarSymbol bool `json:"back-month-first-calendar-symbol"`
IsTradeable bool `json:"is-tradeable"`
FutureETFEquivalent FutureETFEquivalent `json:"future-etf-equivalent"`
FutureProduct FutureProduct `json:"future-product"`
TickSizes []TickSize `json:"tick-sizes"`
OptionTickSizes []TickSize `json:"option-tick-sizes"`
SpreadTickSizes []TickSize `json:"spread-tick-sizes"`
}
type FuturesQueryResponse struct {
Data struct {
Items []Future `json:"items"`
} `json:"data"`
}
type FuturesQueryParams struct {
Symbol []string `json:"symbol"`
ProductCode []string `json:"product-code"`
}
type FutureResponse struct {
Data Future `json:"data"`
Context string `json:"context"`
}
type FutureOptionProduct struct {
RootSymbol string `json:"root-symbol"`
CashSettled bool `json:"cash-settled"`
Code string `json:"code"`
LegacyCode string `json:"legacy-code"`
ClearportCode string `json:"clearport-code"`
ClearingCode string `json:"clearing-code"`
ClearingExchangeCode string `json:"clearing-exchange-code"`
ClearingPriceMultiplier string `json:"clearing-price-multiplier"`
DisplayFactor string `json:"display-factor"`
Exchange string `json:"exchange"`
ProductType string `json:"product-type"`
ExpirationType string `json:"expiration-type"`
SettlementDelayDays int `json:"settlement-delay-days"`
IsRollover bool `json:"is-rollover"`
MarketSector string `json:"market-sector"`
}
type FutureProductsResponse struct {
Data struct {
Items []FutureProduct `json:"items"`
} `json:"data"`
Context string `json:"context"`
}
type FutureProductResponse struct {
Data FutureProduct `json:"data"`
Context string `json:"context"`
}
type FuturesOptionExpirationNested struct {
UnderlyingSymbol string `json:"underlying-symbol"`
RootSymbol string `json:"root-symbol"`
OptionRootSymbol string `json:"option-root-symbol"`
OptionContractSymbol string `json:"option-contract-symbol"`
Asset string `json:"asset"`
ExpirationDate string `json:"expiration-date"`
DaysToExpiration int `json:"days-to-expiration"`
ExpirationType string `json:"expiration-type"`
SettlementType string `json:"settlement-type"`
NotionalValue string `json:"notional-value"`
DisplayFactor string `json:"display-factor"`
StrikeFactor string `json:"strike-factor"`
StopsTradingAt string `json:"stops-trading-at"`
ExpiresAt string `json:"expires-at"`
TickSizes []TickSize `json:"tick-sizes"`
Strikes []StrikeNested `json:"strikes"`
}
type OptionChain struct {
UnderlyingSymbol string `json:"underlying-symbol"`
RootSymbol string `json:"root-symbol"`
ExerciseStyle string `json:"exercise-style"`
Expirations []FuturesOptionExpirationNested `json:"expirations"`
}
type FutureOptionChainsNestedData struct {
Futures []Future `json:"futures"`
OptionChains []OptionChain `json:"option-chains"`
}
type FutureOptionChainsNestedResponse struct {
Data FutureOptionChainsNestedData `json:"data"`
Context string `json:"context"`
}
type FutureOption struct {
Symbol string `json:"symbol"`
UnderlyingSymbol string `json:"underlying-symbol"`
ProductCode string `json:"product-code"`
ExpirationDate string `json:"expiration-date"`
RootSymbol string `json:"root-symbol"`
OptionRootSymbol string `json:"option-root-symbol"`
StrikePrice string `json:"strike-price"`
Exchange string `json:"exchange"`
ExchangeSymbol string `json:"exchange-symbol"`
StreamerSymbol string `json:"streamer-symbol"`
OptionType string `json:"option-type"`
ExerciseStyle string `json:"exercise-style"`
IsVanilla bool `json:"is-vanilla"`
IsPrimaryDeliverable bool `json:"is-primary-deliverable"`
FuturePriceRatio string `json:"future-price-ratio"`
Multiplier string `json:"multiplier"`
UnderlyingCount string `json:"underlying-count"`
IsConfirmed bool `json:"is-confirmed"`
NotionalValue string `json:"notional-value"`
DisplayFactor string `json:"display-factor"`
SecurityExchange string `json:"security-exchange"`
SxID string `json:"sx-id"`
SettlementType string `json:"settlement-type"`
StrikeFactor string `json:"strike-factor"`
MaturityDate string `json:"maturity-date"`
IsExercisableWeekly bool `json:"is-exercisable-weekly"`
LastTradeTime string `json:"last-trade-time"`
DaysToExpiration int `json:"days-to-expiration"`
IsClosingOnly bool `json:"is-closing-only"`
Active bool `json:"active"`
StopsTradingAt string `json:"stops-trading-at"`
ExpiresAt string `json:"expires-at"`
FutureOptionProduct FutureOptionProduct `json:"future-option-product"`
}
type FutureOptionChainsDetailedResponse struct {
Data struct {
Items []FutureOption `json:"items"`
} `json:"data"`
Context string `json:"context"`
}
type FutureOptionsDetailedResponse struct {
Data struct {
Items []FutureOption `json:"items"`
} `json:"data"`
Context string `json:"context"`
}
type FutureOptionProductsResponse struct {
Data struct {
Items []FutureOptionProduct `json:"items"`
} `json:"data"`
Context string `json:"context"`
}
type FutureOptionProductDetailedResponse struct {
Data FutureOptionProduct `json:"data"`
Context string `json:"context"`
}
type FutureOptionsQueryParams struct {
Symbol []string `json:"symbol"`
OptionRootSymbol string `json:"option-root-symbol"`
ExpirationDate string `json:"expiration-date"`
OptionType string `json:"option-type"`
StrikePrice float64 `json:"strike-price"`
}
type FutureOptionDetailedResponse struct {
Data FutureOption `json:"data"`
Context string `json:"context"`
}
// QueryFutures retrieves a list of futures
func (api *TastytradeAPI) QueryFutures(params *FuturesQueryParams) (FuturesQueryResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/futures", api.host)
if params != nil {
queryParams := url.Values{}
for _, symbol := range params.Symbol {
queryParams.Add("symbol[]", symbol)
}
for _, productCode := range params.ProductCode {
queryParams.Add("product-code[]", productCode)
}
urlVal = fmt.Sprintf("%s?%s", urlVal, queryParams.Encode())
}
data, err := api.fetchData(urlVal)
if err != nil {
return FuturesQueryResponse{}, err
}
var response FuturesQueryResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FuturesQueryResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FuturesQueryResponse{}, err
}
return response, nil
}
// GetFuture retrieves data for a specific future symbol
func (api *TastytradeAPI) GetFuture(symbol string) (FutureResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/futures/%s", api.host, url.PathEscape(symbol))
data, err := api.fetchData(urlVal)
if err != nil {
return FutureResponse{}, err
}
var response FutureResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FutureResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FutureResponse{}, err
}
return response, nil
}
// ListFutureProducts retrieves a list of future products
func (api *TastytradeAPI) ListFutureProducts() (FutureProductsResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/future-products", api.host)
data, err := api.fetchData(urlVal)
if err != nil {
return FutureProductsResponse{}, err
}
var response FutureProductsResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FutureProductsResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FutureProductsResponse{}, err
}
return response, nil
}
// GetFutureProduct retrieves data for a specific future product
func (api *TastytradeAPI) GetFutureProduct(exchange string, symbol string) (FutureProductResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/future-products/%s/%s", api.host, exchange, url.PathEscape(symbol))
data, err := api.fetchData(urlVal)
if err != nil {
return FutureProductResponse{}, err
}
var response FutureProductResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FutureProductResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FutureProductResponse{}, err
}
return response, nil
}
// ListFutureOptionChainsNested retrieves nested future option chain data for a specific symbol
func (api *TastytradeAPI) ListFutureOptionChainsNested(symbol string) (FutureOptionChainsNestedResponse, error) {
urlVal := fmt.Sprintf("%s/futures-option-chains/%s/nested", api.host, symbol)
data, err := api.fetchData(urlVal)
if err != nil {
return FutureOptionChainsNestedResponse{}, err
}
var response FutureOptionChainsNestedResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FutureOptionChainsNestedResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FutureOptionChainsNestedResponse{}, err
}
return response, nil
}
// ListFutureOptionChainsDetailed retrieves detailed future option chain data for a specific symbol
func (api *TastytradeAPI) ListFutureOptionChainsDetailed(symbol string) (FutureOptionChainsDetailedResponse, error) {
urlVal := fmt.Sprintf("%s/futures-option-chains/%s", api.host, symbol)
data, err := api.fetchData(urlVal)
if err != nil {
return FutureOptionChainsDetailedResponse{}, err
}
var response FutureOptionChainsDetailedResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FutureOptionChainsDetailedResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FutureOptionChainsDetailedResponse{}, err
}
return response, nil
}
// ListFutureOptions retrieves future option data for a specific symbol with query parameters
func (api *TastytradeAPI) ListFutureOptions(params *FutureOptionsQueryParams) (FutureOptionsDetailedResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/future-options", api.host)
if params != nil {
queryParams := url.Values{}
for _, symbol := range params.Symbol {
queryParams.Add("symbol[]", symbol)
}
if params.OptionRootSymbol != "" {
queryParams.Add("option-root-symbol", params.OptionRootSymbol)
}
if params.ExpirationDate != "" {
queryParams.Add("expiration-date", params.ExpirationDate)
}
if params.OptionType != "" {
queryParams.Add("option-type", params.OptionType)
}
if params.StrikePrice != 0 {
queryParams.Add("strike-price", fmt.Sprintf("%.2f", params.StrikePrice))
}
urlVal = fmt.Sprintf("%s?%s", urlVal, queryParams.Encode())
}
data, err := api.fetchData(urlVal)
if err != nil {
return FutureOptionsDetailedResponse{}, err
}
var response FutureOptionsDetailedResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FutureOptionsDetailedResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FutureOptionsDetailedResponse{}, err
}
return response, nil
}
// GetFutureOption retrieves data for a specific future option symbol
func (api *TastytradeAPI) GetFutureOption(symbol string) (FutureOptionDetailedResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/future-options/%s", api.host, url.PathEscape(symbol))
data, err := api.fetchData(urlVal)
if err != nil {
return FutureOptionDetailedResponse{}, err
}
var response FutureOptionDetailedResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FutureOptionDetailedResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FutureOptionDetailedResponse{}, err
}
return response, nil
}
// ListFutureOptionProducts retrieves a list of future option products
func (api *TastytradeAPI) ListFutureOptionProducts() (FutureOptionProductsResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/future-option-products", api.host)
data, err := api.fetchData(urlVal)
if err != nil {
return FutureOptionProductsResponse{}, err
}
var response FutureOptionProductsResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FutureOptionProductsResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FutureOptionProductsResponse{}, err
}
return response, nil
}
// GetFutureOptionProduct retrieves data for a specific future option product
func (api *TastytradeAPI) GetFutureOptionProduct(exchange string, rootSymbol string) (FutureOptionProductDetailedResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/future-option-products/%s/%s", api.host, exchange, rootSymbol)
data, err := api.fetchData(urlVal)
if err != nil {
return FutureOptionProductDetailedResponse{}, err
}
var response FutureOptionProductDetailedResponse
jsonData, err := json.Marshal(data)
if err != nil {
return FutureOptionProductDetailedResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return FutureOptionProductDetailedResponse{}, err
}
return response, nil
}

304
futures_test.go Normal file
View File

@@ -0,0 +1,304 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestQueryFutures(t *testing.T) {
expectedURL := "/instruments/futures"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.Write([]byte(`{"data": {"items": [{"symbol": "AAPL"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.QueryFutures(nil)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].Symbol)
}
}
func TestQueryFuturesError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
_, err := api.QueryFutures(nil)
if err == nil {
t.Errorf("expected an error, got nil")
}
}
func TestQueryFuturesWithParams(t *testing.T) {
expectedURL := "/instruments/futures?product-code%5B%5D=123&symbol%5B%5D=AAPL"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.Write([]byte(`{"data": {"items": [{"symbol": "AAPL", "product-code": "123"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
params := &FuturesQueryParams{
Symbol: []string{"AAPL"},
ProductCode: []string{"123"},
}
resp, err := api.QueryFutures(params)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].Symbol)
}
if resp.Data.Items[0].ProductCode != "123" {
t.Errorf("expected %s, got %s", "123", resp.Data.Items[0].ProductCode)
}
}
func TestGetFuture(t *testing.T) {
expectedURL := "/instruments/futures/AAPL"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.Write([]byte(`{"data": {"symbol": "AAPL"}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetFuture("AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Data.Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Symbol)
}
}
func TestGetFutureOptionProduct(t *testing.T) {
expectedURL := "/instruments/future-option-products/exchange/AAPL"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.Write([]byte(`{"context": "test", "data": {"root-symbol": "AAPL"}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetFutureOptionProduct("exchange", "AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if resp.Data.RootSymbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.RootSymbol)
}
}
func TestListFutureOptions(t *testing.T) {
expectedURL := "/instruments/future-options?symbol%5B%5D=AAPL"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.Write([]byte(`{"context": "test", "data": {"items": [{"symbol": "AAPL", "instrument-type": "future-option"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.ListFutureOptions(&FutureOptionsQueryParams{Symbol: []string{"AAPL"}})
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].Symbol)
}
}
func TestListFutureOptionChainsNested(t *testing.T) {
expectedURL := "/futures-option-chains/AAPL/nested"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.Write([]byte(`{"context": "test", "data": {"futures": [{"symbol": "AAPL"}], "option-chains": [{"underlying-symbol": "AAPL"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.ListFutureOptionChainsNested("AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Futures) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Futures))
}
if resp.Data.Futures[0].Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Futures[0].Symbol)
}
if len(resp.Data.OptionChains) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.OptionChains))
}
if resp.Data.OptionChains[0].UnderlyingSymbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.OptionChains[0].UnderlyingSymbol)
}
}
func TestListFutureOptionChainsDetailed(t *testing.T) {
expectedURL := "/futures-option-chains/AAPL"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.Write([]byte(`{"context": "test", "data": {"items": [{"symbol": "AAPL", "underlying-symbol": "AAPL"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.ListFutureOptionChainsDetailed("AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].Symbol)
}
if resp.Data.Items[0].UnderlyingSymbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].UnderlyingSymbol)
}
}
func TestGetFutureOption(t *testing.T) {
expectedURL := "/instruments/future-options/AAPL"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.Write([]byte(`{"context": "test", "data": {"symbol": "AAPL", "instrument-type": "future-option"}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetFutureOption("AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if resp.Data.Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Symbol)
}
}
func TestListFutureOptionProducts(t *testing.T) {
expectedURL := "/instruments/future-option-products"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.Write([]byte(`{"context": "test", "data": {"items": [{"root-symbol": "AAPL", "instrument-type": "future-option"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.ListFutureOptionProducts()
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].RootSymbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].RootSymbol)
}
}
func TestGetFutureOptionProductError(t *testing.T) {
expectedURL := "/instruments/future-option-products/exchange/AAPL"
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() != expectedURL {
t.Errorf("expected URL to be %s, got %s", expectedURL, req.URL.String())
}
rw.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
_, err := api.GetFutureOptionProduct("exchange", "AAPL")
if err == nil {
t.Errorf("expected an error, got nil")
}
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/optionsvamp/tastytrade
go 1.22.3

264
option.go Normal file
View File

@@ -0,0 +1,264 @@
package tastytrade
import (
"encoding/json"
"fmt"
"net/url"
)
type OptionDataDetailed struct {
HaltedAt string `json:"halted-at"`
InstrumentType string `json:"instrument-type"`
RootSymbol string `json:"root-symbol"`
Active bool `json:"active"`
IsClosingOnly bool `json:"is-closing-only"`
UnderlyingSymbol string `json:"underlying-symbol"`
DaysToExpiration int `json:"days-to-expiration"`
ExpirationDate string `json:"expiration-date"`
ExpiresAt string `json:"expires-at"`
ListedMarket string `json:"listed-market"`
StrikePrice string `json:"strike-price"`
OldSecurityNumber string `json:"old-security-number"`
OptionType string `json:"option-type"`
MarketTimeInstrumentCollection string `json:"market-time-instrument-collection"`
Symbol string `json:"symbol"`
StreamerSymbol string `json:"streamer-symbol"`
ExpirationType string `json:"expiration-type"`
SharesPerContract int `json:"shares-per-contract"`
StopsTradingAt string `json:"stops-trading-at"`
ExerciseStyle string `json:"exercise-style"`
SettlementType string `json:"settlement-type"`
OptionChainType string `json:"option-chain-type"`
}
type OptionChainsDetailedResponse struct {
Context string `json:"context"`
Data struct {
Items []OptionDataDetailed `json:"items"`
} `json:"data"`
}
type StrikeNested struct {
StrikePrice string `json:"strike-price"`
Call string `json:"call"`
CallStreamerSymbol string `json:"call-streamer-symbol"`
Put string `json:"put"`
PutStreamerSymbol string `json:"put-streamer-symbol"`
}
type ExpirationNested struct {
ExpirationType string `json:"expiration-type"`
ExpirationDate string `json:"expiration-date"`
DaysToExpiration int `json:"days-to-expiration"`
SettlementType string `json:"settlement-type"`
Strikes []StrikeNested `json:"strikes"`
}
type OptionChainItemNested struct {
UnderlyingSymbol string `json:"underlying-symbol"`
RootSymbol string `json:"root-symbol"`
OptionChainType string `json:"option-chain-type"`
SharesPerContract int `json:"shares-per-contract"`
Expirations []ExpirationNested `json:"expirations"`
}
type OptionChainsNestedResponse struct {
Data struct {
Items []OptionChainItemNested `json:"items"`
} `json:"data"`
Context string `json:"context"`
}
type DeliverableCompact struct {
ID int `json:"id"`
RootSymbol string `json:"root-symbol"`
DeliverableType string `json:"deliverable-type"`
Description string `json:"description"`
Amount string `json:"amount"`
Symbol string `json:"symbol"`
InstrumentType string `json:"instrument-type"`
Percent string `json:"percent"`
}
type OptionChainItemCompact struct {
UnderlyingSymbol string `json:"underlying-symbol"`
RootSymbol string `json:"root-symbol"`
OptionChainType string `json:"option-chain-type"`
SettlementType string `json:"settlement-type"`
SharesPerContract int `json:"shares-per-contract"`
ExpirationType string `json:"expiration-type"`
Deliverables []DeliverableCompact `json:"deliverables"`
Symbols []string `json:"symbols"`
}
type OptionChainsCompactResponse struct {
Data struct {
Items []OptionChainItemCompact `json:"items"`
} `json:"data"`
Context string `json:"context"`
}
type EquityOptionData struct {
Symbol string `json:"symbol"`
InstrumentType string `json:"instrument-type"`
Active bool `json:"active"`
StrikePrice string `json:"strike-price"`
RootSymbol string `json:"root-symbol"`
UnderlyingSymbol string `json:"underlying-symbol"`
ExpirationDate string `json:"expiration-date"`
ExerciseStyle string `json:"exercise-style"`
SharesPerContract int `json:"shares-per-contract"`
OptionType string `json:"option-type"`
OptionChainType string `json:"option-chain-type"`
ExpirationType string `json:"expiration-type"`
SettlementType string `json:"settlement-type"`
StopsTradingAt string `json:"stops-trading-at"`
MarketTimeInstrumentCollection string `json:"market-time-instrument-collection"`
DaysToExpiration int `json:"days-to-expiration"`
ExpiresAt string `json:"expires-at"`
IsClosingOnly bool `json:"is-closing-only"`
StreamerSymbol string `json:"streamer-symbol"`
}
type EquityOptionsListResponse struct {
Context string `json:"context"`
Data struct {
Items []EquityOptionData `json:"items"`
} `json:"data"`
}
type EquityOptionsQueryParams struct {
Symbol []string `json:"symbol"`
Active *bool `json:"active"`
WithExpired *bool `json:"with-expired"`
}
type EquityOptionResponse struct {
Data EquityOptionData `json:"data"`
Context string `json:"context"`
}
// ListOptionsChainsDetailed retrieves option chain data for a specific symbol
func (api *TastytradeAPI) ListOptionsChainsDetailed(symbol string) (OptionChainsDetailedResponse, error) {
urlVal := fmt.Sprintf("%s/option-chains/%s", api.host, symbol)
data, err := api.fetchData(urlVal)
if err != nil {
return OptionChainsDetailedResponse{}, err
}
var response OptionChainsDetailedResponse
jsonData, err := json.Marshal(data)
if err != nil {
return OptionChainsDetailedResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return OptionChainsDetailedResponse{}, err
}
return response, nil
}
// ListOptionChainsNested retrieves nested option chain data for a specific symbol
func (api *TastytradeAPI) ListOptionChainsNested(symbol string) (OptionChainsNestedResponse, error) {
urlVal := fmt.Sprintf("%s/option-chains/%s/nested", api.host, symbol)
data, err := api.fetchData(urlVal)
if err != nil {
return OptionChainsNestedResponse{}, err
}
var response OptionChainsNestedResponse
jsonData, err := json.Marshal(data)
if err != nil {
return OptionChainsNestedResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return OptionChainsNestedResponse{}, err
}
return response, nil
}
// GetOptionChainsCompact retrieves compact option chain data for a specific symbol
func (api *TastytradeAPI) GetOptionChainsCompact(symbol string) (OptionChainsCompactResponse, error) {
urlVal := fmt.Sprintf("%s/option-chains/%s/compact", api.host, symbol)
data, err := api.fetchData(urlVal)
if err != nil {
return OptionChainsCompactResponse{}, err
}
var response OptionChainsCompactResponse
jsonData, err := json.Marshal(data)
if err != nil {
return OptionChainsCompactResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return OptionChainsCompactResponse{}, err
}
return response, nil
}
// GetEquityOptions retrieves a list of equity options
func (api *TastytradeAPI) GetEquityOptions(params *EquityOptionsQueryParams) (EquityOptionsListResponse, error) {
urlVal := fmt.Sprintf("%s/instruments/equity-options", api.host)
if params != nil {
queryParams := url.Values{}
for _, symbol := range params.Symbol {
queryParams.Add("symbol[]", symbol)
}
if params.Active != nil {
queryParams.Add("active", fmt.Sprintf("%t", *params.Active))
}
if params.WithExpired != nil {
queryParams.Add("with-expired", fmt.Sprintf("%t", *params.WithExpired))
}
urlVal = fmt.Sprintf("%s?%s", urlVal, queryParams.Encode())
}
data, err := api.fetchData(urlVal)
if err != nil {
return EquityOptionsListResponse{}, err
}
var response EquityOptionsListResponse
jsonData, err := json.Marshal(data)
if err != nil {
return EquityOptionsListResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return EquityOptionsListResponse{}, err
}
return response, nil
}
// GetEquityOption retrieves data for a specific equity option symbol
func (api *TastytradeAPI) GetEquityOption(symbol string) (EquityOptionResponse, error) {
url := fmt.Sprintf("%s/instruments/equity-options/%s", api.host, url.PathEscape(symbol))
data, err := api.fetchData(url)
if err != nil {
return EquityOptionResponse{}, err
}
var response EquityOptionResponse
jsonData, err := json.Marshal(data)
if err != nil {
return EquityOptionResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return EquityOptionResponse{}, err
}
return response, nil
}

137
option_test.go Normal file
View File

@@ -0,0 +1,137 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestListOptionsChainsDetailed(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"items": [{"symbol": "AAPL", "underlying-symbol": "AAPL"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.ListOptionsChainsDetailed("AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].Symbol)
}
if resp.Data.Items[0].UnderlyingSymbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].UnderlyingSymbol)
}
}
func TestListOptionChainsNested(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"items": [{"underlying-symbol": "AAPL", "root-symbol": "AAPL"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.ListOptionChainsNested("AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].UnderlyingSymbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].UnderlyingSymbol)
}
}
func TestGetOptionChainsCompact(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"items": [{"underlying-symbol": "AAPL", "root-symbol": "AAPL"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetOptionChainsCompact("AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].UnderlyingSymbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].UnderlyingSymbol)
}
}
func TestGetEquityOptions(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"items": [{"symbol": "AAPL", "instrument-type": "equity-option"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetEquityOptions(&EquityOptionsQueryParams{Symbol: []string{"AAPL"}})
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].Symbol)
}
}
func TestGetEquityOption(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"symbol": "AAPL", "instrument-type": "equity-option"}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetEquityOption("AAPL")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if resp.Data.Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Symbol)
}
}

61
positions.go Normal file
View File

@@ -0,0 +1,61 @@
package tastytrade
import (
"encoding/json"
"fmt"
)
type Position struct {
AccountNumber string `json:"account-number"`
Symbol string `json:"symbol"`
InstrumentType string `json:"instrument-type"`
UnderlyingSymbol string `json:"underlying-symbol"`
Quantity string `json:"quantity"`
QuantityDirection string `json:"quantity-direction"`
ClosePrice string `json:"close-price"`
AverageOpenPrice string `json:"average-open-price"`
AverageYearlyMarketClosePrice string `json:"average-yearly-market-close-price"`
AverageDailyMarketClosePrice string `json:"average-daily-market-close-price"`
Multiplier int `json:"multiplier"`
CostEffect string `json:"cost-effect"`
IsSuppressed bool `json:"is-suppressed"`
IsFrozen bool `json:"is-frozen"`
RestrictedQuantity string `json:"restricted-quantity"`
RealizedDayGain string `json:"realized-day-gain"`
RealizedDayGainEffect string `json:"realized-day-gain-effect"`
RealizedDayGainDate string `json:"realized-day-gain-date"`
RealizedToday string `json:"realized-today"`
RealizedTodayEffect string `json:"realized-today-effect"`
RealizedTodayDate string `json:"realized-today-date"`
CreatedAt string `json:"created-at"`
UpdatedAt string `json:"updated-at"`
}
type PositionsResponse struct {
Context string `json:"context"`
Data struct {
Items []Position `json:"items"`
} `json:"data"`
}
// GetPositions retrieves positions for a specific account
func (api *TastytradeAPI) GetPositions(accountNumber string) (PositionsResponse, error) {
url := fmt.Sprintf("%s/accounts/%s/positions", api.host, accountNumber)
data, err := api.fetchData(url)
if err != nil {
return PositionsResponse{}, err
}
var response PositionsResponse
jsonData, err := json.Marshal(data)
if err != nil {
return PositionsResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return PositionsResponse{}, err
}
return response, nil
}

37
positions_test.go Normal file
View File

@@ -0,0 +1,37 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetPositions(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"items": [{"symbol": "AAPL", "quantity": "100"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetPositions("123456")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].Symbol)
}
if resp.Data.Items[0].Quantity != "100" {
t.Errorf("expected %s, got %s", "100", resp.Data.Items[0].Quantity)
}
}

56
sessions.go Normal file
View File

@@ -0,0 +1,56 @@
package tastytrade
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type User struct {
Email string `json:"email"`
Username string `json:"username"`
ExternalID string `json:"external-id"`
IsConfirmed bool `json:"is-confirmed"`
}
type AuthData struct {
User User `json:"user"`
SessionToken string `json:"session-token"`
}
type AuthResponse struct {
Data AuthData `json:"data"`
Context string `json:"context"`
}
// Authenticate authenticates the client with the Tastytrade API
func (api *TastytradeAPI) Authenticate(username, password string) error {
authURL := fmt.Sprintf("%s/sessions", api.host)
authData := map[string]string{
"login": username,
"password": password,
}
authBody, err := json.Marshal(authData)
if err != nil {
return err
}
resp, err := api.httpClient.Post(authURL, "application/json", bytes.NewReader(authBody))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("authentication failed: status code %d", resp.StatusCode)
}
authResponse := AuthResponse{}
if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil {
return err
}
api.authToken = authResponse.Data.SessionToken
return nil
}

25
sessions_test.go Normal file
View File

@@ -0,0 +1,25 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestAuthenticate(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"user": {"email": "test@example.com", "username": "testuser", "external-id": "123", "is-confirmed": true}, "session-token": "testtoken"}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
err := api.Authenticate("testuser", "testpassword")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if api.authToken != "testtoken" {
t.Errorf("expected %s, got %s", "testtoken", api.authToken)
}
}

67
status.go Normal file
View File

@@ -0,0 +1,67 @@
package tastytrade
import (
"encoding/json"
"fmt"
)
type TradingStatusData struct {
AccountNumber string `json:"account-number"`
DayTradeCount int `json:"day-trade-count"`
EquitiesMarginCalculationType string `json:"equities-margin-calculation-type"`
FeeScheduleName string `json:"fee-schedule-name"`
FuturesMarginRateMultiplier string `json:"futures-margin-rate-multiplier"`
HasIntradayEquitiesMargin bool `json:"has-intraday-equities-margin"`
ID int `json:"id"`
IsAggregatedAtClearing bool `json:"is-aggregated-at-clearing"`
IsClosed bool `json:"is-closed"`
IsClosingOnly bool `json:"is-closing-only"`
IsCryptocurrencyClosingOnly bool `json:"is-cryptocurrency-closing-only"`
IsCryptocurrencyEnabled bool `json:"is-cryptocurrency-enabled"`
IsFrozen bool `json:"is-frozen"`
IsFullEquityMarginRequired bool `json:"is-full-equity-margin-required"`
IsFuturesClosingOnly bool `json:"is-futures-closing-only"`
IsFuturesIntraDayEnabled bool `json:"is-futures-intra-day-enabled"`
IsFuturesEnabled bool `json:"is-futures-enabled"`
IsInDayTradeEquityMaintenanceCall bool `json:"is-in-day-trade-equity-maintenance-call"`
IsInMarginCall bool `json:"is-in-margin-call"`
IsPatternDayTrader bool `json:"is-pattern-day-trader"`
IsRiskReducingOnly bool `json:"is-risk-reducing-only"`
IsSmallNotionalFuturesIntraDayEnabled bool `json:"is-small-notional-futures-intra-day-enabled"`
IsRollTheDayForwardEnabled bool `json:"is-roll-the-day-forward-enabled"`
AreFarOtmNetOptionsRestricted bool `json:"are-far-otm-net-options-restricted"`
OptionsLevel string `json:"options-level"`
ShortCallsEnabled bool `json:"short-calls-enabled"`
SmallNotionalFuturesMarginRateMultiplier string `json:"small-notional-futures-margin-rate-multiplier"`
IsEquityOfferingEnabled bool `json:"is-equity-offering-enabled"`
IsEquityOfferingClosingOnly bool `json:"is-equity-offering-closing-only"`
EnhancedFraudSafeguardsEnabledAt string `json:"enhanced-fraud-safeguards-enabled-at"`
UpdatedAt string `json:"updated-at"`
}
type TradingStatusResponse struct {
Context string `json:"context"`
Data TradingStatusData `json:"data"`
}
// GetAccountTradingStatus retrieves trading status for a specific account
func (api *TastytradeAPI) GetAccountTradingStatus(accountNumber string) (TradingStatusResponse, error) {
url := fmt.Sprintf("%s/accounts/%s/trading-status", api.host, accountNumber)
data, err := api.fetchData(url)
if err != nil {
return TradingStatusResponse{}, err
}
var response TradingStatusResponse
jsonData, err := json.Marshal(data)
if err != nil {
return TradingStatusResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return TradingStatusResponse{}, err
}
return response, nil
}

33
status_test.go Normal file
View File

@@ -0,0 +1,33 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetAccountTradingStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"account-number": "123456", "day-trade-count": 1}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetAccountTradingStatus("123456")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if resp.Data.AccountNumber != "123456" {
t.Errorf("expected %s, got %s", "123456", resp.Data.AccountNumber)
}
if resp.Data.DayTradeCount != 1 {
t.Errorf("expected %d, got %d", 1, resp.Data.DayTradeCount)
}
}

163
transactions.go Normal file
View File

@@ -0,0 +1,163 @@
package tastytrade
import (
"encoding/json"
"fmt"
"net/url"
)
type Transaction struct {
ID int `json:"id"`
AccountNumber string `json:"account-number"`
Symbol string `json:"symbol"`
InstrumentType string `json:"instrument-type"`
UnderlyingSymbol string `json:"underlying-symbol"`
TransactionType string `json:"transaction-type"`
TransactionSubType string `json:"transaction-sub-type"`
Description string `json:"description"`
Action string `json:"action"`
Quantity string `json:"quantity"`
Price string `json:"price"`
ExecutedAt string `json:"executed-at"`
TransactionDate string `json:"transaction-date"`
Value string `json:"value"`
ValueEffect string `json:"value-effect"`
NetValue string `json:"net-value"`
NetValueEffect string `json:"net-value-effect"`
IsEstimatedFee bool `json:"is-estimated-fee"`
}
type TransactionResponse struct {
Data Transaction `json:"data"`
Context string `json:"context"`
}
type Pagination struct {
PerPage int `json:"per-page"`
PageOffset int `json:"page-offset"`
ItemOffset int `json:"item-offset"`
TotalItems int `json:"total-items"`
TotalPages int `json:"total-pages"`
CurrentItemCount int `json:"current-item-count"`
PreviousLink *string `json:"previous-link"`
NextLink *string `json:"next-link"`
PagingLinkTemplate *string `json:"paging-link-template"`
}
type TransactionsResponse struct {
Data struct {
Items []Transaction `json:"items"`
} `json:"data"`
APIVersion string `json:"api-version"`
Context string `json:"context"`
Pagination Pagination `json:"pagination"`
}
type TransactionQueryParams struct {
Sort string `json:"sort"`
Type string `json:"type"`
SubType []string `json:"sub-type"`
Types []string `json:"types"`
StartDate string `json:"start-date"`
EndDate string `json:"end-date"`
InstrumentType string `json:"instrument-type"`
Symbol string `json:"symbol"`
UnderlyingSymbol string `json:"underlying-symbol"`
Action string `json:"action"`
PartitionKey string `json:"partition-key"`
FuturesSymbol string `json:"futures-symbol"`
StartAt string `json:"start-at"`
EndAt string `json:"end-at"`
}
// GetTransactions retrieves transactions for a specific account
func (api *TastytradeAPI) GetTransactions(accountNumber string, params *TransactionQueryParams) (TransactionsResponse, error) {
urlVal := fmt.Sprintf("%s/accounts/%s/transactions", api.host, accountNumber)
if params != nil {
queryParams := url.Values{}
if params.Sort != "" {
queryParams.Add("sort", params.Sort)
}
if params.Type != "" {
queryParams.Add("type", params.Type)
}
for _, subType := range params.SubType {
queryParams.Add("sub-type", subType)
}
for _, types := range params.Types {
queryParams.Add("types", types)
}
if params.StartDate != "" {
queryParams.Add("start-date", params.StartDate)
}
if params.EndDate != "" {
queryParams.Add("end-date", params.EndDate)
}
if params.InstrumentType != "" {
queryParams.Add("instrument-type", params.InstrumentType)
}
if params.Symbol != "" {
queryParams.Add("symbol", params.Symbol)
}
if params.UnderlyingSymbol != "" {
queryParams.Add("underlying-symbol", params.UnderlyingSymbol)
}
if params.Action != "" {
queryParams.Add("action", params.Action)
}
if params.PartitionKey != "" {
queryParams.Add("partition-key", params.PartitionKey)
}
if params.FuturesSymbol != "" {
queryParams.Add("futures-symbol", params.FuturesSymbol)
}
if params.StartAt != "" {
queryParams.Add("start-at", params.StartAt)
}
if params.EndAt != "" {
queryParams.Add("end-at", params.EndAt)
}
urlVal = fmt.Sprintf("%s?%s", urlVal, queryParams.Encode())
}
data, err := api.fetchData(urlVal)
if err != nil {
return TransactionsResponse{}, err
}
var response TransactionsResponse
jsonData, err := json.Marshal(data)
if err != nil {
return TransactionsResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return TransactionsResponse{}, err
}
return response, nil
}
// GetTransaction retrieves a specific transaction for a specific account
func (api *TastytradeAPI) GetTransaction(accountNumber string, transactionID string) (TransactionResponse, error) {
urlVal := fmt.Sprintf("%s/accounts/%s/transactions/%s", api.host, accountNumber, transactionID)
data, err := api.fetchData(urlVal)
if err != nil {
return TransactionResponse{}, err
}
var response TransactionResponse
jsonData, err := json.Marshal(data)
if err != nil {
return TransactionResponse{}, err
}
err = json.Unmarshal(jsonData, &response)
if err != nil {
return TransactionResponse{}, err
}
return response, nil
}

117
transactions_test.go Normal file
View File

@@ -0,0 +1,117 @@
package tastytrade
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetTransactions(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"items": [{"id": 1, "account-number": "123456", "symbol": "AAPL", "transaction-type": "buy"}]}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetTransactions("123456", nil)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if len(resp.Data.Items) != 1 {
t.Errorf("expected %d, got %d", 1, len(resp.Data.Items))
}
if resp.Data.Items[0].ID != 1 {
t.Errorf("expected %d, got %d", 1, resp.Data.Items[0].ID)
}
if resp.Data.Items[0].AccountNumber != "123456" {
t.Errorf("expected %s, got %s", "123456", resp.Data.Items[0].AccountNumber)
}
if resp.Data.Items[0].Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Items[0].Symbol)
}
if resp.Data.Items[0].TransactionType != "buy" {
t.Errorf("expected %s, got %s", "buy", resp.Data.Items[0].TransactionType)
}
}
func TestGetTransaction(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"context": "test", "data": {"id": 1, "account-number": "123", "symbol": "AAPL", "instrument-type": "equity-option"}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetTransaction("123", "1")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Context != "test" {
t.Errorf("expected %s, got %s", "test", resp.Context)
}
if resp.Data.Symbol != "AAPL" {
t.Errorf("expected %s, got %s", "AAPL", resp.Data.Symbol)
}
}
func TestGetTransactionsPagination(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`{"api-version": "1.0", "context": "test", "data": {"items": [{"id": 1, "account-number": "123", "symbol": "AAPL", "instrument-type": "equity-option"}]}, "pagination": {"per-page": 1, "total-items": 10}}`))
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
resp, err := api.GetTransactions("123", &TransactionQueryParams{Symbol: "AAPL"})
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.Pagination.PerPage != 1 {
t.Errorf("expected %d, got %d", 1, resp.Pagination.PerPage)
}
if resp.Pagination.TotalItems != 10 {
t.Errorf("expected %d, got %d", 10, resp.Pagination.TotalItems)
}
}
func TestGetTransactionsError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
_, err := api.GetTransactions("123", &TransactionQueryParams{Symbol: "AAPL"})
if err == nil {
t.Errorf("expected an error, got nil")
}
}
func TestGetTransactionError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
api := NewTastytradeAPI(server.URL)
_, err := api.GetTransaction("123", "1")
if err == nil {
t.Errorf("expected an error, got nil")
}
}