<|start_filename|>user.go<|end_filename|>
package tdameritrade

import (
	"context"
	"fmt"
	"strings"
)

type Preferences struct {
	ExpressTrading                   bool   `json:"expressTrading"`
	DirectOptionsRouting             bool   `json:"directOptionsRouting"`
	DirectEquityRouting              bool   `json:"directEquityRouting"`
	DefaultEquityOrderLegInstruction string `json:"defaultEquityOrderLegInstruction"`
	DefaultEquityOrderType           string `json:"defaultEquityOrderType"`
	DefaultEquityOrderPriceLinkType  string `json:"defaultEquityOrderPriceLinkType"`
	DefaultEquityOrderDuration       string `json:"defaultEquityOrderDuration"`
	DefaultEquityOrderMarketSession  string `json:"defaultEquityOrderMarketSession"`
	DefaultEquityQuantity            int    `json:"defaultEquityQuantity"`
	MutualFundTaxLotMethod           string `json:"mutualFundTaxLotMethod"`
	OptionTaxLotMethod               string `json:"optionTaxLotMethod"`
	EquityTaxLotMethod               string `json:"equityTaxLotMethod"`
	DefaultAdvancedToolLaunch        string `json:"defaultAdvancedToolLaunch"`
	AuthTokenTimeout                 string `json:"authTokenTimeout"`
}

type StreamerSubscriptionKeys struct {
	Keys []KeyEntry `json:"keys"`
}

type KeyEntry struct {
	Key string `json:"key"`
}

type UserPrincipal struct {
	AuthToken                string                   `json:"authToken"`
	UserID                   string                   `json:"userId"`
	UserCdDomainID           string                   `json:"userCdDomainId"`
	PrimaryAccountID         string                   `json:"primaryAccountId"`
	LastLoginTime            string                   `json:"lastLoginTime"`
	TokenExpirationTime      string                   `json:"tokenExpirationTime"`
	LoginTime                string                   `json:"loginTime"`
	AccessLevel              string                   `json:"accessLevel"`
	StalePassword            bool                     `json:"stalePassword"`
	StreamerInfo             StreamerInfo             `json:"streamerInfo"`
	ProfessionalStatus       string                   `json:"professionalStatus"`
	Quotes                   QuoteDelays              `json:"quotes"`
	StreamerSubscriptionKeys StreamerSubscriptionKeys `json:"streamerSubscriptionKeys"`
	Accounts                 []UserAccountInfo        `json:"accounts"`
}

type UserAccountInfo struct {
	AccountID         string         `json:"accountId"`
	Description       string         `json:"description"`
	DisplayName       string         `json:"displayName"`
	AccountCdDomainID string         `json:"accountCdDomainId"`
	Company           string         `json:"company"`
	Segment           string         `json:"segment"`
	SurrogateIds      string         `json:"surrogateIds"`
	Preferences       Preferences    `json:"preferences"`
	ACL               string         `json:"acl"`
	Authorizations    Authorizations `json:"authorizations"`
}

type Authorizations struct {
	Apex               bool   `json:"apex"`
	LevelTwoQuotes     bool   `json:"levelTwoQuotes"`
	StockTrading       bool   `json:"stockTrading"`
	MarginTrading      bool   `json:"marginTrading"`
	StreamingNews      bool   `json:"streamingNews"`
	OptionTradingLevel string `json:"optionTradingLevel"`
	StreamerAccess     bool   `json:"streamerAccess"`
	AdvancedMargin     bool   `json:"advancedMargin"`
	ScottradeAccount   bool   `json:"scottradeAccount"`
}

type StreamerInfo struct {
	StreamerBinaryURL string `json:"streamerBinaryUrl"`
	StreamerSocketURL string `json:"streamerSocketUrl"`
	Token             string `json:"token"`
	TokenTimestamp    string `json:"tokenTimestamp"`
	UserGroup         string `json:"userGroup"`
	AccessLevel       string `json:"accessLevel"`
	ACL               string `json:"acl"`
	AppID             string `json:"appId"`
}

