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
|
||||
|
||||

|
||||
|
||||
## 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