feat: K线数据使用Decimal类型,防止数值精度丢失和溢出
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import 'package:decimal/decimal.dart';
|
||||
|
||||
/// K 线数据模型
|
||||
class Candle {
|
||||
final int timestamp; // 毫秒时间戳
|
||||
final double open;
|
||||
final double high;
|
||||
final double low;
|
||||
final double close;
|
||||
final double volume;
|
||||
final Decimal open;
|
||||
final Decimal high;
|
||||
final Decimal low;
|
||||
final Decimal close;
|
||||
final Decimal volume;
|
||||
|
||||
const Candle({
|
||||
required this.timestamp,
|
||||
@@ -16,23 +18,82 @@ class Candle {
|
||||
required this.volume,
|
||||
});
|
||||
|
||||
/// 从 JSON 解析,使用 Decimal 避免精度丢失
|
||||
factory Candle.fromJson(Map<String, dynamic> json) {
|
||||
return Candle(
|
||||
timestamp: json['timestamp'] as int? ?? json['time'] as int? ?? 0,
|
||||
open: (json['open'] as num?)?.toDouble() ?? 0,
|
||||
high: (json['high'] as num?)?.toDouble() ?? 0,
|
||||
low: (json['low'] as num?)?.toDouble() ?? 0,
|
||||
close: (json['close'] as num?)?.toDouble() ?? 0,
|
||||
volume: (json['volume'] as num?)?.toDouble() ?? 0,
|
||||
open: _parseDecimal(json['open']),
|
||||
high: _parseDecimal(json['high']),
|
||||
low: _parseDecimal(json['low']),
|
||||
close: _parseDecimal(json['close']),
|
||||
volume: _parseDecimal(json['volume']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 安全解析 Decimal
|
||||
static Decimal _parseDecimal(dynamic value) {
|
||||
if (value == null) return Decimal.zero;
|
||||
if (value is Decimal) return value;
|
||||
if (value is num) return Decimal.fromJson(value.toString());
|
||||
if (value is String) {
|
||||
try {
|
||||
return Decimal.parse(value);
|
||||
} catch (_) {
|
||||
return Decimal.zero;
|
||||
}
|
||||
}
|
||||
return Decimal.zero;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'timestamp': timestamp,
|
||||
'open': open,
|
||||
'high': high,
|
||||
'low': low,
|
||||
'close': close,
|
||||
'volume': volume,
|
||||
'open': open.toDouble(),
|
||||
'high': high.toDouble(),
|
||||
'low': low.toDouble(),
|
||||
'close': close.toDouble(),
|
||||
'volume': volume.toDouble(),
|
||||
};
|
||||
|
||||
/// 转换为 double(供图表库使用)
|
||||
double get openDouble => open.toDouble();
|
||||
double get highDouble => high.toDouble();
|
||||
double get lowDouble => low.toDouble();
|
||||
double get closeDouble => close.toDouble();
|
||||
double get volumeDouble => volume.toDouble();
|
||||
|
||||
/// 安全加法(防止溢出)
|
||||
Decimal safeAdd(Decimal other) {
|
||||
try {
|
||||
return open + other;
|
||||
} catch (_) {
|
||||
return Decimal.zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全乘法(防止溢出)
|
||||
Decimal safeMultiply(Decimal other) {
|
||||
try {
|
||||
return open * other;
|
||||
} catch (_) {
|
||||
return Decimal.zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化价格显示
|
||||
String formatPrice({int decimalPlaces = 2}) {
|
||||
return close.toStringAsFixed(decimalPlaces);
|
||||
}
|
||||
|
||||
/// 格式化成交量显示
|
||||
String formatVolume() {
|
||||
final vol = volume.toDouble();
|
||||
if (vol >= 1000000000) {
|
||||
return '${(vol / 1000000000).toStringAsFixed(2)}B';
|
||||
} else if (vol >= 1000000) {
|
||||
return '${(vol / 1000000).toStringAsFixed(2)}M';
|
||||
} else if (vol >= 1000) {
|
||||
return '${(vol / 1000).toStringAsFixed(2)}K';
|
||||
}
|
||||
return vol.toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_chen_kchart/k_chart.dart';
|
||||
import 'package:decimal/decimal.dart';
|
||||
import '../models/candle.dart';
|
||||
|
||||
/// 时间周期
|
||||
@@ -83,15 +84,15 @@ class ChartProvider extends ChangeNotifier {
|
||||
List<Candle> _candles = [];
|
||||
List<Candle> get candles => _candles;
|
||||
|
||||
// k_chart 需要的数据格式
|
||||
// k_chart 需要的数据格式(转换为 double)
|
||||
List<KLineEntity> get klineData {
|
||||
return _candles.map((c) {
|
||||
return KLineEntity.fromCustom(
|
||||
open: c.open,
|
||||
high: c.high,
|
||||
low: c.low,
|
||||
close: c.close,
|
||||
vol: c.volume,
|
||||
open: c.openDouble,
|
||||
high: c.highDouble,
|
||||
low: c.lowDouble,
|
||||
close: c.closeDouble,
|
||||
vol: c.volumeDouble,
|
||||
time: c.timestamp,
|
||||
);
|
||||
}).toList();
|
||||
@@ -155,7 +156,7 @@ class ChartProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成模拟 K 线数据
|
||||
/// 生成模拟 K 线数据(使用 Decimal 防止精度丢失)
|
||||
List<Candle> _generateMockCandles() {
|
||||
final now = DateTime.now();
|
||||
final basePrice = _getBasePrice(_symbol);
|
||||
@@ -176,13 +177,14 @@ class ChartProvider extends ChangeNotifier {
|
||||
: i * 1440,
|
||||
));
|
||||
|
||||
final random = (i * 17) % 100 / 100;
|
||||
final change = basePrice * 0.02 * (random - 0.5);
|
||||
// 使用 Decimal 进行安全计算
|
||||
final random = Decimal.parse(((i * 17) % 100 / 100).toString());
|
||||
final change = basePrice * Decimal.parse('0.02') * (random - Decimal.parse('0.5'));
|
||||
final open = basePrice + change;
|
||||
final close = open + basePrice * 0.01 * ((i * 13) % 100 / 100 - 0.5);
|
||||
final high = open > close ? open * 1.005 : close * 1.005;
|
||||
final low = open < close ? open * 0.995 : close * 0.995;
|
||||
final volume = 1000 + (i * 37) % 5000;
|
||||
final close = open + basePrice * Decimal.parse('0.01') * (Decimal.parse(((i * 13) % 100 / 100).toString()) - Decimal.parse('0.5'));
|
||||
final high = open > close ? open * Decimal.parse('1.005') : close * Decimal.parse('1.005');
|
||||
final low = open < close ? open * Decimal.parse('0.995') : close * Decimal.parse('0.995');
|
||||
final volume = Decimal.fromInt(1000 + (i * 37) % 5000);
|
||||
|
||||
candles.add(Candle(
|
||||
timestamp: time.millisecondsSinceEpoch,
|
||||
@@ -190,29 +192,29 @@ class ChartProvider extends ChangeNotifier {
|
||||
high: high,
|
||||
low: low,
|
||||
close: close,
|
||||
volume: volume.toDouble(),
|
||||
volume: volume,
|
||||
));
|
||||
}
|
||||
|
||||
return candles;
|
||||
}
|
||||
|
||||
double _getBasePrice(String symbol) {
|
||||
Decimal _getBasePrice(String symbol) {
|
||||
switch (symbol.toUpperCase()) {
|
||||
case 'BTC':
|
||||
return 42000.0;
|
||||
return Decimal.parse('42000');
|
||||
case 'ETH':
|
||||
return 2200.0;
|
||||
return Decimal.parse('2200');
|
||||
case 'BNB':
|
||||
return 300.0;
|
||||
return Decimal.parse('300');
|
||||
case 'SOL':
|
||||
return 100.0;
|
||||
return Decimal.parse('100');
|
||||
case 'XRP':
|
||||
return 0.5;
|
||||
return Decimal.parse('0.5');
|
||||
case 'DOGE':
|
||||
return 0.08;
|
||||
return Decimal.parse('0.08');
|
||||
default:
|
||||
return 100.0;
|
||||
return Decimal.fromInt(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_chen_kchart/k_chart.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:decimal/decimal.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/theme/app_theme_extension.dart';
|
||||
@@ -144,12 +145,14 @@ class ChartPage extends StatelessWidget {
|
||||
final lastCandle = candles.last;
|
||||
final firstCandle = candles.first;
|
||||
final change = lastCandle.close - firstCandle.open;
|
||||
final changePercent = firstCandle.open != 0 ? (change / firstCandle.open) * 100 : 0;
|
||||
final isUp = change >= 0;
|
||||
final changePercent = firstCandle.open == Decimal.zero
|
||||
? 0.0
|
||||
: (change.toDouble() / firstCandle.openDouble * 100);
|
||||
final isUp = change >= Decimal.fromInt(0);
|
||||
|
||||
double high24h = 0;
|
||||
double low24h = double.infinity;
|
||||
double volume24h = 0;
|
||||
Decimal high24h = Decimal.fromInt(0);
|
||||
Decimal low24h = Decimal.parse('999999999999');
|
||||
Decimal volume24h = Decimal.fromInt(0);
|
||||
for (var c in candles) {
|
||||
if (c.high > high24h) high24h = c.high;
|
||||
if (c.low < low24h) low24h = c.low;
|
||||
@@ -171,7 +174,7 @@ class ChartPage extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'\$${lastCandle.close.toStringAsFixed(2)}',
|
||||
'\$${lastCandle.closeDouble.toStringAsFixed(2)}',
|
||||
style: AppTextStyles.displaySmall(context).copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isUp ? context.appColors.up : context.appColors.down,
|
||||
@@ -197,11 +200,11 @@ class ChartPage extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Row(
|
||||
children: [
|
||||
_buildDataItem(context, '24h高', '\$${high24h.toStringAsFixed(2)}'),
|
||||
_buildDataItem(context, '24h高', '\$${high24h.toDouble().toStringAsFixed(2)}'),
|
||||
const SizedBox(width: AppSpacing.lg),
|
||||
_buildDataItem(context, '24h低', '\$${low24h.toStringAsFixed(2)}'),
|
||||
_buildDataItem(context, '24h低', '\$${low24h.toDouble().toStringAsFixed(2)}'),
|
||||
const SizedBox(width: AppSpacing.lg),
|
||||
_buildDataItem(context, '24h量', '${(volume24h / 1000).toStringAsFixed(1)}K'),
|
||||
_buildDataItem(context, '24h量', '${(volume24h.toDouble() / 1000).toStringAsFixed(1)}K'),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user