type QuoteDelays struct {
	IsNyseDelayed   bool `json:"isNyseDelayed"`
	IsNasdaqDelayed bool `json:"isNasdaqDelayed"`
	IsOpraDelayed   bool `json:"isOpraDelayed"`
	IsAmexDelayed   bool `json:"isAmexDelayed"`
	IsCmeDelayed    bool `json:"isCmeDelayed"`
	IsIceDelayed    bool `json:"isIceDelayed"`
	IsForexDelayed  bool `json:"isForexDelayed"`
}

// UserService exposes operations on a user's preferences.
// See https://developer.tdameritrade.com/user-principal/apis.
type UserService struct {
	client *Client
}

// GetPreferences returns Preferences for a specific account.
// See https://developer.tdameritrade.com/user-principal/apis/get/accounts/%7BaccountId%7D/preferences-0
func (s *UserService) GetPreferences(ctx context.Context, accountID string) (*Preferences, *Response, error) {
	u := fmt.Sprintf("accounts/%s/preferences", accountID)
	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	preferences := new(Preferences)
	resp, err := s.client.Do(ctx, req, preferences)
	if err != nil {
		return nil, resp, err
	}

	return preferences, resp, err
}

// GetStreamerSubscriptionKeys returns Subscription Keys for provided accounts or default accounts.
// See https://developer.tdameritrade.com/user-principal/apis/get/userprincipals/streamersubscriptionkeys-0
func (s *UserService) GetStreamerSubscriptionKeys(ctx context.Context, accountIDs ...string) (*StreamerSubscriptionKeys, *Response, error) {
	u := fmt.Sprintf("userprincipals/streamersubscriptionkeys?accountIds=%s", strings.Join(accountIDs, ","))
	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	streamerSubscriptionKeys := new(StreamerSubscriptionKeys)
	resp, err := s.client.Do(ctx, req, streamerSubscriptionKeys)
	if err != nil {
		return nil, resp, err
	}

	return streamerSubscriptionKeys, resp, err
}

// GetUserPrincipals returns User Principal details.
// Valid values for `fields` are "streamerSubscriptionKeys", "streamerConnectionInfo", "preferences" and  "surrogateIds"
// See https://developer.tdameritrade.com/user-principal/apis/get/userprincipals-0
func (s *UserService) GetUserPrincipals(ctx context.Context, fields ...string) (*UserPrincipal, *Response, error) {
	u := "userprincipals"
	if len(fields) > 0 {
		u = fmt.Sprintf("%s?fields=%s", u, strings.Join(fields, ","))
	}

	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	userPrincipal := new(UserPrincipal)
	resp, err := s.client.Do(ctx, req, userPrincipal)
	if err != nil {
		return nil, resp, err
	}

	return userPrincipal, resp, err
}

// UpdatePreferences updates Preferences for a specific account.
// Please note that the directOptionsRouting and directEquityRouting values cannot be modified via this operation, even though they are in the request body.
// See https://developer.tdameritrade.com/user-principal/apis/put/accounts/%7BaccountId%7D/preferences-0
func (s *UserService) UpdatePreferences(ctx context.Context, accountID string, newPreferences *Preferences) (*Response, error) {
	if newPreferences == nil {
		return nil, fmt.Errorf("newPreferences is nil")
	}

	u := fmt.Sprintf("accounts/%s/preferences", accountID)
	req, err := s.client.NewRequest("PUT", u, newPreferences)
	if err != nil {
		return nil, err
	}

	return s.client.Do(ctx, req, nil)
}


<|start_filename|>quotes.go<|end_filename|>
package tdameritrade

import (
	"context"
	"fmt"
)

// QuotesService handles communication with the marketdata related methods of
// the TDAmeritrade API.
//
// TDAmeritrade API docs: https://developer.tdameritrade.com/quotes/apis
type QuotesService struct {
	client *Client
}

type Quotes map[string]*Quote

