diff --git a/.travis.yml b/.travis.yml index 4358b86..861dad8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ cache: env: global: - - FINANCE_MOCK_VERSION=0.0.4 + - FINANCE_MOCK_VERSION=0.0.5 go: - "1.9" diff --git a/README.md b/README.md index af44cda..70fb35f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Status | Description | Source [x] | ETF quote(s) | Yahoo finance [x] | Mutual fund quote(s) | Yahoo finance [x] | Historical quotes | Yahoo finance +[x] | Options straddles | Yahoo finance [ ] | Options chains | Yahoo finance [ ] | Symbols list | BATS diff --git a/history/client.go b/chart/client.go similarity index 56% rename from history/client.go rename to chart/client.go index e3b72cc..b0dd511 100644 --- a/history/client.go +++ b/chart/client.go @@ -1,14 +1,16 @@ -package history +package chart import ( "context" finance "github.com/piquette/finance-go" + "github.com/piquette/finance-go/datetime" form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" "github.com/shopspring/decimal" ) -// Client is used to invoke quote APIs. +// Client is used to invoke chart APIs. type Client struct { B finance.Backend } @@ -23,10 +25,10 @@ type Params struct { finance.Params `form:"-"` // Accessible fields. - Symbol string `form:"-"` - Start *Datetime `form:"-"` - End *Datetime `form:"-"` - Interval Interval `form:"-"` + Symbol string `form:"-"` + Start *datetime.Datetime `form:"-"` + End *datetime.Datetime `form:"-"` + Interval datetime.Interval `form:"-"` IncludeExt bool `form:"includePrePost"` @@ -34,61 +36,59 @@ type Params struct { interval string `form:"interval"` start int `form:"period1"` end int `form:"period2"` - region string `form:"region"` - domain string `form:"corsDomain"` } -// Chart is a structure containing results +// Iter is a structure containing results // and related metadata for a // yfin chart request. -type Chart struct { - *finance.Iter +type Iter struct { + *iter.Iter } // Bar returns the next Bar // visited by a call to Next. -func (ch *Chart) Bar() *finance.ChartBar { - return ch.Current().(*finance.ChartBar) +func (i *Iter) Bar() *finance.ChartBar { + return i.Current().(*finance.ChartBar) } // Meta returns the chart metadata // related to a chart response. -func (ch *Chart) Meta() *finance.ChartMeta { - return ch.Meta() +func (i *Iter) Meta() *finance.ChartMeta { + return i.Iter.Meta().(*finance.ChartMeta) } // Get returns a historical chart. // and requires a params // struct as an argument. -func Get(params *Params) *Chart { +func Get(params *Params) *Iter { return getC().Get(params) } // Get returns a historical chart. -func (c Client) Get(params *Params) *Chart { - - if params.Context == nil { - ctx := context.TODO() - params.Context = &ctx - } +func (c Client) Get(params *Params) *Iter { // Construct request from params input. // TODO: validate symbol.. if params == nil || len(params.Symbol) == 0 { - return &Chart{finance.GetErrIter(finance.CreateArgumentError())} + return &Iter{iter.NewE(finance.CreateArgumentError())} } - // Start and End times. + if params.Context == nil { + ctx := context.TODO() + params.Context = &ctx + } + + // Start and End times params.start = -1 params.end = -1 if params.Start != nil { - params.start = params.Start.ToUnix() + params.start = params.Start.Unix() } if params.End != nil { - params.end = params.End.ToUnix() + params.end = params.End.Unix() } if params.start > params.end { - return &Chart{finance.GetErrIter(finance.CreateChartTimeError())} + return &Iter{iter.NewE(finance.CreateChartTimeError())} } // Parse interval. @@ -96,14 +96,14 @@ func (c Client) Get(params *Params) *Chart { params.interval = string(params.Interval) } - // Set meta data. - params.domain = "com.finance.yahoo" - params.region = "US" - + // Build request. body := &form.Values{} form.AppendTo(body, params) + // Set request meta data. + body.Set("region", "US") + body.Set("corsDomain", "com.finance.yahoo") - return &Chart{finance.GetChartIter(body, func(b *form.Values) (m interface{}, bars []interface{}, err error) { + return &Iter{iter.New(body, func(b *form.Values) (m interface{}, bars []interface{}, err error) { resp := response{} err = c.B.Call("v8/finance/chart/"+params.Symbol, body, params.Context, &resp) @@ -116,22 +116,22 @@ func (c Client) Get(params *Params) *Chart { return } - chartResp := resp.Inner.Result[0] - if chartResp == nil || chartResp.Indicators == nil { + result := resp.Inner.Results[0] + if result == nil || result.Indicators == nil { err = finance.CreateRemoteErrorS("no results in chart response") return } - barQuotes := chartResp.Indicators.Quote + barQuotes := result.Indicators.Quote if barQuotes == nil || barQuotes[0] == nil { err = finance.CreateRemoteErrorS("no results in chart response") return } - adjCloses := chartResp.Indicators.Adjclose + adjCloses := result.Indicators.Adjclose // Process chart response // and chart meta data. - for i, t := range chartResp.Timestamp { + for i, t := range result.Timestamp { b := &finance.ChartBar{ Timestamp: t, @@ -149,14 +149,32 @@ func (c Client) Get(params *Params) *Chart { bars = append(bars, b) } - return chartResp.Meta, bars, nil + return result.Meta, bars, nil })} } // response is a yfin chart response. type response struct { Inner struct { - Result []*finance.ChartResponse `json:"result"` - Error *finance.YfinError `json:"error"` + Results []*result `json:"result"` + Error *finance.YfinError `json:"error"` } `json:"chart"` } + +// result is an umbrella object for chart results. +type result struct { + Meta finance.ChartMeta `json:"meta"` + Timestamp []int `json:"timestamp"` + Indicators *struct { + Quote []*struct { + Open []float64 `json:"open"` + Low []float64 `json:"low"` + High []float64 `json:"high"` + Close []float64 `json:"close"` + Volume []int `json:"volume"` + } `json:"quote"` + Adjclose []*struct { + Adjclose []float64 `json:"adjclose"` + } `json:"adjclose"` + } `json:"indicators"` +} diff --git a/history/client_test.go b/chart/client_test.go similarity index 83% rename from history/client_test.go rename to chart/client_test.go index 7a97b9f..f7c2805 100644 --- a/history/client_test.go +++ b/chart/client_test.go @@ -1,4 +1,4 @@ -package history +package chart import ( "testing" @@ -9,26 +9,26 @@ import ( func TestGetEquityChart(t *testing.T) { p := &Params{Symbol: tests.TestEquitySymbol} - chart := Get(p) - assert.True(t, chart.Next()) + iter := Get(p) + assert.True(t, iter.Next()) } func TestGetETFChart(t *testing.T) { p := &Params{Symbol: tests.TestETFSymbol} - chart := Get(p) - assert.True(t, chart.Next()) + iter := Get(p) + assert.True(t, iter.Next()) } func TestGetFutureChart(t *testing.T) { p := &Params{Symbol: tests.TestFutureSymbol} - chart := Get(p) - assert.True(t, chart.Next()) + iter := Get(p) + assert.True(t, iter.Next()) } func TestGetIndexChart(t *testing.T) { p := &Params{Symbol: tests.TestIndexSymbol} - chart := Get(p) - assert.True(t, chart.Next()) + iter := Get(p) + assert.True(t, iter.Next()) } func TestGetOptionChart(t *testing.T) { diff --git a/crypto/client.go b/crypto/client.go index 92e958f..8a5f4cc 100644 --- a/crypto/client.go +++ b/crypto/client.go @@ -6,6 +6,7 @@ import ( finance "github.com/piquette/finance-go" form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" ) // Client is used to invoke quote APIs. @@ -30,7 +31,7 @@ type Params struct { // The embedded Iter carries methods with it; // see its documentation for details. type Iter struct { - *finance.Iter + *iter.Iter } // CryptoPair returns the most recent CryptoPair @@ -72,14 +73,14 @@ func (c Client) ListP(params *Params) *Iter { // Validate input. // TODO: validate symbols.. if params == nil || len(params.Symbols) == 0 { - return &Iter{finance.GetErrIter(finance.CreateArgumentError())} + return &Iter{iter.NewE(finance.CreateArgumentError())} } params.sym = strings.Join(params.Symbols, ",") body := &form.Values{} form.AppendTo(body, params) - return &Iter{finance.GetIter(body, func(b *form.Values) ([]interface{}, error) { + return &Iter{iter.New(body, func(b *form.Values) (interface{}, []interface{}, error) { resp := response{} err := c.B.Call("/v7/finance/quote", body, params.Context, &resp) @@ -95,7 +96,7 @@ func (c Client) ListP(params *Params) *Iter { err = finance.CreateRemoteError(resp.Inner.Error) } - return ret, err + return nil, ret, err })} } diff --git a/history/time.go b/datetime/datetime.go similarity index 81% rename from history/time.go rename to datetime/datetime.go index 7e7b703..d5eab82 100644 --- a/history/time.go +++ b/datetime/datetime.go @@ -1,4 +1,4 @@ -package history +package datetime import ( "time" @@ -58,18 +58,24 @@ type Datetime struct { t *time.Time } -// NewDatetime creates a new instance of Datetime. -func NewDatetime(t time.Time) *Datetime { +// New creates a new instance of Datetime from a go time struct. +func New(t *time.Time) *Datetime { year, month, day := t.Date() return &Datetime{ Month: int(month), Day: day, Year: year, - t: &t, + t: t, } } -// Time returns a time object from a datetime. +// FromUnix returns a new instance of Datetime from a unix timestamp. +func FromUnix(timestamp int) *Datetime { + t := time.Unix(int64(timestamp), 0) + return New(&t) +} + +// Time returns a go time struct from a datetime. func (d *Datetime) Time() *time.Time { if d.t != nil { return d.t @@ -78,22 +84,14 @@ func (d *Datetime) Time() *time.Time { return d.Time() } -// ToUnix converts a Datetime struct to -// a valid unix timestamp. -func (d *Datetime) ToUnix() int { +// Unix returns a valid unix timestamp from Datetime fields. +func (d *Datetime) Unix() int { if d.t != nil { return int(d.t.Unix()) } d.calculateTime() - return d.ToUnix() -} - -// NewDatetimeU converts a valid unix timestamp -// to a datetime object. -func NewDatetimeU(timestamp int) *Datetime { - t := time.Unix(int64(timestamp), 0) - return NewDatetime(t) + return d.Unix() } func (d *Datetime) calculateTime() { diff --git a/equity/client.go b/equity/client.go index 12b9b74..d44e54c 100644 --- a/equity/client.go +++ b/equity/client.go @@ -6,6 +6,7 @@ import ( finance "github.com/piquette/finance-go" form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" ) // Client is used to invoke quote APIs. @@ -30,7 +31,7 @@ type Params struct { // The embedded Iter carries methods with it; // see its documentation for details. type Iter struct { - *finance.Iter + *iter.Iter } // Equity returns the most recent Equity @@ -72,14 +73,14 @@ func (c Client) ListP(params *Params) *Iter { // Validate input. // TODO: validate symbols.. if params == nil || len(params.Symbols) == 0 { - return &Iter{finance.GetErrIter(finance.CreateArgumentError())} + return &Iter{iter.NewE(finance.CreateArgumentError())} } params.sym = strings.Join(params.Symbols, ",") body := &form.Values{} form.AppendTo(body, params) - return &Iter{finance.GetIter(body, func(b *form.Values) ([]interface{}, error) { + return &Iter{iter.New(body, func(b *form.Values) (interface{}, []interface{}, error) { resp := response{} err := c.B.Call("/v7/finance/quote", body, params.Context, &resp) @@ -95,7 +96,7 @@ func (c Client) ListP(params *Params) *Iter { err = finance.CreateRemoteError(resp.Inner.Error) } - return ret, err + return nil, ret, err })} } diff --git a/etf/client.go b/etf/client.go index 020aa9c..8ed20f9 100644 --- a/etf/client.go +++ b/etf/client.go @@ -6,6 +6,7 @@ import ( finance "github.com/piquette/finance-go" form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" ) // Client is used to invoke quote APIs. @@ -30,7 +31,7 @@ type Params struct { // The embedded Iter carries methods with it; // see its documentation for details. type Iter struct { - *finance.Iter + *iter.Iter } // ETF returns the most recent ETF @@ -72,14 +73,14 @@ func (c Client) ListP(params *Params) *Iter { // Validate input. // TODO: validate symbols.. if params == nil || len(params.Symbols) == 0 { - return &Iter{finance.GetErrIter(finance.CreateArgumentError())} + return &Iter{iter.NewE(finance.CreateArgumentError())} } params.sym = strings.Join(params.Symbols, ",") body := &form.Values{} form.AppendTo(body, params) - return &Iter{finance.GetIter(body, func(b *form.Values) ([]interface{}, error) { + return &Iter{iter.New(body, func(b *form.Values) (interface{}, []interface{}, error) { resp := response{} err := c.B.Call("/v7/finance/quote", body, params.Context, &resp) @@ -95,7 +96,7 @@ func (c Client) ListP(params *Params) *Iter { err = finance.CreateRemoteError(resp.Inner.Error) } - return ret, err + return nil, ret, err })} } diff --git a/examples/main.go b/examples/main.go index de38eaa..4e7a1fb 100644 --- a/examples/main.go +++ b/examples/main.go @@ -3,38 +3,47 @@ package main import ( "fmt" - "github.com/piquette/finance-go/history" - "github.com/piquette/finance-go/quote" + "github.com/piquette/finance-go/options" ) // This file lists several usage examples of this library // and can be used to verify behavior. func main() { - // Basic quote example. - // -------------------- - q, err := quote.Get("SPY") - if err != nil { - fmt.Println(err) - } else { - fmt.Println(q) - } - - // Basic chart example. - // -------------------- - params := &history.Params{ - Symbol: "TWTR", - Interval: history.OneHour, - } - chart := history.Get(params) + iter := options.GetStraddle("TWTR") - for chart.Next() { - b := chart.Bar() - fmt.Println(history.NewDatetimeU(b.Timestamp)) + fmt.Println(iter.Meta()) + for iter.Next() { + fmt.Println(iter.Straddle().Strike) } - if chart.Err() != nil { - fmt.Println(chart.Err()) + if iter.Err() != nil { + fmt.Println(iter.Err()) } + // Basic quote example. + // -------------------- + // q, err := quote.Get("SPY") + // if err != nil { + // fmt.Println(err) + // } else { + // fmt.Println(q) + // } + + // Basic chart example. + // -------------------- + // params := &chart.Params{ + // Symbol: "TWTR", + // Interval: datetime.OneHour, + // } + // iter := chart.Get(params) + // + // for iter.Next() { + // b := iter.Bar() + // fmt.Println(datetime.FromUnix(b.Timestamp)) + // + // } + // if iter.Err() != nil { + // fmt.Println(iter.Err()) + // } } diff --git a/forex/client.go b/forex/client.go index a455343..6abfcdd 100644 --- a/forex/client.go +++ b/forex/client.go @@ -6,6 +6,7 @@ import ( finance "github.com/piquette/finance-go" form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" ) // Client is used to invoke quote APIs. @@ -30,7 +31,7 @@ type Params struct { // The embedded Iter carries methods with it; // see its documentation for details. type Iter struct { - *finance.Iter + *iter.Iter } // ForexPair returns the most recent ForexPair @@ -72,14 +73,14 @@ func (c Client) ListP(params *Params) *Iter { // Validate input. // TODO: validate symbols.. if params == nil || len(params.Symbols) == 0 { - return &Iter{finance.GetErrIter(finance.CreateArgumentError())} + return &Iter{iter.NewE(finance.CreateArgumentError())} } params.sym = strings.Join(params.Symbols, ",") body := &form.Values{} form.AppendTo(body, params) - return &Iter{finance.GetIter(body, func(b *form.Values) ([]interface{}, error) { + return &Iter{iter.New(body, func(b *form.Values) (interface{}, []interface{}, error) { resp := response{} err := c.B.Call("/v7/finance/quote", body, params.Context, &resp) @@ -95,7 +96,7 @@ func (c Client) ListP(params *Params) *Iter { err = finance.CreateRemoteError(resp.Inner.Error) } - return ret, err + return nil, ret, err })} } diff --git a/future/client.go b/future/client.go index 359a95a..73e762f 100644 --- a/future/client.go +++ b/future/client.go @@ -1 +1,109 @@ package future + +import ( + "context" + "strings" + + finance "github.com/piquette/finance-go" + form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" +) + +// Client is used to invoke quote APIs. +type Client struct { + B finance.Backend +} + +func getC() Client { + return Client{finance.GetBackend(finance.YFinBackend)} +} + +// Params carries a context and symbols information. +type Params struct { + finance.Params `form:"-"` + // Symbols are the symbols for which a + // quote is requested. + Symbols []string `form:"-"` + sym string `form:"symbols"` +} + +// Iter is an iterator for a list of quotes. +// The embedded Iter carries methods with it; +// see its documentation for details. +type Iter struct { + *iter.Iter +} + +// Future returns the most recent future +// visited by a call to Next. +func (i *Iter) Future() *finance.Future { + return i.Current().(*finance.Future) +} + +// Get returns an Future quote that matches the parameters specified. +func Get(symbol string) (*finance.Future, error) { + i := List([]string{symbol}) + + if !i.Next() { + return nil, i.Err() + } + + return i.Future(), nil +} + +// List returns several quotes. +func List(symbols []string) *Iter { + return ListP(&Params{Symbols: symbols}) +} + +// ListP returns a quote iterator and requires a params +// struct as an argument. +func ListP(params *Params) *Iter { + return getC().ListP(params) +} + +// ListP returns a quote iterator. +func (c Client) ListP(params *Params) *Iter { + + if params.Context == nil { + ctx := context.TODO() + params.Context = &ctx + } + + // Validate input. + // TODO: validate symbols.. + if params == nil || len(params.Symbols) == 0 { + return &Iter{iter.NewE(finance.CreateArgumentError())} + } + params.sym = strings.Join(params.Symbols, ",") + + body := &form.Values{} + form.AppendTo(body, params) + + return &Iter{iter.New(body, func(b *form.Values) (interface{}, []interface{}, error) { + + resp := response{} + err := c.B.Call("/v7/finance/quote", body, params.Context, &resp) + if err != nil { + err = finance.CreateRemoteError(err) + } + + ret := make([]interface{}, len(resp.Inner.Result)) + for i, v := range resp.Inner.Result { + ret[i] = v + } + if resp.Inner.Error != nil { + err = finance.CreateRemoteError(resp.Inner.Error) + } + + return nil, ret, err + })} +} + +// response is a yfin quote response. +type response struct { + Inner struct { + Result []*finance.Future `json:"result"` + Error *finance.YfinError `json:"error"` + } `json:"quoteResponse"` +} diff --git a/future/client_test.go b/future/client_test.go index 359a95a..363da96 100644 --- a/future/client_test.go +++ b/future/client_test.go @@ -1 +1,20 @@ package future + +import ( + "testing" + + finance "github.com/piquette/finance-go" + tests "github.com/piquette/finance-go/testing" + "github.com/stretchr/testify/assert" +) + +func TestGetFuture(t *testing.T) { + tests.SetMarket(finance.MarketStateRegular) + + q, err := Get(tests.TestFutureSymbol) + + assert.Nil(t, err) + assert.NotNil(t, q) + assert.Equal(t, finance.MarketStateRegular, q.MarketState) + assert.Equal(t, tests.TestFutureSymbol, q.Symbol) +} diff --git a/index/client.go b/index/client.go index d02ed9f..1320481 100644 --- a/index/client.go +++ b/index/client.go @@ -6,6 +6,7 @@ import ( finance "github.com/piquette/finance-go" form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" ) // Client is used to invoke quote APIs. @@ -30,7 +31,7 @@ type Params struct { // The embedded Iter carries methods with it; // see its documentation for details. type Iter struct { - *finance.Iter + *iter.Iter } // Index returns the most recent Index @@ -72,14 +73,14 @@ func (c Client) ListP(params *Params) *Iter { // Validate input. // TODO: validate symbols.. if params == nil || len(params.Symbols) == 0 { - return &Iter{finance.GetErrIter(finance.CreateArgumentError())} + return &Iter{iter.NewE(finance.CreateArgumentError())} } params.sym = strings.Join(params.Symbols, ",") body := &form.Values{} form.AppendTo(body, params) - return &Iter{finance.GetIter(body, func(b *form.Values) ([]interface{}, error) { + return &Iter{iter.New(body, func(b *form.Values) (interface{}, []interface{}, error) { resp := response{} err := c.B.Call("/v7/finance/quote", body, params.Context, &resp) @@ -95,7 +96,7 @@ func (c Client) ListP(params *Params) *Iter { err = finance.CreateRemoteError(resp.Inner.Error) } - return ret, err + return nil, ret, err })} } diff --git a/iter.go b/iter/iter.go similarity index 64% rename from iter.go rename to iter/iter.go index 2667273..a7f0854 100644 --- a/iter.go +++ b/iter/iter.go @@ -1,21 +1,17 @@ -package finance +package iter import ( "github.com/piquette/finance-go/form" ) -// QuoteQuery is the function used to get a quote listing. -type QuoteQuery func(*form.Values) ([]interface{}, error) - -// ChartQuery is the function used to get a chart listing. -type ChartQuery func(*form.Values) (interface{}, []interface{}, error) +// Query is the function used to get a response listing. +type Query = func(*form.Values) (interface{}, []interface{}, error) // Iter provides a convenient interface // for iterating over the elements // returned from paginated list API calls. // Successive calls to the Next method -// will step through each item in the list, -// fetching pages of items as needed. +// will step through each item in the list. // Iterators are not thread-safe, so they should not be consumed // across multiple goroutines. type Iter struct { @@ -25,28 +21,15 @@ type Iter struct { values []interface{} } -// GetIter returns a new Iter for a given quote query and its options. -func GetIter(qs *form.Values, query QuoteQuery) *Iter { - iter := &Iter{} - - q := qs - if q == nil { - q = &form.Values{} - } - - iter.values, iter.err = query(q) - return iter -} - -// GetErrIter returns a iter wrapping an error. -func GetErrIter(e error) *Iter { +// NewE returns a iter wrapping an error. +func NewE(e error) *Iter { iter := &Iter{} iter.err = e return iter } -// GetChartIter returns a new Iter for a given chart query and its options. -func GetChartIter(qs *form.Values, query ChartQuery) *Iter { +// New returns a new instance of Iter for a given query and its options. +func New(qs *form.Values, query Query) *Iter { iter := &Iter{} q := qs diff --git a/mutualfund/client.go b/mutualfund/client.go index 56b3aa6..4f9348d 100644 --- a/mutualfund/client.go +++ b/mutualfund/client.go @@ -6,6 +6,7 @@ import ( finance "github.com/piquette/finance-go" form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" ) // Client is used to invoke quote APIs. @@ -30,7 +31,7 @@ type Params struct { // The embedded Iter carries methods with it; // see its documentation for details. type Iter struct { - *finance.Iter + *iter.Iter } // MutualFund returns the most recent MutualFund @@ -72,14 +73,14 @@ func (c Client) ListP(params *Params) *Iter { // Validate input. // TODO: validate symbols.. if params == nil || len(params.Symbols) == 0 { - return &Iter{finance.GetErrIter(finance.CreateArgumentError())} + return &Iter{iter.NewE(finance.CreateArgumentError())} } params.sym = strings.Join(params.Symbols, ",") body := &form.Values{} form.AppendTo(body, params) - return &Iter{finance.GetIter(body, func(b *form.Values) ([]interface{}, error) { + return &Iter{iter.New(body, func(b *form.Values) (interface{}, []interface{}, error) { resp := response{} err := c.B.Call("/v7/finance/quote", body, params.Context, &resp) @@ -95,7 +96,7 @@ func (c Client) ListP(params *Params) *Iter { err = finance.CreateRemoteError(resp.Inner.Error) } - return ret, err + return nil, ret, err })} } diff --git a/option/client.go b/option/client.go index 774a651..46e7534 100644 --- a/option/client.go +++ b/option/client.go @@ -1 +1,109 @@ package option + +import ( + "context" + "strings" + + finance "github.com/piquette/finance-go" + form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" +) + +// Client is used to invoke quote APIs. +type Client struct { + B finance.Backend +} + +func getC() Client { + return Client{finance.GetBackend(finance.YFinBackend)} +} + +// Params carries a context and symbols information. +type Params struct { + finance.Params `form:"-"` + // Symbols are the symbols for which a + // quote is requested. + Symbols []string `form:"-"` + sym string `form:"symbols"` +} + +// Iter is an iterator for a list of quotes. +// The embedded Iter carries methods with it; +// see its documentation for details. +type Iter struct { + *iter.Iter +} + +// Option returns the most recent option +// visited by a call to Next. +func (i *Iter) Option() *finance.Option { + return i.Current().(*finance.Option) +} + +// Get returns an option quote that matches the parameters specified. +func Get(symbol string) (*finance.Option, error) { + i := List([]string{symbol}) + + if !i.Next() { + return nil, i.Err() + } + + return i.Option(), nil +} + +// List returns several quotes. +func List(symbols []string) *Iter { + return ListP(&Params{Symbols: symbols}) +} + +// ListP returns a quote iterator and requires a params +// struct as an argument. +func ListP(params *Params) *Iter { + return getC().ListP(params) +} + +// ListP returns a quote iterator. +func (c Client) ListP(params *Params) *Iter { + + if params.Context == nil { + ctx := context.TODO() + params.Context = &ctx + } + + // Validate input. + // TODO: validate symbols.. + if params == nil || len(params.Symbols) == 0 { + return &Iter{iter.NewE(finance.CreateArgumentError())} + } + params.sym = strings.Join(params.Symbols, ",") + + body := &form.Values{} + form.AppendTo(body, params) + + return &Iter{iter.New(body, func(b *form.Values) (interface{}, []interface{}, error) { + + resp := response{} + err := c.B.Call("/v7/finance/quote", body, params.Context, &resp) + if err != nil { + err = finance.CreateRemoteError(err) + } + + ret := make([]interface{}, len(resp.Inner.Result)) + for i, v := range resp.Inner.Result { + ret[i] = v + } + if resp.Inner.Error != nil { + err = finance.CreateRemoteError(resp.Inner.Error) + } + + return nil, ret, err + })} +} + +// response is a yfin quote response. +type response struct { + Inner struct { + Result []*finance.Option `json:"result"` + Error *finance.YfinError `json:"error"` + } `json:"quoteResponse"` +} diff --git a/option/client_test.go b/option/client_test.go index 774a651..18508fb 100644 --- a/option/client_test.go +++ b/option/client_test.go @@ -1 +1,20 @@ package option + +import ( + "testing" + + finance "github.com/piquette/finance-go" + tests "github.com/piquette/finance-go/testing" + "github.com/stretchr/testify/assert" +) + +func TestGetOption(t *testing.T) { + tests.SetMarket(finance.MarketStateRegular) + + q, err := Get(tests.TestOptionSymbol) + + assert.Nil(t, err) + assert.NotNil(t, q) + assert.Equal(t, finance.MarketStateRegular, q.MarketState) + assert.Equal(t, tests.TestOptionSymbol, q.Symbol) +} diff --git a/options/client.go b/options/client.go index 0684d05..594f2e4 100644 --- a/options/client.go +++ b/options/client.go @@ -1 +1,161 @@ package options + +import ( + "context" + "encoding/json" + + finance "github.com/piquette/finance-go" + "github.com/piquette/finance-go/datetime" + form "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" +) + +// Client is used to invoke options APIs. +type Client struct { + B finance.Backend +} + +func getC() Client { + return Client{finance.GetBackend(finance.YFinBackend)} +} + +// Params carries a context and chart information. +type Params struct { + // Context access. + finance.Params `form:"-"` + + // Accessible fields. + UnderlyingSymbol string `form:"-"` + Expiration *datetime.Datetime `form:"-"` + date int `form:"date"` + straddle bool `form:"straddle"` +} + +// StraddleIter is a structure containing results +// and related metadata for a +// yfin option straddles request. +type StraddleIter struct { + *iter.Iter +} + +// Straddle returns the current straddle in the iter. +func (si *StraddleIter) Straddle() *finance.Straddle { + return si.Current().(*finance.Straddle) +} + +// Meta returns the metadata associated with the options response. +func (si *StraddleIter) Meta() *finance.OptionsMeta { + return si.Iter.Meta().(*finance.OptionsMeta) +} + +// GetStraddle returns options straddles. +// and requires a underlier symbol as an argument. +func GetStraddle(underlier string) *StraddleIter { + return GetStraddleP(&Params{UnderlyingSymbol: underlier}) +} + +// GetStraddleP returns options straddles. +// and requires a params struct as an argument. +func GetStraddleP(params *Params) *StraddleIter { + return getC().GetStraddleP(params) +} + +// GetStraddleP returns options straddles. +// and requires a params struct as an argument. +func (c Client) GetStraddleP(params *Params) *StraddleIter { + + // Construct request from params input. + // TODO: validate symbol.. + if params == nil || len(params.UnderlyingSymbol) == 0 { + return &StraddleIter{iter.NewE(finance.CreateArgumentError())} + } + + if params.Context == nil { + ctx := context.TODO() + params.Context = &ctx + } + + params.straddle = true + params.date = -1 + if params.Expiration != nil { + params.date = params.Expiration.Unix() + } + + body := &form.Values{} + form.AppendTo(body, params) + + return &StraddleIter{iter.New(body, func(b *form.Values) (meta interface{}, values []interface{}, err error) { + + resp := response{} + err = c.B.Call("v7/finance/options/"+params.UnderlyingSymbol, body, params.Context, &resp) + if err != nil { + return + } + + if resp.Inner.Error != nil { + err = resp.Inner.Error + return + } + + result := resp.Inner.Results[0] + if result == nil { + err = finance.CreateRemoteErrorS("no results in option straddle response") + return + } + + var list []straddleOptions + err = json.Unmarshal(result.Options, &list) + if err != nil || len(list) < 1 { + err = finance.CreateRemoteErrorS("no results in option straddle response") + return + } + ls := list[0] + + meta = &finance.OptionsMeta{ + UnderlyingSymbol: result.UnderlyingSymbol, + ExpirationDate: ls.ExpirationDate, + AllExpirationDates: result.ExpirationDates, + Strikes: result.Strikes, + HasMiniOptions: ls.HasMiniOptions, + Quote: result.Quote, + } + for _, straddle := range ls.Straddles { + values = append(values, straddle) + } + + return + })} +} + +// response is a yfin option response. +type response struct { + Inner struct { + Results []*result `json:"result"` + Error *finance.YfinError `json:"error"` + } `json:"optionChain"` +} + +// result is an umbrella struct for options information for a specified underlying symbol. +type result struct { + UnderlyingSymbol string `json:"underlyingSymbol"` + ExpirationDates []int `json:"expirationDates"` + Strikes []float64 `json:"strikes"` + HasMiniOptions bool `json:"hasMiniOptions"` + Quote *finance.Quote `json:"quote"` + Options json.RawMessage `json:"options"` +} + +// chain is an options chain of puts/calls. +type chainOptions struct { + ExpirationDate int `json:"expirationDate"` + HasMiniOptions bool `json:"hasMiniOptions"` + Calls []*finance.Contract `json:"calls"` + Puts []*finance.Contract `json:"puts"` +} + +// straddles is a list of option straddles. +type straddleOptions struct { + ExpirationDate int `json:"expirationDate"` + HasMiniOptions bool `json:"hasMiniOptions"` + Straddles []*finance.Straddle `json:"straddles"` +} diff --git a/options/client_test.go b/options/client_test.go index 0684d05..932cf1b 100644 --- a/options/client_test.go +++ b/options/client_test.go @@ -1 +1,17 @@ package options + +import ( + "testing" + + tests "github.com/piquette/finance-go/testing" + "github.com/stretchr/testify/assert" +) + +func TestGetStraddle(t *testing.T) { + + iter := GetStraddle(tests.TestStraddleSymbol) + success := iter.Next() + assert.True(t, success) + assert.Nil(t, iter.Err()) + assert.Equal(t, iter.Meta().UnderlyingSymbol, tests.TestStraddleSymbol) +} diff --git a/quote/client.go b/quote/client.go index 4c2b719..05005b2 100644 --- a/quote/client.go +++ b/quote/client.go @@ -6,6 +6,7 @@ import ( finance "github.com/piquette/finance-go" "github.com/piquette/finance-go/form" + "github.com/piquette/finance-go/iter" ) // Client is used to invoke quote APIs. @@ -30,7 +31,7 @@ type Params struct { // The embedded Iter carries methods with it; // see its documentation for details. type Iter struct { - *finance.Iter + *iter.Iter } // Quote returns the most recent Quote @@ -72,14 +73,14 @@ func (c Client) ListP(params *Params) *Iter { // Validate input. // TODO: validate symbols.. if params == nil || len(params.Symbols) == 0 { - return &Iter{finance.GetErrIter(finance.CreateArgumentError())} + return &Iter{iter.NewE(finance.CreateArgumentError())} } params.sym = strings.Join(params.Symbols, ",") body := &form.Values{} form.AppendTo(body, params) - return &Iter{finance.GetIter(body, func(b *form.Values) ([]interface{}, error) { + return &Iter{iter.New(body, func(b *form.Values) (interface{}, []interface{}, error) { resp := response{} err := c.B.Call("/v7/finance/quote", body, params.Context, &resp) @@ -95,7 +96,7 @@ func (c Client) ListP(params *Params) *Iter { err = finance.CreateRemoteError(resp.Inner.Error) } - return ret, err + return nil, ret, err })} } diff --git a/quote/client_test.go b/quote/client_test.go index 7b5d753..2f56573 100644 --- a/quote/client_test.go +++ b/quote/client_test.go @@ -41,7 +41,7 @@ func TestGetPreMarketQuote(t *testing.T) { assert.Equal(t, tests.TestEquitySymbol, q.Symbol) } -func TestNilParamsEquity(t *testing.T) { +func TestNilParamsQuote(t *testing.T) { iter := List(nil) @@ -49,7 +49,7 @@ func TestNilParamsEquity(t *testing.T) { assert.Equal(t, "code: api-error, detail: missing function argument", iter.Err().Error()) } -func TestGetBadEquity(t *testing.T) { +func TestGetBadQuote(t *testing.T) { tests.SetMarket(finance.MarketStateRegular) q, err := Get("TEST") diff --git a/testing/testing.go b/testing/testing.go index 21b7d73..343d503 100644 --- a/testing/testing.go +++ b/testing/testing.go @@ -20,7 +20,7 @@ const ( // added in a more recent version of finance-mock, we can show people a // better error message instead of the test suite crashing with a bunch of // confusing 404 errors or the like. - MockMinimumVersion = "0.0.4" + MockMinimumVersion = "0.0.5" TestServerAddr = "localhost" // Symbols for testing asset classes. @@ -28,10 +28,11 @@ const ( TestETFSymbol = "SPY" TestFutureSymbol = "O=F" TestIndexSymbol = "^GSPC" - TestOptionSymbol = "NYT180518C00016000" + TestOptionSymbol = "AMD180720C00003000" TestMutualFundSymbol = "INPSX" TestForexPairSymbol = "USDGBP=X" TestCryptoPairSymbol = "BTC-USD" + TestStraddleSymbol = "AMD" ) func init() { @@ -45,7 +46,7 @@ func init() { } resp, err := http.Get("http://" + TestServerAddr + ":" + port) - if err != nil { + if err != nil || resp.StatusCode != http.StatusOK { fmt.Fprintf(os.Stderr, "Couldn't reach finance-mock at `%s:%s`. Is "+ "it running? Please see README for setup instructions.\n", TestServerAddr, port) os.Exit(1) diff --git a/yfin.go b/yfin.go index a569ee5..876f891 100644 --- a/yfin.go +++ b/yfin.go @@ -130,8 +130,6 @@ type Option struct { ExpireDate int `json:"expireDate"` Strike float64 `json:"strike"` UnderlyingExchangeSymbol string `json:"underlyingExchangeSymbol"` - HeadSymbolAsString string `json:"headSymbolAsString"` - IsContractSymbol bool `json:"contractSymbol"` } // Future represents a single futures contract quote @@ -251,24 +249,6 @@ type ChartBar struct { Timestamp int } -// ChartResponse is a historical chart response. -type ChartResponse struct { - Meta ChartMeta `json:"meta"` - Timestamp []int `json:"timestamp"` - Indicators *struct { - Quote []*struct { - Open []float64 `json:"open"` - Low []float64 `json:"low"` - High []float64 `json:"high"` - Close []float64 `json:"close"` - Volume []int `json:"volume"` - } `json:"quote"` - Adjclose []*struct { - Adjclose []float64 `json:"adjclose"` - } `json:"adjclose"` - } `json:"indicators"` -} - // ChartMeta is meta data associated with a chart response. type ChartMeta struct { Currency string `json:"currency"` @@ -303,3 +283,39 @@ type ChartMeta struct { DataGranularity string `json:"dataGranularity"` ValidRanges []string `json:"validRanges"` } + +// OptionsMeta is meta data associated with an options response. +type OptionsMeta struct { + UnderlyingSymbol string + ExpirationDate int + AllExpirationDates []int + Strikes []float64 + HasMiniOptions bool + Quote *Quote +} + +// Straddle is a put/call straddle for a particular strike. +type Straddle struct { + Strike float64 `json:"strike"` + Call *Contract `json:"call,omitempty"` + Put *Contract `json:"put,omitempty"` +} + +// Contract is a struct containing a single option contract, usually part of a chain. +type Contract struct { + Symbol string `json:"contractSymbol"` + Strike float64 `json:"strike"` + Currency string `json:"currency"` + LastPrice float64 `json:"lastPrice"` + Change float64 `json:"change"` + PercentChange float64 `json:"percentChange"` + Volume int `json:"volume"` + OpenInterest int `json:"openInterest"` + Bid float64 `json:"bid"` + Ask float64 `json:"ask"` + Size string `json:"contractSize"` + Expiration int `json:"expiration"` + LastTradeDate int `json:"lastTradeDate"` + ImpliedVolatility float64 `json:"impliedVolatility"` + InTheMoney bool `json:"inTheMoney"` +}