Initial functional commit
This commit is contained in:
60
README.md
60
README.md
@@ -1,2 +1,60 @@
|
|||||||
# tastytrade
|
# Go Tastytrade Open API Wrapper
|
||||||
|
|
||||||
Go API wrapper for the Tastytrade Open API
|
Go API wrapper for the Tastytrade Open API
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
86
api.go
Normal 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
44
api_test.go
Normal 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
118
balances.go
Normal 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
43
balances_test.go
Normal 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
188
customer.go
Normal 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
77
customer_test.go
Normal 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
159
equity.go
Normal 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
73
equity_test.go
Normal 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
98
example/main.go
Normal 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
492
futures.go
Normal 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
304
futures_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
264
option.go
Normal file
264
option.go
Normal 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
137
option_test.go
Normal 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
61
positions.go
Normal 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
37
positions_test.go
Normal 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
56
sessions.go
Normal 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
25
sessions_test.go
Normal 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
67
status.go
Normal 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
33
status_test.go
Normal 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
163
transactions.go
Normal 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
117
transactions_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user