type Quote struct {
	AssetType                          string  `json:"assetType"`
	AssetMainType                      string  `json:"assetMainType"`
	Cusip                              string  `json:"cusip"`
	AssetSubType                       string  `json:"assetSubType"`
	Symbol                             string  `json:"symbol"`
	Description                        string  `json:"description"`
	BidPrice                           float64 `json:"bidPrice"`
	BidSize                            float64 `json:"bidSize"`
	BidID                              string  `json:"bidId"`
	AskPrice                           float64 `json:"askPrice"`
	AskSize                            float64 `json:"askSize"`
	AskID                              string  `json:"askId"`
	LastPrice                          float64 `json:"lastPrice"`
	LastSize                           float64 `json:"lastSize"`
	LastID                             string  `json:"lastId"`
	OpenPrice                          float64 `json:"openPrice"`
	HighPrice                          float64 `json:"highPrice"`
	LowPrice                           float64 `json:"lowPrice"`
	BidTick                            string  `json:"bidTick"`
	ClosePrice                         float64 `json:"closePrice"`
	NetChange                          float64 `json:"netChange"`
	TotalVolume                        float64 `json:"totalVolume"`
	QuoteTimeInLong                    int64   `json:"quoteTimeInLong"`
	TradeTimeInLong                    int64   `json:"tradeTimeInLong"`
	Mark                               float64 `json:"mark"`
	Exchange                           string  `json:"exchange"`
	ExchangeName                       string  `json:"exchangeName"`
	Marginable                         bool    `json:"marginable"`
	Shortable                          bool    `json:"shortable"`
	Volatility                         float64 `json:"volatility"`
	Digits                             int     `json:"digits"`
	Five2WkHigh                        float64 `json:"52WkHigh"`
	Five2WkLow                         float64 `json:"52WkLow"`
	NAV                                float64 `json:"nAV"`
	PeRatio                            float64 `json:"peRatio"`
	DivAmount                          float64 `json:"divAmount"`
	DivYield                           float64 `json:"divYield"`
	DivDate                            string  `json:"divDate"`
	SecurityStatus                     string  `json:"securityStatus"`
	RegularMarketLastPrice             float64 `json:"regularMarketLastPrice"`
	RegularMarketLastSize              int     `json:"regularMarketLastSize"`
	RegularMarketNetChange             float64 `json:"regularMarketNetChange"`
	RegularMarketTradeTimeInLong       int64   `json:"regularMarketTradeTimeInLong"`
	NetPercentChangeInDouble           float64 `json:"netPercentChangeInDouble"`
	MarkChangeInDouble                 float64 `json:"markChangeInDouble"`
	MarkPercentChangeInDouble          float64 `json:"markPercentChangeInDouble"`
	RegularMarketPercentChangeInDouble float64 `json:"regularMarketPercentChangeInDouble"`
	Delayed                            bool    `json:"delayed"`
}


func (s *QuotesService) GetQuotes(ctx context.Context, symbols string) (*Quotes, *Response, error) {
	u := fmt.Sprintf("marketdata/quotes")
	if symbols == "" {
		return nil, nil, fmt.Errorf("no symbols present")
	}
	u = fmt.Sprintf("%s?symbol=%s", u, symbols)

	req, err := s.client.NewRequest("GET", u, nil)

	if err != nil {
		return nil, nil, err
	}

	quotes := new(Quotes)

	resp, err := s.client.Do(ctx, req, quotes)
	if err != nil {
		return nil, resp, err
	}

	return quotes, resp, nil
}



<|start_filename|>streaming.go<|end_filename|>
package tdameritrade

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

type Command struct {
	Requests []StreamRequest `json:"requests"`
}

type StreamRequest struct {
	Service    string       `json:"service"`
	Requestid  string       `json:"requestid"`
	Command    string       `json:"command"`
	Account    string       `json:"account"`
	Source     string       `json:"source"`
	Parameters StreamParams `json:"parameters"`
}

type StreamParams struct {
	Keys   string `json:"keys"`
	Fields string `json:"fields"`
}

