feat: K线数据使用Decimal类型,防止数值精度丢失和溢出

This commit is contained in:
2026-04-07 23:04:27 +08:00
parent ab1486929f
commit 007915b6f2
86 changed files with 64527 additions and 63858 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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'),
],
),
],