@@ -257,6 +257,153 @@ var _ = Describe("Monitor Coinbase", func() {
257257 })
258258 })
259259
260+ Describe ("SetSymbols" , func () {
261+ When ("there is a derivatives product (i.e. has an underlying asset)" , func () {
262+ When ("a symbol is already mapped to an underlying symbol" , func () {
263+ It ("should skip adding that symbol to the mapping between product ids and underlying product ids" , func () {
264+ // Initial response for first SetSymbols call
265+ server .AppendHandlers (
266+ ghttp .CombineHandlers (
267+ ghttp .VerifyRequest ("GET" , "/api/v3/brokerage/market/products" , "product_ids=BIT-31JAN25-CDE" ),
268+ ghttp .RespondWithJSONEncoded (http .StatusOK , unary.Response {
269+ Products : []unary.ResponseQuote {
270+ {
271+ Symbol : "BIT-31JAN25-CDE" ,
272+ ProductID : "BIT-31JAN25-CDE" ,
273+ ShortName : "Bitcoin Futures" ,
274+ Price : "60000.00" ,
275+ PriceChange24H : "5.00" ,
276+ Volume24H : "1000000.00" ,
277+ MarketState : "online" ,
278+ Currency : "USD" ,
279+ ExchangeName : "CDE" ,
280+ ProductType : "FUTURE" ,
281+ FutureProductDetails : unary.ResponseQuoteFutureProductDetails {
282+ ContractRootUnit : "BTC" ,
283+ GroupDescription : "Bitcoin January 2025 Future" ,
284+ ExpirationDate : "2025-01-31" ,
285+ ExpirationTimezone : "America/New_York" ,
286+ },
287+ },
288+ },
289+ }),
290+ ),
291+ // Response for getting all quotes after mapping is established
292+ ghttp .CombineHandlers (
293+ ghttp .VerifyRequest ("GET" , "/api/v3/brokerage/market/products" , "product_ids=BIT-31JAN25-CDE&product_ids=BTC-USD" ),
294+ ghttp .RespondWithJSONEncoded (http .StatusOK , unary.Response {
295+ Products : []unary.ResponseQuote {
296+ {
297+ Symbol : "BIT-31JAN25-CDE" ,
298+ ProductID : "BIT-31JAN25-CDE" ,
299+ ShortName : "Bitcoin Futures" ,
300+ Price : "60000.00" ,
301+ PriceChange24H : "5.00" ,
302+ Volume24H : "1000000.00" ,
303+ MarketState : "online" ,
304+ Currency : "USD" ,
305+ ExchangeName : "CDE" ,
306+ ProductType : "FUTURE" ,
307+ FutureProductDetails : unary.ResponseQuoteFutureProductDetails {
308+ ContractRootUnit : "BTC" ,
309+ GroupDescription : "Bitcoin January 2025 Future" ,
310+ ExpirationDate : "2025-01-31" ,
311+ ExpirationTimezone : "America/New_York" ,
312+ },
313+ },
314+ {
315+ Symbol : "BTC" ,
316+ ProductID : "BTC-USD" ,
317+ ShortName : "Bitcoin" ,
318+ Price : "50000.00" ,
319+ PriceChange24H : "5.00" ,
320+ Volume24H : "1000000.00" ,
321+ MarketState : "online" ,
322+ Currency : "USD" ,
323+ ExchangeName : "CBE" ,
324+ ProductType : "SPOT" ,
325+ },
326+ },
327+ }),
328+ ),
329+ )
330+
331+ // Verify that no additional API calls are made to get underlying symbols
332+ // when setting the same symbol again
333+ server .AppendHandlers (
334+ // Only expect the call to get all quotes, not the underlying mapping call
335+ ghttp .CombineHandlers (
336+ ghttp .VerifyRequest ("GET" , "/api/v3/brokerage/market/products" , "product_ids=BIT-31JAN25-CDE" ),
337+ ghttp .RespondWithJSONEncoded (http .StatusOK , unary.Response {
338+ Products : []unary.ResponseQuote {
339+ {
340+ Symbol : "BIT-31JAN25-CDE" ,
341+ ProductID : "BIT-31JAN25-CDE" ,
342+ ShortName : "Bitcoin Futures" ,
343+ Price : "60000.00" ,
344+ PriceChange24H : "5.00" ,
345+ Volume24H : "1000000.00" ,
346+ MarketState : "online" ,
347+ Currency : "USD" ,
348+ ExchangeName : "CDE" ,
349+ ProductType : "FUTURE" ,
350+ FutureProductDetails : unary.ResponseQuoteFutureProductDetails {
351+ ContractRootUnit : "BTC" ,
352+ GroupDescription : "Bitcoin January 2025 Future" ,
353+ ExpirationDate : "2025-01-31" ,
354+ ExpirationTimezone : "America/New_York" ,
355+ },
356+ },
357+ },
358+ }),
359+ ),
360+ )
361+
362+ monitor := monitorPriceCoinbase .NewMonitorPriceCoinbase (monitorPriceCoinbase.Config {
363+ UnaryURL : server .URL (),
364+ Ctx : context .Background (),
365+ ChanRequestCurrencyRates : make (chan []string , 1 ),
366+ ChanUpdateCurrencyRates : make (chan c.CurrencyRates , 1 ),
367+ })
368+
369+ // First call to SetSymbols establishes the mapping
370+ err := monitor .SetSymbols ([]string {"BIT-31JAN25-CDE" }, 0 )
371+ Expect (err ).NotTo (HaveOccurred ())
372+
373+ // Second call to SetSymbols should skip getting underlying symbols
374+ err = monitor .SetSymbols ([]string {"BIT-31JAN25-CDE" }, 1 )
375+ Expect (err ).NotTo (HaveOccurred ())
376+
377+ // Verify that all server handlers were called as expected
378+ Expect (server .ReceivedRequests ()).To (HaveLen (3 ))
379+ })
380+ })
381+
382+ When ("there is an error making the request to retrieve the underlying product ids" , func () {
383+ It ("should return an error" , func () {
384+ server .RouteToHandler ("GET" , "/api/v3/brokerage/market/products" ,
385+ ghttp .CombineHandlers (
386+ ghttp .VerifyRequest ("GET" , "/api/v3/brokerage/market/products" , "product_ids=BIT-31JAN25-CDE" ),
387+ ghttp .RespondWith (http .StatusInternalServerError , "network error" ),
388+ ),
389+ )
390+
391+ monitor := monitorPriceCoinbase .NewMonitorPriceCoinbase (monitorPriceCoinbase.Config {
392+ UnaryURL : server .URL (),
393+ Ctx : context .Background (),
394+ ChanRequestCurrencyRates : make (chan []string , 1 ),
395+ ChanUpdateCurrencyRates : make (chan c.CurrencyRates , 1 ),
396+ })
397+
398+ err := monitor .SetSymbols ([]string {"BIT-31JAN25-CDE" }, 0 )
399+ Expect (err ).To (HaveOccurred ())
400+ Expect (err .Error ()).To (ContainSubstring ("request failed with status 500" ))
401+ })
402+ })
403+
404+ })
405+ })
406+
260407 Describe ("Start" , func () {
261408 It ("should start the monitor" , func () {
262409 monitor := monitorPriceCoinbase .NewMonitorPriceCoinbase (monitorPriceCoinbase.Config {
@@ -871,6 +1018,209 @@ var _ = Describe("Monitor Coinbase", func() {
8711018 })
8721019 })
8731020 })
1021+
1022+ When ("there is a currency rate update" , func () {
1023+ It ("should replace the currency rate cache" , func () {
1024+ var err error
1025+ var outputQuote c.AssetQuote
1026+
1027+ // Set up initial server response for asset quotes
1028+ server .RouteToHandler ("GET" , "/api/v3/brokerage/market/products" , func (w http.ResponseWriter , r * http.Request ) {
1029+ query := r .URL .Query ()["product_ids" ]
1030+
1031+ if len (query ) == 1 && query [0 ] == "BIT-31JAN25-CDE" {
1032+ json .NewEncoder (w ).Encode (unary.Response {
1033+ Products : []unary.ResponseQuote {
1034+ {
1035+ Symbol : "BIT-31JAN25-CDE" ,
1036+ ProductID : "BIT-31JAN25-CDE" ,
1037+ ShortName : "Bitcoin Futures" ,
1038+ Price : "60000.00" ,
1039+ PriceChange24H : "5.00" ,
1040+ Volume24H : "1000000.00" ,
1041+ MarketState : "online" ,
1042+ Currency : "USD" ,
1043+ ExchangeName : "CDE" ,
1044+ ProductType : "FUTURE" ,
1045+ FutureProductDetails : unary.ResponseQuoteFutureProductDetails {
1046+ ContractRootUnit : "BTC" ,
1047+ GroupDescription : "Bitcoin January 2025 Future" ,
1048+ ExpirationDate : "2025-01-31" ,
1049+ ExpirationTimezone : "America/New_York" ,
1050+ },
1051+ },
1052+ },
1053+ })
1054+ return
1055+ }
1056+ if len (query ) == 2 && ((query [0 ] == "BTC-USD" && query [1 ] == "BIT-31JAN25-CDE" ) || (query [0 ] == "BIT-31JAN25-CDE" && query [1 ] == "BTC-USD" )) {
1057+ json .NewEncoder (w ).Encode (unary.Response {
1058+ Products : []unary.ResponseQuote {
1059+ {
1060+ Symbol : "BTC" ,
1061+ ProductID : "BTC-USD" ,
1062+ ShortName : "Bitcoin" ,
1063+ Price : "50000.00" ,
1064+ PriceChange24H : "2.5" ,
1065+ Volume24H : "1000000.00" ,
1066+ MarketState : "online" ,
1067+ Currency : "USD" ,
1068+ ExchangeName : "CBE" ,
1069+ ProductType : "SPOT" ,
1070+ },
1071+ {
1072+ Symbol : "BIT-31JAN25-CDE" ,
1073+ ProductID : "BIT-31JAN25-CDE" ,
1074+ ShortName : "Bitcoin Futures" ,
1075+ Price : "60000.00" ,
1076+ PriceChange24H : "5.00" ,
1077+ Volume24H : "1000000.00" ,
1078+ MarketState : "online" ,
1079+ Currency : "USD" ,
1080+ ExchangeName : "CDE" ,
1081+ ProductType : "FUTURE" ,
1082+ FutureProductDetails : unary.ResponseQuoteFutureProductDetails {
1083+ ContractRootUnit : "BTC" ,
1084+ GroupDescription : "Bitcoin January 2025 Future" ,
1085+ ExpirationDate : "2025-01-31" ,
1086+ ExpirationTimezone : "America/New_York" ,
1087+ },
1088+ },
1089+ },
1090+ })
1091+ return
1092+ }
1093+ w .WriteHeader (http .StatusNotFound )
1094+ })
1095+
1096+ // Create channels for currency rate updates and asset quote updates
1097+ currencyRatesChan := make (chan c.CurrencyRates , 1 )
1098+ updateChan := make (chan c.MessageUpdate [c.AssetQuote ], 10 )
1099+
1100+ // Create and start the monitor
1101+ monitor := monitorPriceCoinbase .NewMonitorPriceCoinbase (monitorPriceCoinbase.Config {
1102+ UnaryURL : server .URL (),
1103+ ChanUpdateAssetQuote : updateChan ,
1104+ Ctx : context .Background (),
1105+ ChanRequestCurrencyRates : make (chan []string , 1 ),
1106+ ChanUpdateCurrencyRates : currencyRatesChan ,
1107+ }, monitorPriceCoinbase .WithRefreshInterval (100 * time .Millisecond ))
1108+
1109+ monitor .SetSymbols ([]string {"BIT-31JAN25-CDE" }, 0 )
1110+ err = monitor .Start ()
1111+
1112+ // Send currency rates update
1113+ currencyRates := c.CurrencyRates {
1114+ "USD" : c.CurrencyRate {
1115+ FromCurrency : "USD" ,
1116+ ToCurrency : "EUR" ,
1117+ Rate : 0.85 ,
1118+ },
1119+ }
1120+ currencyRatesChan <- currencyRates
1121+
1122+ Expect (err ).NotTo (HaveOccurred ())
1123+
1124+ // Wait for and verify the asset quote update with new currency rate
1125+ Eventually (func () float64 {
1126+ select {
1127+ case <- time .After (200 * time .Millisecond ):
1128+ quotes , err := monitor .GetAssetQuotes ()
1129+
1130+ if err != nil {
1131+ return - 1.0
1132+ }
1133+
1134+ outputQuote = quotes [0 ]
1135+
1136+ return quotes [0 ].Currency .Rate
1137+ }
1138+ }, 2 * time .Second ).Should (Equal (0.85 ))
1139+
1140+ Expect (outputQuote .Currency .FromCurrencyCode ).To (Equal ("USD" ))
1141+ Expect (outputQuote .Currency .ToCurrencyCode ).To (Equal ("EUR" ))
1142+
1143+ monitor .Stop ()
1144+ })
1145+
1146+ When ("there is an error getting asset quotes and replacing the cache after new currency rates are recieved" , func () {
1147+ It ("should send a message to the error channel" , func () {
1148+ // Set up initial server response for asset quotes
1149+ server .RouteToHandler ("GET" , "/api/v3/brokerage/market/products" ,
1150+ ghttp .CombineHandlers (
1151+ ghttp .VerifyRequest ("GET" , "/api/v3/brokerage/market/products" , "product_ids=BTC-USD" ),
1152+ ghttp .RespondWithJSONEncoded (http .StatusOK , unary.Response {
1153+ Products : []unary.ResponseQuote {
1154+ {
1155+ Symbol : "BTC" ,
1156+ ProductID : "BTC-USD" ,
1157+ ShortName : "Bitcoin" ,
1158+ Price : "50000.00" ,
1159+ PriceChange24H : "2.5" ,
1160+ Volume24H : "1000000.00" ,
1161+ MarketState : "online" ,
1162+ Currency : "USD" ,
1163+ ExchangeName : "CBE" ,
1164+ ProductType : "SPOT" ,
1165+ },
1166+ },
1167+ }),
1168+ ),
1169+ )
1170+
1171+ // Create channels for updates and errors
1172+ currencyRatesChan := make (chan c.CurrencyRates , 1 )
1173+ errorChan := make (chan error , 1 )
1174+
1175+ // Create and start the monitor
1176+ monitor := monitorPriceCoinbase .NewMonitorPriceCoinbase (monitorPriceCoinbase.Config {
1177+ UnaryURL : server .URL (),
1178+ ChanError : errorChan ,
1179+ Ctx : context .Background (),
1180+ ChanRequestCurrencyRates : make (chan []string , 1 ),
1181+ ChanUpdateCurrencyRates : currencyRatesChan ,
1182+ }, monitorPriceCoinbase .WithRefreshInterval (100 * time .Millisecond ))
1183+
1184+ monitor .SetSymbols ([]string {"BTC-USD" }, 0 )
1185+ monitor .Start ()
1186+
1187+ // Set up error response for the subsequent asset quote request
1188+ server .RouteToHandler ("GET" , "/api/v3/brokerage/market/products" ,
1189+ ghttp .CombineHandlers (
1190+ ghttp .VerifyRequest ("GET" , "/api/v3/brokerage/market/products" , "product_ids=BTC-USD" ),
1191+ ghttp .RespondWith (http .StatusInternalServerError , "Internal Server Error" ),
1192+ ),
1193+ )
1194+
1195+ // Send currency rates update to trigger asset quote refresh
1196+ currencyRates := c.CurrencyRates {
1197+ "USD" : c.CurrencyRate {
1198+ FromCurrency : "USD" ,
1199+ ToCurrency : "EUR" ,
1200+ Rate : 0.85 ,
1201+ },
1202+ }
1203+ currencyRatesChan <- currencyRates
1204+
1205+ // Verify that an error is sent to the error channel
1206+ var err error
1207+ Eventually (func () error {
1208+ select {
1209+ case err = <- errorChan :
1210+ return err
1211+ default :
1212+ return nil
1213+ }
1214+ }, 2 * time .Second ).Should (HaveOccurred ())
1215+
1216+ Expect (err .Error ()).To (ContainSubstring ("request failed with status 500" ))
1217+
1218+ monitor .Stop ()
1219+ })
1220+ })
1221+
1222+ })
1223+
8741224 })
8751225
8761226 Describe ("Stop" , func () {
0 commit comments