// NewStreamAuthCommand creates a StreamAuthCommand from a TD Ameritrade UserPrincipal.
// It validates the account ID against the accounts in the UserPrincipal to avoid creating invalid messages unneccesarily.
func NewStreamAuthCommand(userPrincipal *UserPrincipal, accountID string) (*StreamAuthCommand, error) {
	// findAccount ensures that a user has passed us an account they control to avoid wasting TD Ameritrade's time.
	account, err := findAccount(userPrincipal, accountID)
	if err != nil {
		return nil, err
	}

	timestamp, err := time.Parse("2006-01-02T15:04:05-0700", userPrincipal.StreamerInfo.TokenTimestamp)
	if err != nil {
		return nil, err
	}
	credentials := url.Values{}
	credentials.Add("userid", account.AccountID)
	credentials.Add("token", userPrincipal.StreamerInfo.Token)
	credentials.Add("company", account.Company)
	credentials.Add("segment", account.Segment)
	credentials.Add("cddomain", account.AccountCdDomainID)
	credentials.Add("usergroup", userPrincipal.StreamerInfo.UserGroup)
	credentials.Add("accesslevel", userPrincipal.StreamerInfo.AccessLevel)
	credentials.Add("authorized", "Y")
	credentials.Add("timestamp", fmt.Sprintf("%d", ConvertToEpoch(timestamp)))
	credentials.Add("appid", userPrincipal.StreamerInfo.AppID)
	credentials.Add("acl", userPrincipal.StreamerInfo.ACL)

	// TD Ameritrade expects this JSON command from clients.
	authCmd := StreamAuthCommand{
		Requests: []StreamAuthRequest{
			{
				Service:   "ADMIN",
				Command:   "LOGIN",
				Requestid: 0,
				Account:   account.AccountID,
				Source:    userPrincipal.StreamerInfo.AppID,
				Parameters: StreamAuthParams{
					Credential: credentials.Encode(),
					Token:      userPrincipal.StreamerInfo.Token,
					Version:    "1.0",
				},
			},
		},
	}
	return &authCmd, nil
}

type StreamAuthCommand struct {
	Requests []StreamAuthRequest `json:"requests"`
}

type StreamAuthRequest struct {
	Service    string           `json:"service"`
	Command    string           `json:"command"`
	Requestid  int              `json:"requestid"`
	Account    string           `json:"account"`
	Source     string           `json:"source"`
	Parameters StreamAuthParams `json:"parameters"`
}

type StreamAuthResponse struct {
	Response []StreamAuthResponseBody `json:"response"`
}

type StreamAuthResponseBody struct {
	Service   string                    `json:"service"`
	Requestid string                    `json:"requestid"`
	Command   string                    `json:"command"`
	Timestamp int64                     `json:"timestamp"`
	Content   StreamAuthResponseContent `json:"content"`
}

type StreamAuthResponseContent struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

type StreamAuthParams struct {
	Credential string `json:"credential"`
	Token      string `json:"token"`
	Version    string `json:"version"`
}

// StreamingClient provides real time updates from TD Ameritrade's streaming API.
// See https://developer.tdameritrade.com/content/streaming-data for more information.
type StreamingClient struct {
	connection *websocket.Conn
	messages   chan []byte
	errors     chan error
	mu         sync.Mutex
}

// Close closes the underlying websocket connection.
func (s *StreamingClient) Close() error {
	close(s.messages)
	close(s.errors)
	return s.connection.Close()
}

// SendText sends a byte payload to TD Ameritrade's websocket.
// TD Ameritrade commands are JSON encoded payloads.
// You should generally be using SendCommand to send commands to TD Ameritrade.
func (s *StreamingClient) SendText(payload []byte) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.connection.WriteMessage(websocket.TextMessage, payload)
}

// ReceiveText returns read-only channels with the raw byte responses from TD Ameritrade and errors generated while streaming.
// Callers should select over both of these channels to avoid blocking one.
// Callers are able to handle errors how thes see fit.
// All errors will be from Gorilla's websocket library and implement the net.Error interface.
func (s *StreamingClient) ReceiveText() (<-chan []byte, <-chan error) {
	return s.messages, s.errors
}

