diff --git a/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider.dart b/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider.dart index b121b0fe5..3913e5dd0 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider.dart @@ -42,10 +42,13 @@ class BinanceProvider implements IBinanceProvider { final response = await http.get(uri); if (response.statusCode == 200) { - return CoinOhlc.fromJson(jsonDecode(response.body) as List); + return CoinOhlc.fromJson( + jsonDecode(response.body) as List, + source: OhlcSource.binance, + ); } else { throw Exception( - 'Failed to load klines for \'$symbol\': ' + "Failed to load klines for '$symbol': " '${response.statusCode} ${response.body}', ); } @@ -113,7 +116,7 @@ class BinanceProvider implements IBinanceProvider { ); } else { throw Exception( - 'Failed to load 24hr ticker for \'$symbol\': ' + "Failed to load 24hr ticker for '$symbol': " '${response.statusCode} ${response.body}', ); } diff --git a/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart b/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart index b1ef4d1fd..c21e63aaf 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart @@ -161,7 +161,7 @@ class BinanceRepository implements CexRepository { endAt: endAt, limit: 1, ); - return Decimal.parse(ohlcData.ohlc.first.close.toString()); + return ohlcData.ohlc.first.closeDecimal; } @override @@ -206,10 +206,12 @@ class BinanceRepository implements CexRepository { map, ohlc, ) { - final date = DateTime.fromMillisecondsSinceEpoch(ohlc.closeTime); - map[DateTime(date.year, date.month, date.day)] = Decimal.parse( - ohlc.close.toString(), + final dateUtc = DateTime.fromMillisecondsSinceEpoch( + ohlc.closeTimeMs, + isUtc: true, ); + map[DateTime.utc(dateUtc.year, dateUtc.month, dateUtc.day)] = + ohlc.closeDecimal; return map; }); diff --git a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_cex_provider.dart b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_cex_provider.dart index dfed42104..c943388af 100644 --- a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_cex_provider.dart +++ b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_cex_provider.dart @@ -504,7 +504,7 @@ class CoinGeckoCexProvider implements ICoinGeckoProvider { return http.get(uri).then((http.Response response) { if (response.statusCode == 200) { final data = jsonDecode(response.body) as List; - return CoinOhlc.fromJson(data); + return CoinOhlc.fromJson(data, source: OhlcSource.coingecko); } else { throw Exception( 'Failed to load coin ohlc data: ${response.statusCode} ${response.body}', diff --git a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart index 59e671ac7..96135e18e 100644 --- a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart +++ b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart @@ -228,10 +228,12 @@ class CoinGeckoRepository implements CexRepository { map, ohlc, ) { - final date = DateTime.fromMillisecondsSinceEpoch(ohlc.closeTime); - map[DateTime(date.year, date.month, date.day)] = Decimal.parse( - ohlc.close.toString(), + final dateUtc = DateTime.fromMillisecondsSinceEpoch( + ohlc.closeTimeMs, + isUtc: true, ); + map[DateTime.utc(dateUtc.year, dateUtc.month, dateUtc.day)] = + ohlc.closeDecimal; return map; }); diff --git a/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.dart b/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.dart index 6af06aa8a..ce6ff9ce9 100644 --- a/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.dart +++ b/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.dart @@ -1,16 +1,26 @@ +import 'package:decimal/decimal.dart'; import 'package:equatable/equatable.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:komodo_cex_market_data/src/models/json_converters.dart'; + +part 'coin_ohlc.freezed.dart'; +part 'coin_ohlc.g.dart'; /// Represents Open-High-Low-Close (OHLC) data. class CoinOhlc extends Equatable { /// Creates a new instance of [CoinOhlc]. const CoinOhlc({required this.ohlc}); - /// Creates a new instance of [CoinOhlc] from a JSON array. - factory CoinOhlc.fromJson(List json) { + /// Creates a new instance of [CoinOhlc] from an array of klines. + factory CoinOhlc.fromJson(List json, {OhlcSource? source}) { return CoinOhlc( - ohlc: json - .map((dynamic kline) => Ohlc.fromJson(kline as List)) - .toList(), + ohlc: + json + .map( + (dynamic kline) => + Ohlc.fromKlineArray(kline as List, source: source), + ) + .toList(), ); } @@ -26,14 +36,12 @@ class CoinOhlc extends Equatable { ohlc: List.generate( (endAt.difference(startAt).inSeconds / intervalSeconds).ceil(), (index) { - final time = startAt.add( - Duration(seconds: index * intervalSeconds), - ); - return Ohlc( - high: constantValue, - low: constantValue, - open: constantValue, - close: constantValue, + final time = startAt.add(Duration(seconds: index * intervalSeconds)); + return Ohlc.binance( + high: Decimal.parse(constantValue.toString()), + low: Decimal.parse(constantValue.toString()), + open: Decimal.parse(constantValue.toString()), + close: Decimal.parse(constantValue.toString()), openTime: time.millisecondsSinceEpoch, closeTime: time.millisecondsSinceEpoch, ); @@ -42,11 +50,11 @@ class CoinOhlc extends Equatable { ); coinOhlc.ohlc.add( - Ohlc( - high: constantValue, - low: constantValue, - open: constantValue, - close: constantValue, + Ohlc.binance( + high: Decimal.parse(constantValue.toString()), + low: Decimal.parse(constantValue.toString()), + open: Decimal.parse(constantValue.toString()), + close: Decimal.parse(constantValue.toString()), openTime: endAt.millisecondsSinceEpoch, closeTime: endAt.millisecondsSinceEpoch, ), @@ -76,115 +84,170 @@ extension OhlcListToCoinOhlc on List { } } -/// Represents a Binance Kline (candlestick) data. -class Ohlc extends Equatable { - /// Creates a new instance of [Ohlc]. - const Ohlc({ - required this.openTime, - required this.open, - required this.high, - required this.low, - required this.close, - required this.closeTime, - this.volume, - this.quoteAssetVolume, - this.numberOfTrades, - this.takerBuyBaseAssetVolume, - this.takerBuyQuoteAssetVolume, - }); +/// Represents OHLC (Open-High-Low-Close) candlestick data from various sources. +/// +/// This is a union type that can represent data from different sources: +/// - [CoinGeckoOhlc]: OHLC data from CoinGecko API +/// - [BinanceOhlc]: Kline data from Binance API with additional trading information +@freezed +abstract class Ohlc with _$Ohlc { + /// Creates an OHLC data point from CoinGecko API format. + /// + /// CoinGecko provides basic OHLC data with a single timestamp. + @JsonSerializable(fieldRename: FieldRename.snake) + const factory Ohlc.coingecko({ + /// Unix timestamp in milliseconds for this data point + required int timestamp, + /// Opening price as a [Decimal] for precision + @DecimalConverter() required Decimal open, + /// Highest price reached during this period as a [Decimal] + @DecimalConverter() required Decimal high, + /// Lowest price reached during this period as a [Decimal] + @DecimalConverter() required Decimal low, + /// Closing price as a [Decimal] for precision + @DecimalConverter() required Decimal close, + }) = CoinGeckoOhlc; + + /// Creates a kline (candlestick) data point from Binance API format. + /// + /// Binance provides comprehensive trading data including volume and trade counts. + @JsonSerializable(fieldRename: FieldRename.snake) + const factory Ohlc.binance({ + /// Unix timestamp in milliseconds when this kline opened + required int openTime, + /// Opening price as a [Decimal] for precision + @DecimalConverter() required Decimal open, + /// Highest price reached during this kline as a [Decimal] + @DecimalConverter() required Decimal high, + /// Lowest price reached during this kline as a [Decimal] + @DecimalConverter() required Decimal low, + /// Closing price as a [Decimal] for precision + @DecimalConverter() required Decimal close, + /// Unix timestamp in milliseconds when this kline closed + required int closeTime, + /// Trading volume during this kline as a [Decimal] + @DecimalConverter() Decimal? volume, + /// Quote asset volume during this kline as a [Decimal] + @DecimalConverter() Decimal? quoteAssetVolume, + /// Number of trades executed during this kline + int? numberOfTrades, + /// Volume of the asset bought by takers during this kline as a [Decimal] + @DecimalConverter() Decimal? takerBuyBaseAssetVolume, + /// Quote asset volume of the asset bought by takers during this kline as a [Decimal] + @DecimalConverter() Decimal? takerBuyQuoteAssetVolume, + }) = BinanceOhlc; + + /// Creates an [Ohlc] instance from a JSON map. + factory Ohlc.fromJson(Map json) => _$OhlcFromJson(json); /// Creates a new instance of [Ohlc] from a JSON array. - factory Ohlc.fromJson(List json) { - return Ohlc( - openTime: json[0] as int, - open: double.parse(json[1] as String), - high: double.parse(json[2] as String), - low: double.parse(json[3] as String), - close: double.parse(json[4] as String), - volume: double.parse(json[5] as String), - closeTime: json[6] as int, - quoteAssetVolume: double.parse(json[7] as String), - numberOfTrades: json[8] as int, - takerBuyBaseAssetVolume: double.parse(json[9] as String), - takerBuyQuoteAssetVolume: double.parse(json[10] as String), + /// + /// The array format varies by source: + /// - CoinGecko: [timestamp, open, high, low, close] (5 elements) + /// - Binance: [openTime, open, high, low, close, volume, closeTime, quoteAssetVolume, numberOfTrades, takerBuyBaseAssetVolume, takerBuyQuoteAssetVolume] (11+ elements) + /// + /// If [source] is provided, it forces parsing in that format. + /// If [source] is null, the parser uses array length heuristics. + factory Ohlc.fromKlineArray(List json, {OhlcSource? source}) { + Decimal asDecimal(dynamic value) { + final dec = const DecimalConverter().fromJson(value); + if (dec == null) { + throw ArgumentError('Cannot convert value "$value" to Decimal'); + } + return dec; + } + + int asInt(dynamic value) { + if (value is int) return value; + if (value is double) return value.toInt(); + if (value is String) return int.parse(value); + throw ArgumentError('Cannot convert value "$value" to int'); + } + + // Prefer explicit source; fall back to heuristic by length + if (source == OhlcSource.coingecko || + (source == null && json.length == 5)) { + final ts = asInt(json[0]); + return Ohlc.coingecko( + timestamp: ts, + open: asDecimal(json[1]), + high: asDecimal(json[2]), + low: asDecimal(json[3]), + close: asDecimal(json[4]), + ); + } + + // Binance-like arrays have >= 11 elements + if (source == OhlcSource.binance || json.length >= 11) { + return Ohlc.binance( + openTime: asInt(json[0]), + open: asDecimal(json[1]), + high: asDecimal(json[2]), + low: asDecimal(json[3]), + close: asDecimal(json[4]), + volume: + json.length > 5 ? const DecimalConverter().fromJson(json[5]) : null, + closeTime: json.length > 6 ? asInt(json[6]) : asInt(json[0]), + quoteAssetVolume: + json.length > 7 ? const DecimalConverter().fromJson(json[7]) : null, + numberOfTrades: json.length > 8 ? asInt(json[8]) : null, + takerBuyBaseAssetVolume: + json.length > 9 ? const DecimalConverter().fromJson(json[9]) : null, + takerBuyQuoteAssetVolume: + json.length > 10 + ? const DecimalConverter().fromJson(json[10]) + : null, + ); + } + + throw ArgumentError( + 'Invalid OHLC array length: ${json.length}. Expected 5 (CoinGecko) or >=11 (Binance).', ); } +} - /// Converts the [Ohlc] object to a JSON array. - List toJson() { - return [ - openTime, - open, - high, - low, - close, - volume, - closeTime, - quoteAssetVolume, - numberOfTrades, - takerBuyBaseAssetVolume, - takerBuyQuoteAssetVolume, - ]; - } - - /// Converts the kline data into a JSON object like that returned in the previously used OHLC endpoint. - Map toMap() { - return { - 'timestamp': openTime, - 'open': open, - 'high': high, - 'low': low, - 'close': close, - 'volume': volume, - 'quote_volume': quoteAssetVolume, - }; - } - - /// The opening time of the kline as a Unix timestamp since epoch (UTC). - final int openTime; - - /// The opening price of the kline. - final double open; - - /// The highest price reached during the kline. - final double high; - - /// The lowest price reached during the kline. - final double low; - - /// The closing price of the kline. - final double close; - - /// The trading volume during the kline. - final double? volume; - - /// The closing time of the kline. - final int closeTime; - - /// The quote asset volume during the kline. - final double? quoteAssetVolume; - - /// The number of trades executed during the kline. - final int? numberOfTrades; - - /// The volume of the asset bought by takers during the kline. - final double? takerBuyBaseAssetVolume; - - /// The quote asset volume of the asset bought by takers during the kline. - final double? takerBuyQuoteAssetVolume; +/// Source hint for parsing OHLC arrays. +/// +/// Used to disambiguate between different API response formats when parsing +/// raw array data into [Ohlc] objects. +enum OhlcSource { + /// CoinGecko API format: 5-element arrays + coingecko, + /// Binance API format: 11+ element arrays + binance +} - @override - List get props => [ - openTime, - open, - high, - low, - close, - volume, - closeTime, - quoteAssetVolume, - numberOfTrades, - takerBuyBaseAssetVolume, - takerBuyQuoteAssetVolume, - ]; +/// Extension providing unified accessors for [Ohlc] data regardless of source. +/// +/// This extension normalizes the different field names and structures between +/// CoinGecko and Binance formats, providing consistent access patterns. +extension OhlcGetters on Ohlc { + /// Gets the opening time in milliseconds since epoch. + /// + /// For CoinGecko data, this returns the timestamp. + /// For Binance data, this returns the openTime. + int get openTimeMs => + map(coingecko: (c) => c.timestamp, binance: (b) => b.openTime); + + /// Gets the closing time in milliseconds since epoch. + /// + /// For CoinGecko data, this returns the timestamp (same as open time). + /// For Binance data, this returns the closeTime. + int get closeTimeMs => + map(coingecko: (c) => c.timestamp, binance: (b) => b.closeTime); + + /// Gets the opening price as a [Decimal] for precision. + Decimal get openDecimal => + map(coingecko: (c) => c.open, binance: (b) => b.open); + + /// Gets the highest price as a [Decimal] for precision. + Decimal get highDecimal => + map(coingecko: (c) => c.high, binance: (b) => b.high); + + /// Gets the lowest price as a [Decimal] for precision. + Decimal get lowDecimal => map(coingecko: (c) => c.low, binance: (b) => b.low); + + /// Gets the closing price as a [Decimal] for precision. + Decimal get closeDecimal => + map(coingecko: (c) => c.close, binance: (b) => b.close); } diff --git a/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.freezed.dart b/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.freezed.dart new file mode 100644 index 000000000..1ce75da59 --- /dev/null +++ b/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.freezed.dart @@ -0,0 +1,414 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'coin_ohlc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +Ohlc _$OhlcFromJson( + Map json +) { + switch (json['runtimeType']) { + case 'coingecko': + return CoinGeckoOhlc.fromJson( + json + ); + case 'binance': + return BinanceOhlc.fromJson( + json + ); + + default: + throw CheckedFromJsonException( + json, + 'runtimeType', + 'Ohlc', + 'Invalid union type "${json['runtimeType']}"!' +); + } + +} + +/// @nodoc +mixin _$Ohlc { + +@DecimalConverter() Decimal get open;@DecimalConverter() Decimal get high;@DecimalConverter() Decimal get low;@DecimalConverter() Decimal get close; +/// Create a copy of Ohlc +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$OhlcCopyWith get copyWith => _$OhlcCopyWithImpl(this as Ohlc, _$identity); + + /// Serializes this Ohlc to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Ohlc&&(identical(other.open, open) || other.open == open)&&(identical(other.high, high) || other.high == high)&&(identical(other.low, low) || other.low == low)&&(identical(other.close, close) || other.close == close)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,open,high,low,close); + +@override +String toString() { + return 'Ohlc(open: $open, high: $high, low: $low, close: $close)'; +} + + +} + +/// @nodoc +abstract mixin class $OhlcCopyWith<$Res> { + factory $OhlcCopyWith(Ohlc value, $Res Function(Ohlc) _then) = _$OhlcCopyWithImpl; +@useResult +$Res call({ +@DecimalConverter() Decimal open,@DecimalConverter() Decimal high,@DecimalConverter() Decimal low,@DecimalConverter() Decimal close +}); + + + + +} +/// @nodoc +class _$OhlcCopyWithImpl<$Res> + implements $OhlcCopyWith<$Res> { + _$OhlcCopyWithImpl(this._self, this._then); + + final Ohlc _self; + final $Res Function(Ohlc) _then; + +/// Create a copy of Ohlc +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? open = null,Object? high = null,Object? low = null,Object? close = null,}) { + return _then(_self.copyWith( +open: null == open ? _self.open : open // ignore: cast_nullable_to_non_nullable +as Decimal,high: null == high ? _self.high : high // ignore: cast_nullable_to_non_nullable +as Decimal,low: null == low ? _self.low : low // ignore: cast_nullable_to_non_nullable +as Decimal,close: null == close ? _self.close : close // ignore: cast_nullable_to_non_nullable +as Decimal, + )); +} + +} + + +/// Adds pattern-matching-related methods to [Ohlc]. +extension OhlcPatterns on Ohlc { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( CoinGeckoOhlc value)? coingecko,TResult Function( BinanceOhlc value)? binance,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case CoinGeckoOhlc() when coingecko != null: +return coingecko(_that);case BinanceOhlc() when binance != null: +return binance(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( CoinGeckoOhlc value) coingecko,required TResult Function( BinanceOhlc value) binance,}){ +final _that = this; +switch (_that) { +case CoinGeckoOhlc(): +return coingecko(_that);case BinanceOhlc(): +return binance(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( CoinGeckoOhlc value)? coingecko,TResult? Function( BinanceOhlc value)? binance,}){ +final _that = this; +switch (_that) { +case CoinGeckoOhlc() when coingecko != null: +return coingecko(_that);case BinanceOhlc() when binance != null: +return binance(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( int timestamp, @DecimalConverter() Decimal open, @DecimalConverter() Decimal high, @DecimalConverter() Decimal low, @DecimalConverter() Decimal close)? coingecko,TResult Function( int openTime, @DecimalConverter() Decimal open, @DecimalConverter() Decimal high, @DecimalConverter() Decimal low, @DecimalConverter() Decimal close, int closeTime, @DecimalConverter() Decimal? volume, @DecimalConverter() Decimal? quoteAssetVolume, int? numberOfTrades, @DecimalConverter() Decimal? takerBuyBaseAssetVolume, @DecimalConverter() Decimal? takerBuyQuoteAssetVolume)? binance,required TResult orElse(),}) {final _that = this; +switch (_that) { +case CoinGeckoOhlc() when coingecko != null: +return coingecko(_that.timestamp,_that.open,_that.high,_that.low,_that.close);case BinanceOhlc() when binance != null: +return binance(_that.openTime,_that.open,_that.high,_that.low,_that.close,_that.closeTime,_that.volume,_that.quoteAssetVolume,_that.numberOfTrades,_that.takerBuyBaseAssetVolume,_that.takerBuyQuoteAssetVolume);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( int timestamp, @DecimalConverter() Decimal open, @DecimalConverter() Decimal high, @DecimalConverter() Decimal low, @DecimalConverter() Decimal close) coingecko,required TResult Function( int openTime, @DecimalConverter() Decimal open, @DecimalConverter() Decimal high, @DecimalConverter() Decimal low, @DecimalConverter() Decimal close, int closeTime, @DecimalConverter() Decimal? volume, @DecimalConverter() Decimal? quoteAssetVolume, int? numberOfTrades, @DecimalConverter() Decimal? takerBuyBaseAssetVolume, @DecimalConverter() Decimal? takerBuyQuoteAssetVolume) binance,}) {final _that = this; +switch (_that) { +case CoinGeckoOhlc(): +return coingecko(_that.timestamp,_that.open,_that.high,_that.low,_that.close);case BinanceOhlc(): +return binance(_that.openTime,_that.open,_that.high,_that.low,_that.close,_that.closeTime,_that.volume,_that.quoteAssetVolume,_that.numberOfTrades,_that.takerBuyBaseAssetVolume,_that.takerBuyQuoteAssetVolume);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( int timestamp, @DecimalConverter() Decimal open, @DecimalConverter() Decimal high, @DecimalConverter() Decimal low, @DecimalConverter() Decimal close)? coingecko,TResult? Function( int openTime, @DecimalConverter() Decimal open, @DecimalConverter() Decimal high, @DecimalConverter() Decimal low, @DecimalConverter() Decimal close, int closeTime, @DecimalConverter() Decimal? volume, @DecimalConverter() Decimal? quoteAssetVolume, int? numberOfTrades, @DecimalConverter() Decimal? takerBuyBaseAssetVolume, @DecimalConverter() Decimal? takerBuyQuoteAssetVolume)? binance,}) {final _that = this; +switch (_that) { +case CoinGeckoOhlc() when coingecko != null: +return coingecko(_that.timestamp,_that.open,_that.high,_that.low,_that.close);case BinanceOhlc() when binance != null: +return binance(_that.openTime,_that.open,_that.high,_that.low,_that.close,_that.closeTime,_that.volume,_that.quoteAssetVolume,_that.numberOfTrades,_that.takerBuyBaseAssetVolume,_that.takerBuyQuoteAssetVolume);case _: + return null; + +} +} + +} + +/// @nodoc + +@JsonSerializable(fieldRename: FieldRename.snake) +class CoinGeckoOhlc implements Ohlc { + const CoinGeckoOhlc({required this.timestamp, @DecimalConverter() required this.open, @DecimalConverter() required this.high, @DecimalConverter() required this.low, @DecimalConverter() required this.close, final String? $type}): $type = $type ?? 'coingecko'; + factory CoinGeckoOhlc.fromJson(Map json) => _$CoinGeckoOhlcFromJson(json); + + final int timestamp; +@override@DecimalConverter() final Decimal open; +@override@DecimalConverter() final Decimal high; +@override@DecimalConverter() final Decimal low; +@override@DecimalConverter() final Decimal close; + +@JsonKey(name: 'runtimeType') +final String $type; + + +/// Create a copy of Ohlc +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CoinGeckoOhlcCopyWith get copyWith => _$CoinGeckoOhlcCopyWithImpl(this, _$identity); + +@override +Map toJson() { + return _$CoinGeckoOhlcToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CoinGeckoOhlc&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.open, open) || other.open == open)&&(identical(other.high, high) || other.high == high)&&(identical(other.low, low) || other.low == low)&&(identical(other.close, close) || other.close == close)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,timestamp,open,high,low,close); + +@override +String toString() { + return 'Ohlc.coingecko(timestamp: $timestamp, open: $open, high: $high, low: $low, close: $close)'; +} + + +} + +/// @nodoc +abstract mixin class $CoinGeckoOhlcCopyWith<$Res> implements $OhlcCopyWith<$Res> { + factory $CoinGeckoOhlcCopyWith(CoinGeckoOhlc value, $Res Function(CoinGeckoOhlc) _then) = _$CoinGeckoOhlcCopyWithImpl; +@override @useResult +$Res call({ + int timestamp,@DecimalConverter() Decimal open,@DecimalConverter() Decimal high,@DecimalConverter() Decimal low,@DecimalConverter() Decimal close +}); + + + + +} +/// @nodoc +class _$CoinGeckoOhlcCopyWithImpl<$Res> + implements $CoinGeckoOhlcCopyWith<$Res> { + _$CoinGeckoOhlcCopyWithImpl(this._self, this._then); + + final CoinGeckoOhlc _self; + final $Res Function(CoinGeckoOhlc) _then; + +/// Create a copy of Ohlc +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? timestamp = null,Object? open = null,Object? high = null,Object? low = null,Object? close = null,}) { + return _then(CoinGeckoOhlc( +timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable +as int,open: null == open ? _self.open : open // ignore: cast_nullable_to_non_nullable +as Decimal,high: null == high ? _self.high : high // ignore: cast_nullable_to_non_nullable +as Decimal,low: null == low ? _self.low : low // ignore: cast_nullable_to_non_nullable +as Decimal,close: null == close ? _self.close : close // ignore: cast_nullable_to_non_nullable +as Decimal, + )); +} + + +} + +/// @nodoc + +@JsonSerializable(fieldRename: FieldRename.snake) +class BinanceOhlc implements Ohlc { + const BinanceOhlc({required this.openTime, @DecimalConverter() required this.open, @DecimalConverter() required this.high, @DecimalConverter() required this.low, @DecimalConverter() required this.close, required this.closeTime, @DecimalConverter() this.volume, @DecimalConverter() this.quoteAssetVolume, this.numberOfTrades, @DecimalConverter() this.takerBuyBaseAssetVolume, @DecimalConverter() this.takerBuyQuoteAssetVolume, final String? $type}): $type = $type ?? 'binance'; + factory BinanceOhlc.fromJson(Map json) => _$BinanceOhlcFromJson(json); + + final int openTime; +@override@DecimalConverter() final Decimal open; +@override@DecimalConverter() final Decimal high; +@override@DecimalConverter() final Decimal low; +@override@DecimalConverter() final Decimal close; + final int closeTime; +@DecimalConverter() final Decimal? volume; +@DecimalConverter() final Decimal? quoteAssetVolume; + final int? numberOfTrades; +@DecimalConverter() final Decimal? takerBuyBaseAssetVolume; +@DecimalConverter() final Decimal? takerBuyQuoteAssetVolume; + +@JsonKey(name: 'runtimeType') +final String $type; + + +/// Create a copy of Ohlc +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BinanceOhlcCopyWith get copyWith => _$BinanceOhlcCopyWithImpl(this, _$identity); + +@override +Map toJson() { + return _$BinanceOhlcToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BinanceOhlc&&(identical(other.openTime, openTime) || other.openTime == openTime)&&(identical(other.open, open) || other.open == open)&&(identical(other.high, high) || other.high == high)&&(identical(other.low, low) || other.low == low)&&(identical(other.close, close) || other.close == close)&&(identical(other.closeTime, closeTime) || other.closeTime == closeTime)&&(identical(other.volume, volume) || other.volume == volume)&&(identical(other.quoteAssetVolume, quoteAssetVolume) || other.quoteAssetVolume == quoteAssetVolume)&&(identical(other.numberOfTrades, numberOfTrades) || other.numberOfTrades == numberOfTrades)&&(identical(other.takerBuyBaseAssetVolume, takerBuyBaseAssetVolume) || other.takerBuyBaseAssetVolume == takerBuyBaseAssetVolume)&&(identical(other.takerBuyQuoteAssetVolume, takerBuyQuoteAssetVolume) || other.takerBuyQuoteAssetVolume == takerBuyQuoteAssetVolume)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,openTime,open,high,low,close,closeTime,volume,quoteAssetVolume,numberOfTrades,takerBuyBaseAssetVolume,takerBuyQuoteAssetVolume); + +@override +String toString() { + return 'Ohlc.binance(openTime: $openTime, open: $open, high: $high, low: $low, close: $close, closeTime: $closeTime, volume: $volume, quoteAssetVolume: $quoteAssetVolume, numberOfTrades: $numberOfTrades, takerBuyBaseAssetVolume: $takerBuyBaseAssetVolume, takerBuyQuoteAssetVolume: $takerBuyQuoteAssetVolume)'; +} + + +} + +/// @nodoc +abstract mixin class $BinanceOhlcCopyWith<$Res> implements $OhlcCopyWith<$Res> { + factory $BinanceOhlcCopyWith(BinanceOhlc value, $Res Function(BinanceOhlc) _then) = _$BinanceOhlcCopyWithImpl; +@override @useResult +$Res call({ + int openTime,@DecimalConverter() Decimal open,@DecimalConverter() Decimal high,@DecimalConverter() Decimal low,@DecimalConverter() Decimal close, int closeTime,@DecimalConverter() Decimal? volume,@DecimalConverter() Decimal? quoteAssetVolume, int? numberOfTrades,@DecimalConverter() Decimal? takerBuyBaseAssetVolume,@DecimalConverter() Decimal? takerBuyQuoteAssetVolume +}); + + + + +} +/// @nodoc +class _$BinanceOhlcCopyWithImpl<$Res> + implements $BinanceOhlcCopyWith<$Res> { + _$BinanceOhlcCopyWithImpl(this._self, this._then); + + final BinanceOhlc _self; + final $Res Function(BinanceOhlc) _then; + +/// Create a copy of Ohlc +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? openTime = null,Object? open = null,Object? high = null,Object? low = null,Object? close = null,Object? closeTime = null,Object? volume = freezed,Object? quoteAssetVolume = freezed,Object? numberOfTrades = freezed,Object? takerBuyBaseAssetVolume = freezed,Object? takerBuyQuoteAssetVolume = freezed,}) { + return _then(BinanceOhlc( +openTime: null == openTime ? _self.openTime : openTime // ignore: cast_nullable_to_non_nullable +as int,open: null == open ? _self.open : open // ignore: cast_nullable_to_non_nullable +as Decimal,high: null == high ? _self.high : high // ignore: cast_nullable_to_non_nullable +as Decimal,low: null == low ? _self.low : low // ignore: cast_nullable_to_non_nullable +as Decimal,close: null == close ? _self.close : close // ignore: cast_nullable_to_non_nullable +as Decimal,closeTime: null == closeTime ? _self.closeTime : closeTime // ignore: cast_nullable_to_non_nullable +as int,volume: freezed == volume ? _self.volume : volume // ignore: cast_nullable_to_non_nullable +as Decimal?,quoteAssetVolume: freezed == quoteAssetVolume ? _self.quoteAssetVolume : quoteAssetVolume // ignore: cast_nullable_to_non_nullable +as Decimal?,numberOfTrades: freezed == numberOfTrades ? _self.numberOfTrades : numberOfTrades // ignore: cast_nullable_to_non_nullable +as int?,takerBuyBaseAssetVolume: freezed == takerBuyBaseAssetVolume ? _self.takerBuyBaseAssetVolume : takerBuyBaseAssetVolume // ignore: cast_nullable_to_non_nullable +as Decimal?,takerBuyQuoteAssetVolume: freezed == takerBuyQuoteAssetVolume ? _self.takerBuyQuoteAssetVolume : takerBuyQuoteAssetVolume // ignore: cast_nullable_to_non_nullable +as Decimal?, + )); +} + + +} + +// dart format on diff --git a/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.g.dart b/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.g.dart new file mode 100644 index 000000000..f95bd7f15 --- /dev/null +++ b/packages/komodo_cex_market_data/lib/src/models/coin_ohlc.g.dart @@ -0,0 +1,70 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'coin_ohlc.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CoinGeckoOhlc _$CoinGeckoOhlcFromJson(Map json) => + CoinGeckoOhlc( + timestamp: (json['timestamp'] as num).toInt(), + open: Decimal.fromJson(json['open'] as String), + high: Decimal.fromJson(json['high'] as String), + low: Decimal.fromJson(json['low'] as String), + close: Decimal.fromJson(json['close'] as String), + $type: json['runtimeType'] as String?, + ); + +Map _$CoinGeckoOhlcToJson(CoinGeckoOhlc instance) => + { + 'timestamp': instance.timestamp, + 'open': instance.open, + 'high': instance.high, + 'low': instance.low, + 'close': instance.close, + 'runtimeType': instance.$type, + }; + +BinanceOhlc _$BinanceOhlcFromJson(Map json) => BinanceOhlc( + openTime: (json['open_time'] as num).toInt(), + open: Decimal.fromJson(json['open'] as String), + high: Decimal.fromJson(json['high'] as String), + low: Decimal.fromJson(json['low'] as String), + close: Decimal.fromJson(json['close'] as String), + closeTime: (json['close_time'] as num).toInt(), + volume: const DecimalConverter().fromJson(json['volume']), + quoteAssetVolume: const DecimalConverter().fromJson( + json['quote_asset_volume'], + ), + numberOfTrades: (json['number_of_trades'] as num?)?.toInt(), + takerBuyBaseAssetVolume: const DecimalConverter().fromJson( + json['taker_buy_base_asset_volume'], + ), + takerBuyQuoteAssetVolume: const DecimalConverter().fromJson( + json['taker_buy_quote_asset_volume'], + ), + $type: json['runtimeType'] as String?, +); + +Map _$BinanceOhlcToJson(BinanceOhlc instance) => + { + 'open_time': instance.openTime, + 'open': instance.open, + 'high': instance.high, + 'low': instance.low, + 'close': instance.close, + 'close_time': instance.closeTime, + 'volume': const DecimalConverter().toJson(instance.volume), + 'quote_asset_volume': const DecimalConverter().toJson( + instance.quoteAssetVolume, + ), + 'number_of_trades': instance.numberOfTrades, + 'taker_buy_base_asset_volume': const DecimalConverter().toJson( + instance.takerBuyBaseAssetVolume, + ), + 'taker_buy_quote_asset_volume': const DecimalConverter().toJson( + instance.takerBuyQuoteAssetVolume, + ), + 'runtimeType': instance.$type, + }; diff --git a/packages/komodo_cex_market_data/lib/src/models/json_converters.dart b/packages/komodo_cex_market_data/lib/src/models/json_converters.dart index c1ce0134e..89a0edb33 100644 --- a/packages/komodo_cex_market_data/lib/src/models/json_converters.dart +++ b/packages/komodo_cex_market_data/lib/src/models/json_converters.dart @@ -1,10 +1,22 @@ import 'package:decimal/decimal.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -/// Custom converter for Decimal type +/// Custom JSON converter for [Decimal] type with null-safe handling. +/// +/// This converter handles various input formats including strings, numbers, +/// integers, and doubles, converting them to [Decimal] for high-precision +/// arithmetic operations. Returns null for invalid or null inputs. class DecimalConverter implements JsonConverter { const DecimalConverter(); + /// Converts JSON value to [Decimal]. + /// + /// Supports conversion from: + /// - String: parsed directly as [Decimal] + /// - num, int, double: converted to string then parsed as [Decimal] + /// - Other types: converted to string then parsed + /// + /// Returns null for null inputs, empty strings, or parsing failures. @override Decimal? fromJson(dynamic json) { if (json == null) return null; @@ -31,24 +43,43 @@ class DecimalConverter implements JsonConverter { } } + /// Converts [Decimal] to JSON string representation. + /// + /// Returns null if the input [decimal] is null. @override String? toJson(Decimal? decimal) { return decimal?.toString(); } } -/// Custom converter for timestamp (Unix epoch in seconds) +/// Custom JSON converter for Unix timestamps in seconds to UTC [DateTime]. +/// +/// This converter handles Unix epoch timestamps (seconds since 1970-01-01 UTC) +/// and converts them to UTC [DateTime] objects. All converted dates are +/// explicitly set to UTC timezone to ensure consistency across different +/// system timezones. class TimestampConverter implements JsonConverter { const TimestampConverter(); - /// Converts Unix timestamp in seconds to DateTime + /// Converts Unix timestamp in seconds to UTC [DateTime]. + /// + /// Takes a Unix epoch timestamp (seconds since 1970-01-01 UTC) and + /// returns a [DateTime] object explicitly set to UTC timezone. + /// + /// Returns null if the input timestamp is null. @override DateTime? fromJson(int? json) { if (json == null) return null; - return DateTime.fromMillisecondsSinceEpoch(json * 1000); + return DateTime.fromMillisecondsSinceEpoch(json * 1000, isUtc: true); } - /// Converts DateTime to Unix timestamp in seconds + /// Converts [DateTime] to Unix timestamp in seconds. + /// + /// Takes a [DateTime] object and returns the Unix epoch timestamp + /// in seconds since 1970-01-01 UTC. The input [DateTime] timezone + /// is automatically handled by the conversion. + /// + /// Returns null if the input [dateTime] is null. @override int? toJson(DateTime? dateTime) { if (dateTime == null) return null; diff --git a/packages/komodo_cex_market_data/lib/src/sparkline_repository.dart b/packages/komodo_cex_market_data/lib/src/sparkline_repository.dart index 7a291fcc6..7f80a929b 100644 --- a/packages/komodo_cex_market_data/lib/src/sparkline_repository.dart +++ b/packages/komodo_cex_market_data/lib/src/sparkline_repository.dart @@ -135,7 +135,7 @@ class SparklineRepository with RepositoryFallbackMixin { startAt: startAt, endAt: endAt, ); - final data = ohlcData.ohlc.map((e) => e.close).toList(); + final data = ohlcData.ohlc.map((e) => e.closeDecimal.toDouble()).toList(); if (data.isEmpty) { _logger.fine('Empty OHLC data for $symbol from ${repo.runtimeType}'); throw StateError( @@ -182,7 +182,8 @@ class SparklineRepository with RepositoryFallbackMixin { endAt: endAt, intervalSeconds: interval, ); - final constantData = ohlcData.ohlc.map((e) => e.close).toList(); + final constantData = + ohlcData.ohlc.map((e) => e.closeDecimal.toDouble()).toList(); await _box!.put(symbol, { 'data': constantData, 'timestamp': DateTime.now().toIso8601String(), diff --git a/packages/komodo_cex_market_data/test/binance/binance_repository_test.dart b/packages/komodo_cex_market_data/test/binance/binance_repository_test.dart index c4550283b..19f5efb85 100644 --- a/packages/komodo_cex_market_data/test/binance/binance_repository_test.dart +++ b/packages/komodo_cex_market_data/test/binance/binance_repository_test.dart @@ -255,11 +255,11 @@ void main() { test('should use mapped currency in getCoinFiatPrice', () async { final mockOhlc = CoinOhlc( ohlc: [ - Ohlc( - open: 50000, - high: 51000, - low: 49000, - close: 50500, + Ohlc.binance( + open: Decimal.fromInt(50000), + high: Decimal.fromInt(51000), + low: Decimal.fromInt(49000), + close: Decimal.fromInt(50500), openTime: DateTime.now() .subtract(const Duration(days: 1)) diff --git a/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart b/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart index 2edcdead9..8b4010613 100644 --- a/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart +++ b/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart @@ -27,11 +27,11 @@ void main() { final endAt = DateTime(2023, 12, 31); // 364 days final mockOhlc = CoinOhlc( ohlc: [ - Ohlc( - open: 100, - high: 110, - low: 90, - close: 105, + Ohlc.binance( + open: Decimal.fromInt(100), + high: Decimal.fromInt(110), + low: Decimal.fromInt(90), + close: Decimal.fromInt(105), openTime: startAt.millisecondsSinceEpoch, closeTime: endAt.millisecondsSinceEpoch, ), @@ -71,11 +71,11 @@ void main() { final endAt = DateTime(2024); // More than 365 days final mockOhlc = CoinOhlc( ohlc: [ - Ohlc( - open: 100, - high: 110, - low: 90, - close: 105, + Ohlc.binance( + open: Decimal.fromInt(100), + high: Decimal.fromInt(110), + low: Decimal.fromInt(90), + close: Decimal.fromInt(105), openTime: startAt.millisecondsSinceEpoch, closeTime: endAt.millisecondsSinceEpoch, ), @@ -117,11 +117,11 @@ void main() { final endAt = DateTime(2024); // Exactly 365 days final mockOhlc = CoinOhlc( ohlc: [ - Ohlc( - open: 100, - high: 110, - low: 90, - close: 105, + Ohlc.binance( + open: Decimal.fromInt(100), + high: Decimal.fromInt(110), + low: Decimal.fromInt(90), + close: Decimal.fromInt(105), openTime: startAt.millisecondsSinceEpoch, closeTime: endAt.millisecondsSinceEpoch, ), @@ -502,17 +502,11 @@ void main() { ); // Call method with USDT - should use USD as vs_currency, not USDT - await repository.getCoin24hrPriceChange( - assetId, - fiatCurrency: Stablecoin.usdt, - ); + await repository.getCoin24hrPriceChange(assetId); // Verify that USD was used, not USDT verify( - () => mockProvider.fetchCoinMarketData( - ids: ['bitcoin'], - vsCurrency: 'usd', // Should be 'usd', not 'usdt' - ), + () => mockProvider.fetchCoinMarketData(ids: ['bitcoin']), ).called(1); }, ); @@ -580,17 +574,11 @@ void main() { ); // Call method with USDT - should fall back to USD (final fallback), not USDT - await repository.getCoin24hrPriceChange( - assetId, - fiatCurrency: Stablecoin.usdt, - ); + await repository.getCoin24hrPriceChange(assetId); // Verify that USD was used as final fallback, not USDT verify( - () => mockProvider.fetchCoinMarketData( - ids: ['bitcoin'], - vsCurrency: 'usd', // Should fall back to 'usd', never 'usdt' - ), + () => mockProvider.fetchCoinMarketData(ids: ['bitcoin']), ).called(1); }, ); diff --git a/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart b/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart index 5b3e492cf..c10a15cdf 100644 --- a/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart @@ -139,10 +139,15 @@ class CexMarketDataManager QuoteCurrency quoteCurrency = Stablecoin.usdt, }) { final basePrefix = assetId.baseCacheKeyPrefix; + // Normalize input dates to UTC midnight before lookups to avoid timezone issues + final normalizedDate = + priceDate != null + ? DateTime.utc(priceDate.year, priceDate.month, priceDate.day) + : null; return canonicalCacheKeyFromBasePrefix(basePrefix, { 'quote': quoteCurrency.symbol, 'kind': 'price', - if (priceDate != null) 'ts': priceDate.millisecondsSinceEpoch, + if (normalizedDate != null) 'ts': normalizedDate.millisecondsSinceEpoch, }); } @@ -339,11 +344,17 @@ class CexMarketDataManager _checkNotDisposed(); _assertInitialized(); + // Normalize input dates to UTC midnight to avoid timezone issues + final normalizedDates = + dates + .map((date) => DateTime.utc(date.year, date.month, date.day)) + .toList(); + final cached = {}; final missingDates = []; - // Check cache for each date - for (final date in dates) { + // Check cache for each normalized date + for (final date in normalizedDates) { final cacheKey = _getCacheKey( assetId, priceDate: date, @@ -377,13 +388,15 @@ class CexMarketDataManager // Convert to Decimal, cache, and merge with cached final priceMap = priceDoubleMap.map((date, value) { final dec = Decimal.parse(value.toString()); + // Normalize the date from the repository response to UTC midnight + final normalizedDate = DateTime.utc(date.year, date.month, date.day); final cacheKey = _getCacheKey( assetId, - priceDate: date, + priceDate: normalizedDate, quoteCurrency: quoteCurrency, ); _priceCache[cacheKey] = dec; - return MapEntry(date, dec); + return MapEntry(normalizedDate, dec); }); return {...cached, ...priceMap}; diff --git a/packages/komodo_defi_sdk/test/market_data_manager_test.dart b/packages/komodo_defi_sdk/test/market_data_manager_test.dart index d6d3f0a38..8d195153b 100644 --- a/packages/komodo_defi_sdk/test/market_data_manager_test.dart +++ b/packages/komodo_defi_sdk/test/market_data_manager_test.dart @@ -316,7 +316,7 @@ void main() { () => fallbackRepo.supports(any(), any(), any()), ).thenAnswer((_) async => true); - final testDates = [DateTime(2023), DateTime(2023, 1, 2)]; + final testDates = [DateTime.utc(2023), DateTime.utc(2023, 1, 2)]; // Setup strategy to return primary repo first when(