github.com/polygon-io/client-go@v1.16.4/rest/snapshot_test.go (about)

     1  package polygon_test
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"testing"
     8  
     9  	"github.com/jarcoal/httpmock"
    10  	polygon "github.com/polygon-io/client-go/rest"
    11  	"github.com/polygon-io/client-go/rest/models"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  var snapshot1 = `{
    17  	"day": {
    18  		"c": 20.506,
    19  		"h": 20.64,
    20  		"l": 20.506,
    21  		"o": 20.64,
    22  		"v": 37216,
    23  		"vw": 20.616
    24  	},
    25  	"lastQuote": {
    26  		"P": 20.6,
    27  		"p": 20.5,
    28  		"S": 22,
    29  		"s": 13,
    30  		"t": 1605192959994246100
    31  	},
    32  	"lastTrade": {
    33  		"c": [
    34  			14,
    35  			41
    36  		],
    37  		"i": "71675577320245",
    38  		"p": 20.506,
    39  		"s": 2416,
    40  		"t": 1605192894630916600,
    41  		"x": 4
    42  	},
    43  	"min": {
    44  		"av": 37216.0,
    45  		"c": 20.506,
    46  		"h": 20.506,
    47  		"l": 20.506,
    48  		"o": 20.506,
    49  		"v": 5000,
    50  		"vw": 20.5105,
    51  		"t": 1684428060000,
    52  		"n": 4
    53  	},
    54  	"prevDay": {
    55  		"c": 20.63,
    56  		"h": 21,
    57  		"l": 20.5,
    58  		"o": 20.79,
    59  		"v": 292738,
    60  		"vw": 20.6939
    61  	},
    62  	"ticker": "BCAT",
    63  	"todaysChange": -0.124,
    64  	"todaysChangePerc": -0.601,
    65  	"updated": 1605192894630916600,
    66  	"fmv": 20.506
    67  }`
    68  
    69  var snapshot2 = `{
    70  	"day": {
    71  		"c": 313.225,
    72  		"h": 314.35,
    73  		"l": 309.71,
    74  		"o": 310.09,
    75  		"v": 6322693,
    76  		"vw": 312.6791
    77  	},
    78  	"lastQuote": {
    79  		"P": 313.13,
    80  		"p": 313.11,
    81  		"S": 4,
    82  		"s": 2,
    83  		"t": 1649083047683654000
    84  	},
    85  	"lastTrade": {
    86  		"i": "23432",
    87  		"p": 313.1296,
    88  		"s": 100.0,
    89  		"t": 1649083047682204000,
    90  		"x": 4
    91  	},
    92  	"min": {
    93  		"av": 6321712,
    94  		"c": 313.1826,
    95  		"h": 313.19,
    96  		"l": 312.66,
    97  		"o": 312.78,
    98  		"v": 54315,
    99  		"vw": 312.9441,
   100  		"t": 1684428060000,
   101  		"n": 4
   102  	},
   103  	"prevDay": {
   104  		"c": 309.42,
   105  		"h": 310.13,
   106  		"l": 305.54,
   107  		"o": 309.37,
   108  		"v": 27101029,
   109  		"vw": 308.0485
   110  	},
   111  	"ticker": "MSFT",
   112  	"todaysChange": 3.71,
   113  	"todaysChangePerc": 1.199,
   114  	"updated": 1649083047682204000,
   115  	"fmv": 313.1296
   116  }`
   117  
   118  func TestListSnapshotAllTickers(t *testing.T) {
   119  	c := polygon.New("API_KEY")
   120  
   121  	httpmock.ActivateNonDefault(c.HTTP.GetClient())
   122  	defer httpmock.DeactivateAndReset()
   123  
   124  	expectedResponse := `{
   125  	"status": "OK",
   126  	"count": 2,
   127  	"tickers": [
   128  ` + indent(true, snapshot1, "\t\t") + `,
   129  ` + indent(true, snapshot2, "\t\t") + `
   130  	]
   131  }`
   132  
   133  	registerResponder("https://api.polygon.io/v2/snapshot/locale/us/markets/stocks/tickers?tickers=AAPL%2CMSFT", expectedResponse)
   134  	res, err := c.GetAllTickersSnapshot(context.Background(), models.GetAllTickersSnapshotParams{
   135  		Locale:     "us",
   136  		MarketType: "stocks",
   137  	}.WithTickers("AAPL,MSFT"))
   138  	assert.Nil(t, err)
   139  
   140  	var expect models.GetAllTickersSnapshotResponse
   141  	err = json.Unmarshal([]byte(expectedResponse), &expect)
   142  	assert.Nil(t, err)
   143  	assert.Equal(t, &expect, res)
   144  }
   145  
   146  func TestGetTickerSnapshot(t *testing.T) {
   147  	c := polygon.New("API_KEY")
   148  
   149  	httpmock.ActivateNonDefault(c.HTTP.GetClient())
   150  	defer httpmock.DeactivateAndReset()
   151  
   152  	expectedResponse := `{
   153  	"status": "OK",
   154  	"count": 2,
   155  	"ticker": ` + indent(false, snapshot1, "\t") + `
   156  }`
   157  
   158  	registerResponder("https://api.polygon.io/v2/snapshot/locale/us/markets/stocks/tickers/AAPL", expectedResponse)
   159  	res, err := c.GetTickerSnapshot(context.Background(), &models.GetTickerSnapshotParams{
   160  		Ticker:     "AAPL",
   161  		Locale:     "us",
   162  		MarketType: "stocks",
   163  	})
   164  	assert.Nil(t, err)
   165  
   166  	var expect models.GetTickerSnapshotResponse
   167  	err = json.Unmarshal([]byte(expectedResponse), &expect)
   168  	assert.Nil(t, err)
   169  	assert.Equal(t, &expect, res)
   170  }
   171  
   172  func TestGetGainersLosersSnapshot(t *testing.T) {
   173  	c := polygon.New("API_KEY")
   174  
   175  	httpmock.ActivateNonDefault(c.HTTP.GetClient())
   176  	defer httpmock.DeactivateAndReset()
   177  
   178  	expectedResponse := `{
   179  	"status": "OK",
   180  	"count": 2,
   181  	"tickers": [
   182  ` + indent(true, snapshot1, "\t\t") + `,
   183  ` + indent(true, snapshot2, "\t\t") + `
   184  	]
   185  }`
   186  
   187  	registerResponder("https://api.polygon.io/v2/snapshot/locale/us/markets/stocks/gainers", expectedResponse)
   188  	res, err := c.GetGainersLosersSnapshot(context.Background(), &models.GetGainersLosersSnapshotParams{
   189  		Locale:     "us",
   190  		MarketType: "stocks",
   191  		Direction:  "gainers",
   192  	})
   193  	assert.Nil(t, err)
   194  
   195  	var expect models.GetGainersLosersSnapshotResponse
   196  	err = json.Unmarshal([]byte(expectedResponse), &expect)
   197  	assert.Nil(t, err)
   198  	assert.Equal(t, &expect, res)
   199  }
   200  
   201  func TestGetOptionContractSnapshot(t *testing.T) {
   202  	c := polygon.New("API_KEY")
   203  
   204  	httpmock.ActivateNonDefault(c.HTTP.GetClient())
   205  	defer httpmock.DeactivateAndReset()
   206  
   207  	expectedResponse := `{
   208  	"status": "OK",
   209  	"request_id": "d9ff18dac69f55c218f69e4753706acd",
   210  	"results": {
   211  		"break_even_price": 171.075,
   212  		"day": {
   213  			"change": -1.05,
   214  			"change_percent": -4.67,
   215  			"close": 21.4,
   216  			"high": 22.49,
   217  			"last_updated": 1636520400000000000,
   218  			"low": 21.35,
   219  			"open": 22.49,
   220  			"previous_close": 22.45,
   221  			"volume": 37,
   222  			"vwap": 21.6741
   223  		},
   224  		"details": {
   225  			"contract_type": "call",
   226  			"exercise_style": "american",
   227  			"expiration_date": "2023-06-16",
   228  			"shares_per_contract": 100,
   229  			"strike_price": 150,
   230  			"ticker": "O:AAPL230616C00150000"
   231  		},
   232  		"greeks": {
   233  			"delta": 0.5520187372272933,
   234  			"gamma": 0.00706756515659829,
   235  			"theta": -0.018532772783847958,
   236  			"vega": 0.7274811132998142
   237  		},
   238  		"implied_volatility": 0.3048997097864957,
   239  		"last_quote": {
   240  			"ask": 21.25,
   241  			"ask_size": 110,
   242  			"bid": 20.9,
   243  			"bid_size": 172,
   244  			"last_updated": 1636573458756383500,
   245  			"midpoint": 21.075,
   246  			"timeframe": "REAL-TIME"
   247  		},
   248  		"last_trade": {
   249  			"sip_timestamp": 1676573362154648300,
   250  			"conditions": [
   251  				209
   252  			],
   253  			"price": 110.9,
   254  			"size": 10,
   255  			"exchange": 308,
   256  			"timeframe": "REAL-TIME"
   257  		},
   258  		"open_interest": 8921,
   259  		"underlying_asset": {
   260  			"change_to_break_even": 23.123999999999995,
   261  			"last_updated": 1636573459862384600,
   262  			"price": 147.951,
   263  			"ticker": "AAPL",
   264  			"timeframe": "REAL-TIME"
   265  		},
   266  		"fmv": 21.075
   267  	}
   268  }`
   269  
   270  	registerResponder("https://api.polygon.io/v3/snapshot/options/AAPL/O:AAPL230616C00150000", expectedResponse)
   271  	res, err := c.GetOptionContractSnapshot(context.Background(), &models.GetOptionContractSnapshotParams{
   272  		UnderlyingAsset: "AAPL",
   273  		OptionContract:  "O:AAPL230616C00150000",
   274  	})
   275  	assert.Nil(t, err)
   276  
   277  	var expect models.GetOptionContractSnapshotResponse
   278  	err = json.Unmarshal([]byte(expectedResponse), &expect)
   279  	assert.Nil(t, err)
   280  	assert.Equal(t, &expect, res)
   281  }
   282  
   283  func TestListOptionsChainSnapshot(t *testing.T) {
   284  	c := polygon.New("API_KEY")
   285  
   286  	httpmock.ActivateNonDefault(c.HTTP.GetClient())
   287  	defer httpmock.DeactivateAndReset()
   288  
   289  	chain1 := `{
   290  		"break_even_price": 162.375,
   291  		"day": {
   292  		"change": 0,
   293  			"change_percent": 0,
   294  			"close": 79.35,
   295  			"high": 79.35,
   296  			"last_updated": 1672434000000,
   297  			"low": 79.3,
   298  			"open": 79.3,
   299  			"previous_close": 79.35,
   300  			"volume": 22,
   301  			"vwap": 79.325
   302  		},
   303  		"details": {
   304  			"contract_type": "call",
   305  			"exercise_style": "american",
   306  			"expiration_date": "2023-01-06",
   307  			"shares_per_contract": 100,
   308  			"strike_price": 50,
   309  			"ticker": "O:AAPL230106C00050000"
   310  		},
   311  		"greeks": {},
   312  		"last_quote": {
   313  			"ask": 75.05,
   314  			"ask_size": 48,
   315  			"bid": 74.85,
   316  			"bid_size": 43,
   317  			"last_updated": 1672775256862312000,
   318  			"midpoint": 112.375,
   319  			"timeframe": "DELAYED"
   320  		},
   321  		"last_trade": {
   322  			"sip_timestamp": 1676573362154648300,
   323  			"conditions": [
   324  				209
   325  			],
   326  			"price": 110.9,
   327  			"size": 10,
   328  			"exchange": 308,
   329  			"timeframe": "REAL-TIME"
   330  		},
   331  		"open_interest": 5,
   332  		"underlying_asset": {
   333  			"change_to_break_even": 37.435,
   334  			"last_updated": 1672775257417223400,
   335  			"price": 124.94,
   336  			"ticker": "AAPL",
   337  			"timeframe": "DELAYED"
   338  		},
   339  		"fmv": 75.06
   340  	}`
   341  	chain2 := `{
   342  		"break_even_price": 162.375,
   343  		"day": {
   344  		"change": 0,
   345  			"change_percent": 0,
   346  			"close": 79.35,
   347  			"high": 79.35,
   348  			"last_updated": 1672434000000,
   349  			"low": 79.3,
   350  			"open": 79.3,
   351  			"previous_close": 79.35,
   352  			"volume": 22,
   353  			"vwap": 79.325
   354  		},
   355  		"details": {
   356  			"contract_type": "call",
   357  			"exercise_style": "american",
   358  			"expiration_date": "2023-01-06",
   359  			"shares_per_contract": 100,
   360  			"strike_price": 50,
   361  			"ticker": "O:AAPL230106C00050000"
   362  		},
   363  		"greeks": {},
   364  		"last_quote": {
   365  			"ask": 75.05,
   366  			"ask_size": 48,
   367  			"bid": 74.85,
   368  			"bid_size": 43,
   369  			"last_updated": 1672775256862312000,
   370  			"midpoint": 112.375,
   371  			"timeframe": "DELAYED"
   372  		},
   373  		"last_trade": {
   374  			"sip_timestamp": 1676573362154648300,
   375  			"conditions": [
   376  				209
   377  			],
   378  			"price": 110.9,
   379  			"size": 10,
   380  			"exchange": 308,
   381  			"timeframe": "REAL-TIME"
   382  		},
   383  		"open_interest": 5,
   384  		"underlying_asset": {
   385  			"change_to_break_even": 37.435,
   386  			"last_updated": 1672775257417223400,
   387  			"price": 124.94,
   388  			"ticker": "AAPL",
   389  			"timeframe": "DELAYED"
   390  		},
   391  		"fmv": 75.06
   392  	}`
   393  	chain3 := `{
   394  		"break_even_price": 162.375,
   395  		"day": {
   396  		"change": 0,
   397  			"change_percent": 0,
   398  			"close": 79.35,
   399  			"high": 79.35,
   400  			"last_updated": 1672434000000,
   401  			"low": 79.3,
   402  			"open": 79.3,
   403  			"previous_close": 79.35,
   404  			"volume": 22,
   405  			"vwap": 79.325
   406  		},
   407  		"details": {
   408  			"contract_type": "call",
   409  			"exercise_style": "american",
   410  			"expiration_date": "2023-01-06",
   411  			"shares_per_contract": 100,
   412  			"strike_price": 50,
   413  			"ticker": "O:AAPL230106C00050000"
   414  		},
   415  		"greeks": {},
   416  		"last_quote": {
   417  			"ask": 75.05,
   418  			"ask_size": 48,
   419  			"bid": 74.85,
   420  			"bid_size": 43,
   421  			"last_updated": 1672775256862312000,
   422  			"midpoint": 112.375,
   423  			"timeframe": "DELAYED"
   424  		},
   425  		"last_trade": {
   426  			"sip_timestamp": 1676573362154648300,
   427  			"conditions": [
   428  				209
   429  			],
   430  			"price": 110.9,
   431  			"size": 10,
   432  			"exchange": 308,
   433  			"timeframe": "REAL-TIME"
   434  		},
   435  		"open_interest": 5,
   436  		"underlying_asset": {
   437  			"change_to_break_even": 37.435,
   438  			"last_updated": 1672775257417223400,
   439  			"price": 124.94,
   440  			"ticker": "AAPL",
   441  			"timeframe": "DELAYED"
   442  		},
   443  		"fmv": 75.06
   444  	}`
   445  
   446  	expectedResponse := `{
   447  	  "results": [
   448  		` + indent(true, chain1, "\t\t") + `,
   449  		` + indent(true, chain2, "\t\t") + `,
   450  		` + indent(true, chain3, "\t\t") + `
   451  	  ],
   452  	  "status": "OK",
   453  	  "request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6",
   454  	  "next_url": "https://api.polygon.io/v3/snapshot/options/AAPL?cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg"
   455  	}`
   456  
   457  	registerResponder("https://api.polygon.io/v3/snapshot/options/AAPL", expectedResponse)
   458  	registerResponder("https://api.polygon.io/v3/snapshot/options/AAPL?cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg", "{}")
   459  
   460  	iter := c.ListOptionsChainSnapshot(context.Background(), &models.ListOptionsChainParams{UnderlyingAsset: "AAPL"})
   461  
   462  	// iter creation
   463  	assert.Nil(t, iter.Err())
   464  	assert.NotNil(t, iter.Item())
   465  
   466  	// first item
   467  	assert.True(t, iter.Next())
   468  	assert.Nil(t, iter.Err())
   469  	var expect1 models.OptionContractSnapshot
   470  	err := json.Unmarshal([]byte(chain1), &expect1)
   471  	assert.Nil(t, err)
   472  	assert.Equal(t, expect1, iter.Item())
   473  
   474  	// second item
   475  	assert.True(t, iter.Next())
   476  	assert.Nil(t, iter.Err())
   477  	var expect2 models.OptionContractSnapshot
   478  	err = json.Unmarshal([]byte(chain2), &expect2)
   479  	assert.Nil(t, err)
   480  	assert.Equal(t, expect2, iter.Item())
   481  
   482  	// third item
   483  	assert.True(t, iter.Next())
   484  	assert.Nil(t, iter.Err())
   485  	var expect3 models.OptionContractSnapshot
   486  	err = json.Unmarshal([]byte(chain3), &expect3)
   487  	assert.Nil(t, err)
   488  	assert.Equal(t, expect3, iter.Item())
   489  
   490  	// end of list
   491  	assert.False(t, iter.Next())
   492  	assert.Nil(t, iter.Err())
   493  }
   494  
   495  func TestGetCryptoFullBookSnapshot(t *testing.T) {
   496  	c := polygon.New("API_KEY")
   497  
   498  	httpmock.ActivateNonDefault(c.HTTP.GetClient())
   499  	defer httpmock.DeactivateAndReset()
   500  
   501  	expectedResponse := `{
   502  	"status": "OK",
   503  	"data": {
   504  		"askCount": 593.1412981600005,
   505  		"asks": [
   506  			{
   507  				"p": 11454,
   508  				"x": {
   509  					"2": 1
   510  				}
   511  			},
   512  			{
   513  				"p": 11455,
   514  				"x": {
   515  					"2": 1
   516  				}
   517  			}
   518  		],
   519  		"bidCount": 694.951789670001,
   520  		"bids": [
   521  			{
   522  				"p": 16303.17,
   523  				"x": {
   524  					"1": 2
   525  				}
   526  			},
   527  			{
   528  				"p": 16302.94,
   529  				"x": {
   530  					"1": 0.02859424,
   531  					"6": 0.023455
   532  				}
   533  			}
   534  		],
   535  		"spread": -4849.17,
   536  		"ticker": "X:BTCUSD",
   537  		"updated": 1605295074162
   538  	}
   539  }`
   540  
   541  	registerResponder("https://api.polygon.io/v2/snapshot/locale/global/markets/crypto/tickers/X:BTCUSD/book", expectedResponse)
   542  	res, err := c.GetCryptoFullBookSnapshot(context.Background(), &models.GetCryptoFullBookSnapshotParams{
   543  		Ticker: "X:BTCUSD",
   544  	})
   545  	assert.Nil(t, err)
   546  
   547  	var expect models.GetCryptoFullBookSnapshotResponse
   548  	err = json.Unmarshal([]byte(expectedResponse), &expect)
   549  	assert.Nil(t, err)
   550  	assert.Equal(t, &expect, res)
   551  }
   552  
   553  func TestGetIndicesSnapshot(t *testing.T) {
   554  	c := polygon.New("API_KEY")
   555  
   556  	httpmock.ActivateNonDefault(c.HTTP.GetClient())
   557  	defer httpmock.DeactivateAndReset()
   558  	expectedIndicesSnapshotResponse := `{
   559    "results": [
   560      {
   561        "value": 1326.17,
   562        "name": "Dow Jones Americas Health Care Index",
   563        "ticker": "I:A1HCR",
   564        "market_status": "open",
   565        "type": "indices",
   566        "session": {
   567          "change": 47.07,
   568          "change_percent": 3.68,
   569          "close": 1282.67,
   570          "high": 1288.89,
   571          "low": 1282.25,
   572          "open": 1283.33,
   573          "previous_close": 1279.1000000000001
   574        }
   575      },
   576      {
   577        "value": 3918.32,
   578        "name": "Standard & Poor's 500",
   579        "ticker": "I:SPX",
   580        "market_status": "open",
   581        "type": "indices",
   582        "session": {
   583          "change": 5.56,
   584          "change_percent": 0.142,
   585          "close": 3926.36,
   586          "high": 3927.38,
   587          "low": 3878.1,
   588          "open": 3914.13,
   589          "previous_close": 3912.76
   590        }
   591      }
   592    ],
   593    "status": "OK",
   594    "request_id": "5ad18f153c5aa4a543cc10aeb9245622"
   595  }
   596  
   597  `
   598  
   599  	expectedGetIndicesSnapshotUrl := "https://api.polygon.io/v3/snapshot/indices?ticker.any_of=I%3AA1HCR%2CI%3ASPX"
   600  	registerResponder(expectedGetIndicesSnapshotUrl, expectedIndicesSnapshotResponse)
   601  	tickerAnyOf := []string{"I:A1HCR", "I:SPX"}
   602  
   603  	res, err := c.GetIndicesSnapshot(context.Background(), models.GetIndicesSnapshotParams{}.WithTickerAnyOf(tickerAnyOf...))
   604  	assert.Nil(t, err)
   605  
   606  	var expect models.GetIndicesSnapshotResponse
   607  	err = json.Unmarshal([]byte(expectedIndicesSnapshotResponse), &expect)
   608  	assert.Nil(t, err)
   609  	assert.Equal(t, &expect, res)
   610  }
   611  
   612  func TestListUniversalSnapshots(t *testing.T) {
   613  	c := polygon.New("API_KEY")
   614  
   615  	httpmock.ActivateNonDefault(c.HTTP.GetClient())
   616  	defer httpmock.DeactivateAndReset()
   617  
   618  	tt := []struct {
   619  		name           string
   620  		haveParams     *models.ListUniversalSnapshotsParams
   621  		haveRequestURL string
   622  		wantResponse   string
   623  		testData       []string
   624  		wantErr        bool
   625  	}{
   626  		{
   627  			name:           "Stock tickers",
   628  			haveParams:     models.ListUniversalSnapshotsParams{}.WithTickerAnyOf("AAPL,META,F"),
   629  			haveRequestURL: "https://api.polygon.io/v3/snapshot?ticker.any_of=AAPL%2CMETA%2CF",
   630  			wantResponse: `{
   631  				"results": [
   632  					` + indent(true, stockSnapshotsTestData[0], "\t\t") + `,
   633  					` + indent(true, stockSnapshotsTestData[1], "\t\t") + `,
   634  					` + indent(true, stockSnapshotsTestData[2], "\t\t") + `
   635  					],
   636  					"status": "OK",
   637  					"request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6",
   638  					"next_url": "https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg"
   639  				}`,
   640  			testData: stockSnapshotsTestData,
   641  			wantErr:  false,
   642  		},
   643  		{
   644  			name:           "Options tickers",
   645  			haveParams:     models.ListUniversalSnapshotsParams{}.WithTickerAnyOf("O:AAPL230512C00050000,O:META230512C00020000,O:F230512C00005000"),
   646  			haveRequestURL: "https://api.polygon.io/v3/snapshot?ticker.any_of=O%3AAAPL230512C00050000%2CO%3AMETA230512C00020000%2CO%3AF230512C00005000",
   647  			wantResponse: `{
   648  				"results": [
   649  					` + indent(true, optionsSnapshotsTestData[0], "\t\t") + `,
   650  					` + indent(true, optionsSnapshotsTestData[1], "\t\t") + `,
   651  					` + indent(true, optionsSnapshotsTestData[2], "\t\t") + `
   652  					],
   653  					"status": "OK",
   654  					"request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6",
   655  					"next_url": "https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg"
   656  				}`,
   657  			testData: optionsSnapshotsTestData,
   658  		},
   659  		{
   660  			name:           "Crypto Snapshot",
   661  			haveParams:     models.ListUniversalSnapshotsParams{}.WithTickerAnyOf("X:BTCUSD,X:ETHUSD,X:FLOWUSD"),
   662  			haveRequestURL: "https://api.polygon.io/v3/snapshot?ticker.any_of=X%3ABTCUSD%2CX%3AETHUSD%2CX%3AFLOWUSD",
   663  			wantResponse: `{
   664  				"results": [
   665  					` + indent(true, cryptoSnapshotsTestData[0], "\t\t") + `,
   666  					` + indent(true, cryptoSnapshotsTestData[1], "\t\t") + `,
   667  					` + indent(true, cryptoSnapshotsTestData[2], "\t\t") + `
   668  					],
   669  					"status": "OK",
   670  					"request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6",
   671  					"next_url": "https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg"
   672  				}`,
   673  			testData: cryptoSnapshotsTestData,
   674  		},
   675  		{
   676  			name:           "Forex Snapshot",
   677  			haveParams:     models.ListUniversalSnapshotsParams{}.WithTickerAnyOf("C:USDCAD,C:USDEUR,C:USDAUD"),
   678  			haveRequestURL: "https://api.polygon.io/v3/snapshot?ticker.any_of=C%3AUSDCAD%2CC%3AUSDEUR%2CC%3AUSDAUD",
   679  			wantResponse: `{
   680  				"results": [
   681  					` + indent(true, forexSnapshotTestData[0], "\t\t") + `,
   682  					` + indent(true, forexSnapshotTestData[1], "\t\t") + `,
   683  					` + indent(true, forexSnapshotTestData[2], "\t\t") + `
   684  					],
   685  					"status": "OK",
   686  					"request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6",
   687  					"next_url": "https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg"
   688  				}`,
   689  			testData: forexSnapshotTestData,
   690  		},
   691  		{
   692  			name:           "Indices Snapshot",
   693  			haveParams:     models.ListUniversalSnapshotsParams{}.WithTickerAnyOf("I:SPX,I:DJI,I:A1BSC"),
   694  			haveRequestURL: "https://api.polygon.io/v3/snapshot?ticker.any_of=I%3ASPX%2CI%3ADJI%2CI%3AA1BSC",
   695  			wantResponse: `{
   696  				"results": [
   697  					` + indent(true, indicesSnapshotTestData[0], "\t\t") + `,
   698  					` + indent(true, indicesSnapshotTestData[1], "\t\t") + `,
   699  					` + indent(true, indicesSnapshotTestData[2], "\t\t") + `
   700  					],
   701  					"status": "OK",
   702  					"request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6",
   703  					"next_url": "https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg"
   704  				}`,
   705  			testData: indicesSnapshotTestData,
   706  		},
   707  		{
   708  			name:           "Partial success (200/OK with an error message in the body)",
   709  			haveParams:     models.ListUniversalSnapshotsParams{}.WithTickerAnyOf("AAPL,APx"),
   710  			haveRequestURL: "https://api.polygon.io/v3/snapshot?ticker.any_of=AAPL%2CAPx",
   711  			wantResponse: `{
   712  				"results": [
   713  					` + indent(true, partialSuccessWithStocksTestData[0], "\t\t") + `,
   714  					` + indent(true, partialSuccessWithStocksTestData[1], "\t\t") + `
   715  					],
   716  					"status": "OK",
   717  					"request_id": "0d350849-a2a8-43c5-8445-9c6f55d371e6",
   718  					"next_url": "https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg"
   719  				}`,
   720  			testData: partialSuccessWithStocksTestData,
   721  			wantErr:  true,
   722  		},
   723  	}
   724  
   725  	for _, tc := range tt {
   726  		t.Run(tc.name, func(t *testing.T) {
   727  
   728  			registerResponder(tc.haveRequestURL, tc.wantResponse)
   729  			registerResponder("https://api.polygon.io/v3/snapshot/cursor=YXA9MSZhcz0mbGltaXQ9MSZzb3J0PXRpY2tlcg", "{}")
   730  
   731  			iter := c.ListUniversalSnapshots(
   732  				context.Background(),
   733  				tc.haveParams,
   734  			)
   735  
   736  			require.NoError(t, iter.Err())
   737  			require.NotNil(t, iter.Item())
   738  
   739  			var iterCount int
   740  			for iter.Next() {
   741  				var gotSnapshot models.SnapshotResponseModel
   742  				err := json.Unmarshal([]byte(tc.testData[iterCount]), &gotSnapshot)
   743  				require.Nil(t, err)
   744  
   745  				require.Nil(t, iter.Err())
   746  				assert.Equal(t, gotSnapshot, iter.Item())
   747  				iterCount++
   748  			}
   749  
   750  			assert.Equal(t, len(tc.testData), iterCount, fmt.Sprintf("expected %d results", len(tc.testData)))
   751  			assert.False(t, iter.Next())
   752  			assert.Nil(t, iter.Err())
   753  		})
   754  	}
   755  }
   756  
   757  var stockSnapshotsTestData = []string{
   758  	`{
   759  		"market_status": "late_trading",
   760  		"name": "Apple Inc.",
   761  		"session": {
   762  			"change": -0.07,
   763  			"change_percent": -0.0403,
   764  			"close": 173.5,
   765  			"early_trading_change": 0,
   766  			"early_trading_change_percent": 0,
   767  			"high": 173.85,
   768  			"low": 172.11,
   769  			"open": 172.48,
   770  			"previous_close": 173.57,
   771  			"price": 173.5,
   772  			"volume": 50823329
   773  		},
   774  		"last_quote": {
   775  			"ask": 173.34,
   776  			"ask_size": 3,
   777  			"bid": 173.32,
   778  			"bid_size": 4,
   779  			"last_updated": 1683577209434314800,
   780  			"timeframe": "REAL-TIME"
   781  		},
   782  		"last_trade": {
   783  			"conditions": [
   784  				12,
   785  				22
   786  			],
   787  			"exchange": 4,
   788  			"id": "247862",
   789  			"last_updated": 1683577205678289200,
   790  			"price": 173.5,
   791  			"size": 31535,
   792  			"timeframe": "REAL-TIME"
   793  		},
   794  		"ticker": "AAPL",
   795  		"type": "stocks"
   796  	}`,
   797  	`{
   798  		"market_status": "late_trading",
   799  		"name": "Meta Platforms, Inc. Class A Common Stock",
   800  		"session": {
   801  			"change": -0.04,
   802  			"change_percent": -0.0172,
   803  			"close": 233.27,
   804  			"early_trading_change": 0,
   805  			"early_trading_change_percent": 0,
   806  			"high": 235.62,
   807  			"low": 230.27,
   808  			"open": 231.415,
   809  			"previous_close": 232.78,
   810  			"price": 232.74,
   811  			"volume": 14940329
   812  		},
   813  		"last_quote": {
   814  			"ask": 232.83,
   815  			"ask_size": 1,
   816  			"bid": 232.73,
   817  			"bid_size": 1,
   818  			"last_updated": 1683577187244746200,
   819  			"timeframe": "REAL-TIME"
   820  		},
   821  		"last_trade": {
   822  			"conditions": [
   823  				12,
   824  				37
   825  			],
   826  			"exchange": 4,
   827  			"id": "57128",
   828  			"last_updated": 1683577202547284000,
   829  			"price": 232.74,
   830  			"size": 50,
   831  			"timeframe": "REAL-TIME"
   832  		},
   833  		"ticker": "META",
   834  		"type": "stocks"
   835  	}`,
   836  	`{
   837  		"market_status": "late_trading",
   838  		"name": "Ford Motor Company",
   839  		"session": {
   840  			"change": 0.005,
   841  			"change_percent": 0.0417,
   842  			"close": 12.02,
   843  			"early_trading_change": 0,
   844  			"early_trading_change_percent": 0,
   845  			"high": 12.055,
   846  			"low": 11.85,
   847  			"open": 12.02,
   848  			"previous_close": 11.99,
   849  			"price": 11.995,
   850  			"volume": 49539926
   851  		},
   852  		"last_quote": {
   853  			"ask": 12,
   854  			"ask_size": 23,
   855  			"bid": 11.99,
   856  			"bid_size": 28,
   857  			"last_updated": 1683577084319878700,
   858  			"timeframe": "REAL-TIME"
   859  		},
   860  		"last_trade": {
   861  			"conditions": [
   862  				12,
   863  				37
   864  			],
   865  			"exchange": 4,
   866  			"id": "71697320268354",
   867  			"last_updated": 1683577186411804000,
   868  			"price": 11.995,
   869  			"size": 1,
   870  			"timeframe": "REAL-TIME"
   871  		},
   872  		"ticker": "F",
   873  		"type": "stocks"
   874  	}`,
   875  }
   876  
   877  var optionsSnapshotsTestData = []string{
   878  	`{
   879  		"name": "AAPL $50.00 call",
   880  		"market_status": "open",
   881  		"ticker": "O:AAPL230512C00050000",
   882  		"type": "options",
   883  		"last_quote": {
   884  		  "ask": 123.1,
   885  		  "ask_size": 90,
   886  		  "bid": 122.95,
   887  		  "bid_size": 90,
   888  		  "last_updated": 1683731850932649728,
   889  		  "midpoint": 123.025,
   890  		  "timeframe": "REAL-TIME"
   891  		},
   892  		"last_trade": {},
   893  		"session": {},
   894  		"break_even_price": 173.025,
   895  		"details": {
   896  		  "contract_type": "call",
   897  		  "exercise_style": "american",
   898  		  "expiration_date": "2023-05-12",
   899  		  "shares_per_contract": 100,
   900  		  "strike_price": 50
   901  		},
   902  		"greeks": {},
   903  		"underlying_asset": {
   904  		  "change_to_break_even": -0.11,
   905  		  "last_updated": 1683732072879546553,
   906  		  "price": 173.135,
   907  		  "ticker": "AAPL",
   908  		  "timeframe": "REAL-TIME"
   909  		},
   910  		"fmv": 123.1,
   911  		"error": "",
   912  		"message": ""
   913  	}`,
   914  	`{
   915  		"name": "META $20.00 call",
   916  		"market_status": "open",
   917  		"ticker": "O:META230512C00020000",
   918  		"type": "options",
   919  		"last_quote": {},
   920  		"last_trade": {
   921  		  "sip_timestamp": 1682970890371000000,
   922  		  "conditions": [
   923  			209
   924  		  ],
   925  		  "price": 223.75,
   926  		  "size": 1,
   927  		  "exchange": 302,
   928  		  "timeframe": "REAL-TIME"
   929  		},
   930  		"session": {},
   931  		"details": {
   932  		  "contract_type": "call",
   933  		  "exercise_style": "american",
   934  		  "expiration_date": "2023-05-12",
   935  		  "shares_per_contract": 100,
   936  		  "strike_price": 20
   937  		},
   938  		"greeks": {},
   939  		"underlying_asset": {
   940  		  "last_updated": 1683731579449632715,
   941  		  "price": 232.37,
   942  		  "ticker": "META",
   943  		  "timeframe": "REAL-TIME"
   944  		},
   945  		"fmv": 200.2,
   946  		"error": "",
   947  		"message": ""
   948  	}`,
   949  	`{
   950  		"name": "F $5.00 call",
   951  		"market_status": "open",
   952  		"ticker": "O:F230512C00005000",
   953  		"type": "options",
   954  		"last_quote": {},
   955  		"last_trade": {
   956  		  "sip_timestamp": 1683316735432000000,
   957  		  "conditions": [
   958  			232
   959  		  ],
   960  		  "price": 6.97,
   961  		  "size": 1,
   962  		  "exchange": 312,
   963  		  "timeframe": "REAL-TIME"
   964  		},
   965  		"session": {},
   966  		"details": {
   967  		  "contract_type": "call",
   968  		  "exercise_style": "american",
   969  		  "expiration_date": "2023-05-12",
   970  		  "shares_per_contract": 100,
   971  		  "strike_price": 5
   972  		},
   973  		"greeks": {},
   974  		"underlying_asset": {
   975  		  "last_updated": 1683732072773028096,
   976  		  "price": 11.93,
   977  		  "ticker": "F",
   978  		  "timeframe": "REAL-TIME"
   979  		},
   980  		"fmv": 6.97
   981  	}`,
   982  }
   983  
   984  var cryptoSnapshotsTestData = []string{
   985  	`{
   986  		"market_status": "open",
   987  		"name": "Bitcoin - United States Dollar",
   988  		"ticker": "X:BTCUSD",
   989  		"type": "crypto",
   990  		"session": {
   991  		  "change": -181,
   992  		  "change_percent": -0.661,
   993  		  "close": 27236.1,
   994  		  "high": 27506,
   995  		  "low": 27010,
   996  		  "open": 27402.3,
   997  		  "volume": 10012.03414028,
   998  		  "previous_close": 27400.74,
   999  		  "price": 27220
  1000  		},
  1001  		"last_trade": {
  1002  		  "participant_timestamp": 1684422449502000000,
  1003  		  "timeframe": "REAL-TIME",
  1004  		  "id": "285449387",
  1005  		  "price": 27220,
  1006  		  "exchange": 6,
  1007  		  "conditions": [
  1008  			1
  1009  		  ]
  1010  		},
  1011  		"fmv": 27220.3
  1012  	  }`,
  1013  	`{
  1014  		"market_status": "open",
  1015  		"name": "Ethereum - United States Dollar",
  1016  		"ticker": "X:ETHUSD",
  1017  		"type": "crypto",
  1018  		"session": {
  1019  		  "change": -5.53,
  1020  		  "change_percent": -0.304,
  1021  		  "close": 1817.14,
  1022  		  "high": 1833.4,
  1023  		  "low": 1802.26,
  1024  		  "open": 1823.8,
  1025  		  "volume": 47673.72258305,
  1026  		  "previous_close": 1821.84,
  1027  		  "price": 1816.31
  1028  		},
  1029  		"last_trade": {
  1030  		  "participant_timestamp": 1684422449301037000,
  1031  		  "timeframe": "REAL-TIME",
  1032  		  "id": "451453400",
  1033  		  "price": 1816.31,
  1034  		  "exchange": 1,
  1035  		  "conditions": [
  1036  			1
  1037  		  ]
  1038  		},
  1039  		"fmv": 1816.31
  1040  	  }`,
  1041  	`{
  1042  		"market_status": "open",
  1043  		"name": "Flow - United States Dollar",
  1044  		"ticker": "X:FLOWUSD",
  1045  		"type": "crypto",
  1046  		"session": {
  1047  		  "change": -0.006,
  1048  		  "change_percent": -0.759,
  1049  		  "close": 0.784,
  1050  		  "high": 0.793,
  1051  		  "low": 0.779,
  1052  		  "open": 0.791,
  1053  		  "volume": 89094.52551417,
  1054  		  "previous_close": 0.79,
  1055  		  "price": 0.784
  1056  		},
  1057  		"last_trade": {
  1058  		  "participant_timestamp": 1684422355917759000,
  1059  		  "timeframe": "REAL-TIME",
  1060  		  "id": "1224329",
  1061  		  "price": 0.784,
  1062  		  "exchange": 1,
  1063  		  "conditions": [
  1064  			1
  1065  		  ]
  1066  		},
  1067  		"fmv": 0.784
  1068  	  }`,
  1069  }
  1070  
  1071  var forexSnapshotTestData = []string{
  1072  	`{
  1073  		"market_status": "open",
  1074  		"name": "United States dollar - Canadian dollar",
  1075  		"ticker": "C:USDCAD",
  1076  		"type": "fx",
  1077  		"session": {
  1078  		  "change": 0.00518,
  1079  		  "change_percent": 0.385,
  1080  		  "close": 1.35178,
  1081  		  "high": 1.35244,
  1082  		  "low": 1.34479,
  1083  		  "open": 1.34667,
  1084  		  "volume": 133712,
  1085  		  "previous_close": 1.34667
  1086  		},
  1087  		"last_quote": {
  1088  		  "last_updated": 1684431002000000000,
  1089  		  "timeframe": "REAL-TIME",
  1090  		  "ask": 1.35191,
  1091  		  "bid": 1.35185,
  1092  		  "exchange": 48
  1093  		},
  1094  		"fmv": 1.35185
  1095  	}`,
  1096  	`{
  1097  		"market_status": "open",
  1098  		"name": "United States dollar - Euro",
  1099  		"ticker": "C:USDEUR",
  1100  		"type": "fx",
  1101  		"session": {
  1102  		  "change": 0.00612,
  1103  		  "change_percent": 0.663,
  1104  		  "close": 0.92877,
  1105  		  "high": 0.92913,
  1106  		  "low": 0.9215,
  1107  		  "open": 0.92266,
  1108  		  "volume": 58275,
  1109  		  "previous_close": 0.92265
  1110  		},
  1111  		"last_quote": {
  1112  		  "last_updated": 1684430998000000000,
  1113  		  "timeframe": "REAL-TIME",
  1114  		  "ask": 0.92883,
  1115  		  "bid": 0.92877,
  1116  		  "exchange": 48
  1117  		},
  1118  		"fmv": 0.92877
  1119  	}`,
  1120  	`{
  1121  		"market_status": "open",
  1122  		"name": "United States dollar - Australian dollar",
  1123  		"ticker": "C:USDAUD",
  1124  		"type": "fx",
  1125  		"session": {
  1126  		  "change": 0.0104,
  1127  		  "change_percent": 0.692,
  1128  		  "close": 1.512722,
  1129  		  "high": 1.51398,
  1130  		  "low": 1.4965803,
  1131  		  "open": 1.502449,
  1132  		  "volume": 148378,
  1133  		  "previous_close": 1.502449
  1134  		},
  1135  		"last_quote": {
  1136  		  "last_updated": 1684431002000000000,
  1137  		  "timeframe": "REAL-TIME",
  1138  		  "ask": 1.51297,
  1139  		  "bid": 1.51281,
  1140  		  "exchange": 48
  1141  		},
  1142  		"fmv": 1.51281
  1143  	}`,
  1144  }
  1145  
  1146  var indicesSnapshotTestData = []string{
  1147  	`{
  1148  		"value": 4191.9800000000005,
  1149  		"last_updated": 1684530171934000000,
  1150  		"timeframe": "REAL-TIME",
  1151  		"name": "Standard & Poor's 500",
  1152  		"ticker": "I:SPX",
  1153  		"market_status": "open",
  1154  		"type": "indices",
  1155  		"session": {
  1156  			"change": -9.09,
  1157  			"change_percent": -0.216,
  1158  			"close": 4180.7,
  1159  			"high": 4212.91,
  1160  			"low": 4180.2,
  1161  			"open": 4204.15,
  1162  			"previous_close": 4201.07
  1163  		}
  1164  	}`,
  1165  	`{
  1166  		"value": 33426.63,
  1167  		"last_updated": 1684530172103533800,
  1168  		"timeframe": "REAL-TIME",
  1169  		"name": "Dow Jones Industrial Average",
  1170  		"ticker": "I:DJI",
  1171  		"market_status": "closed",
  1172  		"type": "indices",
  1173  		"session": {
  1174  			"change": -143,
  1175  			"change_percent": -0.426,
  1176  			"close": 33339.37,
  1177  			"high": 33652.9,
  1178  			"low": 33336.66,
  1179  			"open": 33582.95,
  1180  			"previous_close": 33569.5
  1181  		}
  1182  	}`,
  1183  	`{
  1184  		"value": 443.7,
  1185  		"last_updated": 1684528800037231600,
  1186  		"timeframe": "REAL-TIME",
  1187  		"name": "Dow Jones Americas Basic Materials Index",
  1188  		"ticker": "I:A1BSC",
  1189  		"market_status": "closed",
  1190  		"type": "indices",
  1191  		"session": {
  1192  			"change": -0.29,
  1193  			"change_percent": -0.0653,
  1194  			"close": 442.72,
  1195  			"high": 445.92,
  1196  			"low": 442.63,
  1197  			"open": 443.7,
  1198  			"previous_close": 443.99
  1199  		}
  1200  	}`,
  1201  }
  1202  
  1203  var partialSuccessWithStocksTestData = []string{
  1204  	`{
  1205  		"market_status": "late_trading",
  1206  		"name": "Apple Inc.",
  1207  		"session": {
  1208  			"change": -0.07,
  1209  			"change_percent": -0.0403,
  1210  			"close": 173.5,
  1211  			"early_trading_change": 0,
  1212  			"early_trading_change_percent": 0,
  1213  			"high": 173.85,
  1214  			"low": 172.11,
  1215  			"open": 172.48,
  1216  			"previous_close": 173.57,
  1217  			"price": 173.5,
  1218  			"volume": 50823329
  1219  		},
  1220  		"last_quote": {
  1221  			"ask": 173.34,
  1222  			"ask_size": 3,
  1223  			"bid": 173.32,
  1224  			"bid_size": 4,
  1225  			"last_updated": 1683577209434314800,
  1226  			"timeframe": "REAL-TIME"
  1227  		},
  1228  		"last_trade": {
  1229  			"conditions": [
  1230  				12,
  1231  				22
  1232  			],
  1233  			"exchange": 4,
  1234  			"id": "247862",
  1235  			"last_updated": 1683577205678289200,
  1236  			"price": 173.5,
  1237  			"size": 31535,
  1238  			"timeframe": "REAL-TIME"
  1239  		},
  1240  		"ticker": "AAPL",
  1241  		"type": "stocks",
  1242  		"fmv": 173.5
  1243  	}`,
  1244  	`{
  1245  		"error": "NOT_ENTITLED",
  1246  		"message": "Not entitled to this ticker.",
  1247  		"ticker": "APy"
  1248  	}`,
  1249  }