// SendCommand serializes and sends a Command struct to TD Ameritrade.
// It is a wrapper around SendText.
func (s *StreamingClient) SendCommand(command Command) error {
	commandBytes, err := json.Marshal(command)
	if err != nil {
		return err
	}

	return s.SendText(commandBytes)
}

func (s *StreamingClient) Authenticate(authCmd *StreamAuthCommand) error {
	jsonCmd, err := json.Marshal(authCmd)
	if err != nil {
		return err
	}

	// Authenticate with TD's websocket using the StreamAuthCommand
	return s.SendText(jsonCmd)
}

// NewUnauthenticatedStreamingClient returns an unauthenticated streaming client that has a connection to the TD Ameritrade websocket.
// You can get an authenticated streaming client with NewAuthenticatedStreamingClient.
// To authenticate manually, send a JSON serialized StreamAuthCommand message with the StreamingClient's Authenticate method.
// You'll need to Close a streaming client to free up the underlying resources.
func NewUnauthenticatedStreamingClient(userPrincipal *UserPrincipal) (*StreamingClient, error) {
	streamURL := url.URL{
		Scheme: "wss",
		Host:   userPrincipal.StreamerInfo.StreamerSocketURL,
		Path:   "/ws",
	}

	conn, _, err := websocket.DefaultDialer.Dial(streamURL.String(), nil)
	if err != nil {
		return nil, err
	}

	streamingClient := &StreamingClient{
		connection: conn,
		messages:   make(chan []byte),
		errors:     make(chan error),
	}

	// Pass messages and errors down the respective channels.
	go func() {
		for {
			_, message, err := streamingClient.connection.ReadMessage()
			if err != nil {
				streamingClient.errors <- err
				return
			}

			streamingClient.messages <- message
		}
	}()

	return streamingClient, nil
}

// NewAuthenticatedStreamingClient returns a client that will pull live updates for a TD Ameritrade account.
// It sends an initial authentication message to TD Ameritrade and waits for a response before returning.
// Use NewUnauthenticatedStreamingClient if you want to handle authentication yourself.
// You'll need to Close a StreamingClient to free up the underlying resources.
func NewAuthenticatedStreamingClient(userPrincipal *UserPrincipal, accountID string) (*StreamingClient, error) {
	streamingClient, err := NewUnauthenticatedStreamingClient(userPrincipal)
	if err != nil {
		return nil, err
	}

	authCmd, err := NewStreamAuthCommand(userPrincipal, accountID)
	if err != nil {
		return nil, err
	}

	err = streamingClient.Authenticate(authCmd)
	if err != nil {
		return nil, err
	}

	// Wait on a response from TD Ameritrade.
	select {
	case message := <-streamingClient.messages:
		var authResponse StreamAuthResponse
		err = json.Unmarshal(message, &authResponse)
		if err != nil {
			return nil, err
		}

		// Response with a code 0 means authentication succeeded.
		if authResponse.Response[0].Content.Code != 0 {
			return nil, errors.New(authResponse.Response[0].Content.Msg)
		}

		return streamingClient, nil

	case err := <-streamingClient.errors:
		return nil, err
	}

}

func findAccount(userPrincipal *UserPrincipal, accountID string) (*UserAccountInfo, error) {
	for _, acc := range userPrincipal.Accounts {
		if acc.AccountID == accountID {
			return &acc, nil
		}
	}

	return nil, fmt.Errorf("account '%s' not found", accountID)
}


<|start_filename|>tdameritrade.go<|end_filename|>
// Package tdameritrade is a wrapper around the TD Ameritrade REST API.
// It contains helpers for building apps that interact with TD Ameritrade's API on behalf of traders.
package tdameritrade


<|start_filename|>hours.go<|end_filename|>
package tdameritrade

import (
	"context"
	"fmt"
	"time"
)

