diff --git a/.gitignore b/.gitignore index ad769d22..f64283a5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ # Dependency directories vendor/ + +# IntelliJ project settings +.idea diff --git a/README.md b/README.md index fd4d4593..171fef64 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Polygon Go Client -![Coverage](https://img.shields.io/badge/Coverage-40.6%25-yellow) +![Coverage](https://img.shields.io/badge/Coverage-40.4%25-yellow) diff --git a/rest/example/options/snapshots-assets/main.go b/rest/example/options/snapshots-assets/main.go new file mode 100644 index 00000000..dfed4fe8 --- /dev/null +++ b/rest/example/options/snapshots-assets/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "log" + "os" + + polygon "github.com/polygon-io/client-go/rest" + "github.com/polygon-io/client-go/rest/models" +) + +func main() { + // Init client + c := polygon.New(os.Getenv("POLYGON_API_KEY")) + + // Set parameters + params := models.ListAssetSnapshotsParams{}. + WithTickerAnyOf("O:AAPL230512C00050000,O:META230512C00020000,O:F230512C00005000") + + // Make request + iter := c.ListAssetSnapshots(context.Background(), params) + + // do something with the result + for iter.Next() { + log.Println(iter.Item()) + } + if iter.Err() != nil { + log.Fatal(iter.Err()) + } +} diff --git a/rest/example/stocks/snapshots-assets/main.go b/rest/example/stocks/snapshots-assets/main.go new file mode 100644 index 00000000..8c91c05d --- /dev/null +++ b/rest/example/stocks/snapshots-assets/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "log" + "os" + + polygon "github.com/polygon-io/client-go/rest" + "github.com/polygon-io/client-go/rest/models" +) + +func main() { + // Init client + c := polygon.New(os.Getenv("POLYGON_API_KEY")) + + // Set parameters + params := models.ListAssetSnapshotsParams{}.WithTickerAnyOf("AAPL,META,F") + + // Make request + iter := c.ListAssetSnapshots(context.Background(), params) + + // do something with the result + for iter.Next() { + log.Println(iter.Item()) + } + if iter.Err() != nil { + log.Fatal(iter.Err()) + } +} diff --git a/rest/models/snapshot.go b/rest/models/snapshot.go index da9721e9..1b915f8e 100644 --- a/rest/models/snapshot.go +++ b/rest/models/snapshot.go @@ -1,6 +1,8 @@ package models -import "strings" +import ( + "strings" +) // GetAllTickersSnapshotParams is the set of parameters for the GetAllTickersSnapshot method. type GetAllTickersSnapshotParams struct { @@ -378,3 +380,98 @@ type OrderBookQuote struct { Price float64 `json:"p,omitempty"` ExchangeToShares map[string]float64 `json:"x,omitempty"` } + +type ListAssetSnapshotsParams struct { + TickerAnyOf *string `query:"ticker.any_of"` + Ticker *string `query:"ticker"` + + TickerLT *string `query:"ticker.lt"` + TickerLTE *string `query:"ticker.lte"` + TickerGT *string `query:"ticker.gt"` + TickerGTE *string `query:"ticker.gte"` + + Type *string `query:"type"` +} + +func (p ListAssetSnapshotsParams) WithTickerAnyOf(q string) *ListAssetSnapshotsParams { + p.TickerAnyOf = &q + return &p +} + +func (p ListAssetSnapshotsParams) WithTicker(q string) *ListAssetSnapshotsParams { + p.Ticker = &q + return &p +} + +func (p ListAssetSnapshotsParams) WithType(q string) *ListAssetSnapshotsParams { + p.Type = &q + return &p +} + +func (p ListAssetSnapshotsParams) WithTickersByComparison(c Comparator, q string) *ListAssetSnapshotsParams { + switch c { + case LT: + p.TickerLT = &q + case LTE: + p.TickerLTE = &q + case GT: + p.TickerGT = &q + case GTE: + p.TickerGTE = &q + } + return &p +} + +type ListAssetSnapshotsResponse struct { + BaseResponse + Results []SnapshotResponseModel `json:"results,omitempty"` +} + +type SnapshotResponseModel struct { + Name string `json:"name,omitempty"` + MarketStatus string `json:"market_status,omitempty"` + Ticker string `json:"ticker,omitempty"` + Type string `json:"type,omitempty"` + LastQuote SnapshotLastQuote `json:"last_quote,omitempty"` + LastTrade SnapshotLastTrade `json:"last_trade,omitempty"` + Session Session `json:"session,omitempty"` + + BreakEvenPrice float64 `json:"break_even_price,omitempty"` + Details Details `json:"details,omitempty"` + Greeks Greeks `json:"greeks,omitempty"` + ImpliedVolatility float64 `json:"implied_volatility,omitempty"` + OpenInterest float64 `json:"open_interest,omitempty"` + UnderlyingAsset UnderlyingAsset `json:"underlying_asset,omitempty"` + + Error string `json:"error"` + Message string `json:"message"` +} + +type SnapshotLastQuote struct { + Ask float64 `json:"ask,omitempty"` + AskSize float64 `json:"ask_size,omitempty"` + Bid float64 `json:"bid,omitempty"` + BidSize float64 `json:"bid_size,omitempty"` + LastUpdated int64 `json:"last_updated,omitempty"` + Midpoint float64 `json:"midpoint,omitempty"` + Timeframe string `json:"timeframe,omitempty"` +} + +type SnapshotLastTrade struct { + Timestamp int64 `json:"sip_timestamp,omitempty"` + Conditions []int32 `json:"conditions,omitempty"` + Price float64 `json:"price,omitempty"` + Size uint32 `json:"size,omitempty"` + Exchange int32 `json:"exchange,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + ID string `json:"id,omitempty"` + LastUpdated int64 `json:"last_updated,omitempty"` +} + +type Details struct { + ContractType string `json:"contract_type,omitempty"` + ExerciseStyle string `json:"exercise_style,omitempty"` + ExpirationDate string `json:"expiration_date,omitempty"` + SharesPerContract float64 `json:"shares_per_contract,omitempty"` + StrikePrice float64 `json:"strike_price,omitempty"` +} diff --git a/rest/snapshot.go b/rest/snapshot.go index 3f87f3db..2e8414e5 100644 --- a/rest/snapshot.go +++ b/rest/snapshot.go @@ -17,6 +17,7 @@ const ( ListOptionsChainSnapshotPath = "/v3/snapshot/options/{underlyingAsset}" GetCryptoFullBookSnapshotPath = "/v2/snapshot/locale/global/markets/crypto/tickers/{ticker}/book" GetIndicesSnapshotPath = "/v3/snapshot/indices" + ListAssetSnapshots = "/v3/snapshot" ) // SnapshotClient defines a REST client for the Polygon snapshot API. @@ -110,3 +111,24 @@ func (ac *SnapshotClient) GetIndicesSnapshot(ctx context.Context, params *models err := ac.Call(ctx, http.MethodGet, GetIndicesSnapshotPath, params, res, opts...) return res, err } + +// ListAssetSnapshots retrieves the snapshots for the specified tickers for the specified time. For more details see: +// - https://staging.polygon.io/docs/stocks/get_v3_snapshot +// - https://staging.polygon.io/docs/options/get_v3_snapshot +// +// This method returns an iterator that should be used to access the results via this pattern: +// +// iter := c.ListAssetSnapshots(context, params, opts...) +// for iter.Next() { +// log.Print(iter.Item()) // do something with the current value +// } +// if iter.Err() != nil { +// return iter.Err() +// } +func (ac *SnapshotClient) ListAssetSnapshots(ctx context.Context, params *models.ListAssetSnapshotsParams, options ...models.RequestOption) *iter.Iter[models.SnapshotResponseModel] { + return iter.NewIter(ctx, ListAssetSnapshots, params, func(uri string) (iter.ListResponse, []models.SnapshotResponseModel, error) { + res := &models.ListAssetSnapshotsResponse{} + err := ac.CallURL(ctx, http.MethodGet, uri, res, options...) + return res, res.Results, err + }) +} diff --git a/rest/snapshot_test.go b/rest/snapshot_test.go index 3d81753f..980d135b 100644 --- a/rest/snapshot_test.go +++ b/rest/snapshot_test.go @@ -3,12 +3,14 @@ package polygon_test import ( "context" "encoding/json" + "fmt" "testing" "github.com/jarcoal/httpmock" polygon "github.com/polygon-io/client-go/rest" "github.com/polygon-io/client-go/rest/models" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var snapshot1 = `{ @@ -596,3 +598,373 @@ func TestGetIndicesSnapshot(t *testing.T) { assert.Nil(t, err) assert.Equal(t, &expect, res) } + +func TestListAssetSnapshots(t *testing.T) { + c := polygon.New("API_KEY") + + httpmock.ActivateNonDefault(c.HTTP.GetClient()) + defer httpmock.DeactivateAndReset() + + tt := []struct { + name string + haveParams *models.ListAssetSnapshotsParams + haveRequestURL string + wantResponse string + testData []string + wantErr bool + }{ + { + name: "Stock tickers", + haveParams: models.ListAssetSnapshotsParams{}.WithTickerAnyOf("AAPL,META,F"), + haveRequestURL: "https://api.polygon.io/v3/snapshot?ticker.any_of=AAPL%2CMETA%2CF", + wantResponse: `{ + "results": [ + ` + indent(true, stockSnapshotsTestData[0], "\t\t") + `, + ` + indent(true, stockSnapshotsTestData[1], "\t\t") + `, + ` + indent(true, stockSnapshotsTestData[2], "\t\t") + ` + ], + "status": "OK", + "request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6", + "next_url": "https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg" + }`, + testData: stockSnapshotsTestData, + wantErr: false, + }, + { + name: "Options tickers", + haveParams: models.ListAssetSnapshotsParams{}.WithTickerAnyOf("O:AAPL230512C00050000,O:META230512C00020000,O:F230512C00005000"), + haveRequestURL: "https://api.polygon.io/v3/snapshot?ticker.any_of=O%3AAAPL230512C00050000%2CO%3AMETA230512C00020000%2CO%3AF230512C00005000", + wantResponse: `{ + "results": [ + ` + indent(true, optionsSnapshotsTestData[0], "\t\t") + `, + ` + indent(true, optionsSnapshotsTestData[1], "\t\t") + `, + ` + indent(true, optionsSnapshotsTestData[2], "\t\t") + ` + ], + "status": "OK", + "request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6", + "next_url": "https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg" + }`, + testData: optionsSnapshotsTestData, + }, + { + name: "Partial success (200/OK with an error message in the body)", + haveParams: models.ListAssetSnapshotsParams{}.WithTickerAnyOf("AAPL,APx"), + haveRequestURL: "https://api.polygon.io/v3/snapshot?ticker.any_of=AAPL%2CAPx", + wantResponse: `{ + "results": [ + ` + indent(true, partialSuccessWithStocksTestData[0], "\t\t") + `, + ` + indent(true, partialSuccessWithStocksTestData[1], "\t\t") + ` + ], + "status": "OK", + "request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6", + "next_url": "https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg" + }`, + testData: partialSuccessWithStocksTestData, + wantErr: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + + registerResponder(tc.haveRequestURL, tc.wantResponse) + registerResponder("https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg", "{}") + + iter := c.ListAssetSnapshots( + context.Background(), + tc.haveParams, + ) + + // iter creation + require.NoError(t, iter.Err()) + require.NotNil(t, iter.Item()) + + // correct values + var iterCount int + for iter.Next() { + var gotSnapshot models.SnapshotResponseModel + err := json.Unmarshal([]byte(tc.testData[iterCount]), &gotSnapshot) + require.Nil(t, err) + + require.Nil(t, iter.Err()) + assert.Equal(t, gotSnapshot, iter.Item()) + iterCount++ + } + + assert.Equal(t, len(tc.testData), iterCount, fmt.Sprintf("expected %d results", len(tc.testData))) + assert.False(t, iter.Next()) + assert.Nil(t, iter.Err()) + }) + } +} + +var stockSnapshotsTestData = []string{ + `{ + "market_status": "late_trading", + "name": "Apple Inc.", + "session": { + "change": -0.07, + "change_percent": -0.0403, + "close": 173.5, + "early_trading_change": 0, + "early_trading_change_percent": 0, + "high": 173.85, + "low": 172.11, + "open": 172.48, + "previous_close": 173.57, + "price": 173.5, + "volume": 50823329 + }, + "last_quote": { + "ask": 173.34, + "ask_size": 3, + "bid": 173.32, + "bid_size": 4, + "last_updated": 1683577209434314800, + "timeframe": "REAL-TIME" + }, + "last_trade": { + "conditions": [ + 12, + 22 + ], + "exchange": 4, + "id": "247862", + "last_updated": 1683577205678289200, + "price": 173.5, + "size": 31535, + "timeframe": "REAL-TIME" + }, + "ticker": "AAPL", + "type": "stocks" + }`, + `{ + "market_status": "late_trading", + "name": "Meta Platforms, Inc. Class A Common Stock", + "session": { + "change": -0.04, + "change_percent": -0.0172, + "close": 233.27, + "early_trading_change": 0, + "early_trading_change_percent": 0, + "high": 235.62, + "low": 230.27, + "open": 231.415, + "previous_close": 232.78, + "price": 232.74, + "volume": 14940329 + }, + "last_quote": { + "ask": 232.83, + "ask_size": 1, + "bid": 232.73, + "bid_size": 1, + "last_updated": 1683577187244746200, + "timeframe": "REAL-TIME" + }, + "last_trade": { + "conditions": [ + 12, + 37 + ], + "exchange": 4, + "id": "57128", + "last_updated": 1683577202547284000, + "price": 232.74, + "size": 50, + "timeframe": "REAL-TIME" + }, + "ticker": "META", + "type": "stocks" + }`, + `{ + "market_status": "late_trading", + "name": "Ford Motor Company", + "session": { + "change": 0.005, + "change_percent": 0.0417, + "close": 12.02, + "early_trading_change": 0, + "early_trading_change_percent": 0, + "high": 12.055, + "low": 11.85, + "open": 12.02, + "previous_close": 11.99, + "price": 11.995, + "volume": 49539926 + }, + "last_quote": { + "ask": 12, + "ask_size": 23, + "bid": 11.99, + "bid_size": 28, + "last_updated": 1683577084319878700, + "timeframe": "REAL-TIME" + }, + "last_trade": { + "conditions": [ + 12, + 37 + ], + "exchange": 4, + "id": "71697320268354", + "last_updated": 1683577186411804000, + "price": 11.995, + "size": 1, + "timeframe": "REAL-TIME" + }, + "ticker": "F", + "type": "stocks" + }`, +} + +var optionsSnapshotsTestData = []string{ + `{ + "name": "AAPL $50.00 call", + "market_status": "open", + "ticker": "O:AAPL230512C00050000", + "type": "options", + "last_quote": { + "ask": 123.1, + "ask_size": 90, + "bid": 122.95, + "bid_size": 90, + "last_updated": 1683731850932649728, + "midpoint": 123.025, + "timeframe": "REAL-TIME" + }, + "last_trade": {}, + "session": {}, + "break_even_price": 173.025, + "details": { + "contract_type": "call", + "exercise_style": "american", + "expiration_date": "2023-05-12", + "shares_per_contract": 100, + "strike_price": 50 + }, + "greeks": {}, + "underlying_asset": { + "change_to_break_even": -0.11, + "last_updated": 1683732072879546553, + "price": 173.135, + "ticker": "AAPL", + "timeframe": "REAL-TIME" + }, + "error": "", + "message": "" + }`, + `{ + "name": "META $20.00 call", + "market_status": "open", + "ticker": "O:META230512C00020000", + "type": "options", + "last_quote": {}, + "last_trade": { + "sip_timestamp": 1682970890371000000, + "conditions": [ + 209 + ], + "price": 223.75, + "size": 1, + "exchange": 302, + "timeframe": "REAL-TIME" + }, + "session": {}, + "details": { + "contract_type": "call", + "exercise_style": "american", + "expiration_date": "2023-05-12", + "shares_per_contract": 100, + "strike_price": 20 + }, + "greeks": {}, + "underlying_asset": { + "last_updated": 1683731579449632715, + "price": 232.37, + "ticker": "META", + "timeframe": "REAL-TIME" + }, + "error": "", + "message": "" + }`, + `{ + "name": "F $5.00 call", + "market_status": "open", + "ticker": "O:F230512C00005000", + "type": "options", + "last_quote": {}, + "last_trade": { + "sip_timestamp": 1683316735432000000, + "conditions": [ + 232 + ], + "price": 6.97, + "size": 1, + "exchange": 312, + "timeframe": "REAL-TIME" + }, + "session": {}, + "details": { + "contract_type": "call", + "exercise_style": "american", + "expiration_date": "2023-05-12", + "shares_per_contract": 100, + "strike_price": 5 + }, + "greeks": {}, + "underlying_asset": { + "last_updated": 1683732072773028096, + "price": 11.93, + "ticker": "F", + "timeframe": "REAL-TIME" + } + }`, +} + +var partialSuccessWithStocksTestData = []string{ + `{ + "market_status": "late_trading", + "name": "Apple Inc.", + "session": { + "change": -0.07, + "change_percent": -0.0403, + "close": 173.5, + "early_trading_change": 0, + "early_trading_change_percent": 0, + "high": 173.85, + "low": 172.11, + "open": 172.48, + "previous_close": 173.57, + "price": 173.5, + "volume": 50823329 + }, + "last_quote": { + "ask": 173.34, + "ask_size": 3, + "bid": 173.32, + "bid_size": 4, + "last_updated": 1683577209434314800, + "timeframe": "REAL-TIME" + }, + "last_trade": { + "conditions": [ + 12, + 22 + ], + "exchange": 4, + "id": "247862", + "last_updated": 1683577205678289200, + "price": 173.5, + "size": 31535, + "timeframe": "REAL-TIME" + }, + "ticker": "AAPL", + "type": "stocks" + }`, + `{ + "error": "NOT_ENTITLED", + "message": "Not entitled to this ticker.", + "ticker": "APy" + }`, +}