// MarketHoursService handles communication with the marketdata related methods of
// the TDAmeritrade API.
//
// TDAmeritrade API docs: https://developer.tdameritrade.com/market-hours/apis
type MarketHoursService struct {
	client *Client
}

type MarketHours map[string]map[string]*Hours

type Period struct {
	Start string `json:"start"`
	End   string `json:"end"`
}

type SessionHours struct {
	PreMarket     []Period `json:"preMarket"`
	RegularMarket []Period `json:"regularMarket"`
	PostMarket    []Period `json:"postMarket"`
}

type Hours struct {
	Category     string       `json:"category"`
	Date         string       `json:"date"`
	Exchange     string       `json:"exchange"`
	IsOpen       bool         `json:"isOpen"`
	MarketType   string       `json:"marketType"`
	Product      string       `json:"product"`
	ProductName  string       `json:"productName"`
	SessionHours SessionHours `json:"sessionHours"`
}

func (s *MarketHoursService) GetMarketHoursMulti(ctx context.Context, markets string, date time.Time) (*MarketHours, *Response, error) {
	u := fmt.Sprintf("marketdata/hours")
	if markets == "" {
		return nil, nil, fmt.Errorf("no markets present")
	}
	u = fmt.Sprintf("%s?markets=%s", u, markets)
	if !date.IsZero() {
		u = fmt.Sprintf("%s&date=%s", u, date.Format("2006-01-02"))
	}

	req, err := s.client.NewRequest("GET", u, nil)

	if err != nil {
		return nil, nil, err
	}

	hours := new(MarketHours)

	resp, err := s.client.Do(ctx, req, hours)
	if err != nil {
		return nil, resp, err
	}

	return hours, resp, nil
}

func (s *MarketHoursService) GetMarketHours(ctx context.Context, market string, date time.Time) (*MarketHours, *Response, error) {
	u := fmt.Sprintf("marketdata/%s/hours", market)

	if !date.IsZero() {
		u = fmt.Sprintf("%s?date=%s", u, date.Format("2006-01-02"))
	}

	req, err := s.client.NewRequest("GET", u, nil)

	if err != nil {
		return nil, nil, err
	}

	hours := new(MarketHours)

	resp, err := s.client.Do(ctx, req, hours)
	if err != nil {
		return nil, resp, err
	}

	return hours, resp, nil
}


<|start_filename|>instruments.go<|end_filename|>
package tdameritrade

import (
	"context"
	"fmt"
)

// InstrumentService handles communication with the marketdata related methods of
// the TDAmeritrade API.
//
// TDAmeritrade API docs: https://developer.tdameritrade.com/instruments/apis
type InstrumentService struct {
	client *Client
}

type Instruments map[string]*InstrumentInfo

type InstrumentInfo struct {
	Cusip       string `json:"cusip,omitempty"`
	Symbol      string `json:"symbol"`
	Description string `json:"description,omitempty"`
	Type        string `json:"assetType"` //"'NOT_APPLICABLE' or 'OPEN_END_NON_TAXABLE' or 'OPEN_END_TAXABLE' or 'NO_LOAD_NON_TAXABLE' or 'NO_LOAD_TAXABLE'"
	Exchange    string `json:"exchange"`
}

func (s *InstrumentService) GetInstrument(ctx context.Context, cusip string) (*Instruments, *Response, error) {
	if cusip == "" {
		return nil, nil, fmt.Errorf("no cusip present")
	}
	u := fmt.Sprintf("instruments/%s", cusip)
	req, err := s.client.NewRequest("GET", u, nil)

	if err != nil {
		return nil, nil, err
	}

	instruments := new(Instruments)

	resp, err := s.client.Do(ctx, req, instruments)
	if err != nil {
		return nil, resp, err
	}
	return nil, nil, nil
}

func (s *InstrumentService) SearchInstruments(ctx context.Context, symbol, projection string) (*Instruments, *Response, error) {
	u := fmt.Sprintf("instruments")
	if symbol == "" {
		return nil, nil, fmt.Errorf("no symbol present")
	}
	if projection == "" {
		projection = "symbol-search"
	}
	u = fmt.Sprintf("%s?symbol=%s&projection=%s", u, symbol, projection)

	req, err := s.client.NewRequest("GET", u, nil)

	if err != nil {
		return nil, nil, err
	}

	instruments := new(Instruments)

	resp, err := s.client.Do(ctx, req, instruments)
	if err != nil {
		return nil, resp, err
	}

	return instruments, resp, nil
}


<|start_filename|>movers.go<|end_filename|>
package tdameritrade

import (
	"context"
	"fmt"
	"github.com/google/go-querystring/query"
)

const (
	defaultChangeType    = "percent"
	defaultDirectionType = "up"
)

var (
	ChangeTypes    = []string{"value", "percent"}
	DirectionTypes = []string{"up", "down"}
)

type MoverService struct {
	client *Client
}

type MoverOptions struct {
	Direction  string `url:"direction"`
	ChangeType string `url:"change"`
}

type Mover struct {
	Change      float64 `json:"change"`
	Description string  `json:"description"`
	Direction   string  `json:"direction"`
	Last        float64 `json:"last"`
	TotalVolume float64 `json:"totalVolume"`
	Symbol      string  `json:"symbol"`
}

func (s *MoverService) Mover(ctx context.Context, symbol string, opts *MoverOptions) (*[]Mover, *Response, error) {
	u := fmt.Sprintf("marketdata/%s/movers", symbol)
	if opts != nil {
		if err := opts.validate(); err != nil {
			return nil, nil, err
		}

	} else {
		opts = &MoverOptions{
			Direction:  defaultDirectionType,
			ChangeType: defaultChangeType,
		}
	}
	q, err := query.Values(opts)
	if err != nil {
		return nil, nil, err
	}
	u = fmt.Sprintf("%s?%s", u, q.Encode())

	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	movers := new([]Mover)
	resp, err := s.client.Do(ctx, req, movers)
	if err != nil {
		return nil, resp, err
	}

	return movers, resp, nil
}

func (opts *MoverOptions) validate() error {
	if opts.ChangeType != "" {
		if !contains(opts.ChangeType, ChangeTypes) {
			return fmt.Errorf("invalid changeType, must have the value of one of the following %v", ChangeTypes)
		}
	} else {
		opts.ChangeType = defaultChangeType
	}

	if opts.Direction != "" {
		if !contains(opts.Direction, DirectionTypes) {
			return fmt.Errorf("invalid direction, must have the value of one of the following %v", DirectionTypes)
		}
	} else {
		opts.ChangeType = defaultDirectionType
	}

	return nil
}


<|start_filename|>txnHistory.go<|end_filename|>
package tdameritrade

import (
	"context"
	"fmt"

	"github.com/google/go-querystring/query"
)

// Transactions is a slice of transactions
type Transactions []*Transaction

// Transaction represents a single transaction
type Transaction struct {
	Type                          string          `json:"type"`
	ClearingReferenceNumber       string          `json:"clearingReferenceNumber"`
	SubAccount                    string          `json:"subAccount"`
	SettlementDate                string          `json:"settlementDate"`
	OrderID                       string          `json:"orderId"`
	SMA                           float64         `json:"sma"`
	RequirementReallocationAmount float64         `json:"requirementReallocationAmount"`
	DayTradeBuyingPowerEffect     float64         `json:"dayTradeBuyingPowerEffect"`
	NetAmount                     float64         `json:"netAmount"`
	TransactionDate               string          `json:"transactionDate"`
	OrderDate                     string          `json:"orderDate"`
	TransactionSubType            string          `json:"transactionSubType"`
	TransactionID                 int64           `json:"transactionId"`
	CashBalanceEffectFlag         bool            `json:"cashBalanceEffectFlag"`
	Description                   string          `json:"description"`
	ACHStatus                     string          `json:"achStatus"`
	AccruedInterest               float64         `json:"accruedInterest"`
	Fees                          TransactionFees `json:"fees"`
	TransactionItem               TransactionItem `json:"transactionItem"`
}

// TransactionFees contains fees related to the transaction
type TransactionFees struct {
	AdditionalFee float64 `json:"additionalFee"`
	CdscFee       float64 `json:"cdscFee"`
	Commission    float64 `json:"commission"`
	OptRegFee     float64 `json:"optRegFee"`
	OtherCharges  float64 `json:"otherCharges"`
	RFee          float64 `json:"rFee"`
	RegFee        float64 `json:"regFee"`
	SecFee        float64 `json:"secFee"`
}

// TransactionItem is an item within a transaction response
type TransactionItem struct {
	AccountID            int32                 `json:"accountId"`
	Amount               float64               `json:"amount"`
	Price                float64               `json:"price"`
	Cost                 float64               `json:"cost"`
	ParentOrderKey       int32                 `json:"parentOrderKey"`
	ParentChildIndicator string                `json:"parentChildIndicator"`
	Instruction          string                `json:"instruction"`
	PositionEffect       string                `json:"positionEffect"`
	Instrument           TransactionInstrument `json:"instrument"`
}

// TransactionInstrument is the instrumnet traded within a transaction
type TransactionInstrument struct {
	Symbol               string  `json:"symbol"`
	UnderlyingSymbol     string  `json:"underlyingSymbol"`
	OptionExpirationDate string  `json:"optionExpirationDate"`
	OptionStrikePrice    float64 `json:"optionStrikePrice"`
	PutCall              string  `json:"putCall"`
	CUSIP                string  `json:"cusip"`
	Description          string  `json:"description"`
	AssetType            string  `json:"assetType"`
	BondMaturityDate     string  `json:"bondMaturityDate"`
	BondInterestRate     float64 `json:"bondInterestRate"`
}

// TransactionHistoryOptions is parsed and translated to query options in the https request
type TransactionHistoryOptions struct {
	Type   string `url:"type,omitempty"`
	Symbol string `url:"symbol,omitempty"`
	// ISO8601 format, day granularity yyyy-MM-dd
	StartDate string `url:"startDate,omitempty"`
	// ISO8601 format, day granularity yyyy-MM-dd
	EndDate string `url:"endDate,omitempty"`
}

// TransactionHistoryService handles communication with the transaction history related methods of
// the TDAmeritrade API.
//
// TDAmeritrade API docs: https://developer.tdameritrade.com/transaction-history/apis
type TransactionHistoryService struct {
	client *Client
}

// GetTransaction gets a specific transaction by account
// TDAmeritrade API Docs: https://developer.tdameritrade.com/transaction-history/apis/get/accounts/%7BaccountId%7D/transactions/%7BtransactionId%7D-0
func (s *TransactionHistoryService) GetTransaction(ctx context.Context, accountID string, transactionID string) (*Transaction, *Response, error) {
	u := fmt.Sprintf("accounts/%s/transactions/%s", accountID, transactionID)

	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	txn := new(Transaction)
	resp, err := s.client.Do(ctx, req, txn)
	if err != nil {
		return nil, resp, err
	}
	return txn, resp, nil
}

// GetTransactions gets all transaction by account
// TDAmeritrade API Docs: https://developer.tdameritrade.com/transaction-history/apis/get/accounts/%7BaccountId%7D/transactions-0
func (s *TransactionHistoryService) GetTransactions(ctx context.Context, accountID string, opts *TransactionHistoryOptions) (*Transactions, *Response, error) {
	u := fmt.Sprintf("accounts/%s/transactions", accountID)
	if opts != nil {
		q, err := query.Values(opts)
		if err != nil {
			return nil, nil, err
		}
		u = fmt.Sprintf("%s?%s", u, q.Encode())
	}

	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	txns := new(Transactions)
	resp, err := s.client.Do(ctx, req, txns)
	if err != nil {
		return nil, resp, err
	}
	return txns, resp, nil
}
