fix(trading): K线Y轴动态缩放 + 价格显示格式统一 + 首日涨幅修复
- K线图Y轴改为只基于可见K线计算范围,实现动态缩放(类似TradingView)
- 价格显示统一使用 0.0{n}xxx 格式替代科学计数法,覆盖全局 formatPrice、
K线图、交易记录页、挖矿记录页
- 修复"较上线首日"百分比始终显示 +0.00% 的问题:getFirstSnapshot 过滤
price=0 的早期快照
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f13814e577
commit
c24f383501
|
|
@ -29,10 +29,11 @@ export class PriceSnapshotRepository {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取最早的价格快照(上线首日价格)
|
||||
* 获取最早的非零价格快照(上线首日价格)
|
||||
*/
|
||||
async getFirstSnapshot(): Promise<PriceSnapshotEntity | null> {
|
||||
const record = await this.prisma.priceSnapshot.findFirst({
|
||||
where: { price: { gt: 0 } },
|
||||
orderBy: { snapshotTime: 'asc' },
|
||||
});
|
||||
if (!record) {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,30 @@ String formatPercent(String? value, [int precision = 2]) {
|
|||
}
|
||||
|
||||
String formatPrice(String? value) {
|
||||
return formatDecimal(value, 8);
|
||||
if (value == null || value.isEmpty) return '0';
|
||||
try {
|
||||
final decimal = Decimal.parse(value);
|
||||
if (decimal >= Decimal.one) return decimal.toStringAsFixed(4);
|
||||
if (decimal >= Decimal.parse('0.0001')) return decimal.toStringAsFixed(6);
|
||||
if (decimal <= Decimal.zero) return '0';
|
||||
// 0.00000980 → 0.0{5}980
|
||||
final str = decimal.toStringAsFixed(18);
|
||||
final dotIndex = str.indexOf('.');
|
||||
int zeroCount = 0;
|
||||
for (int i = dotIndex + 1; i < str.length; i++) {
|
||||
if (str[i] == '0') {
|
||||
zeroCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final sigStart = dotIndex + 1 + zeroCount;
|
||||
final sigEnd = sigStart + 3 > str.length ? str.length : sigStart + 3;
|
||||
final significant = str.substring(sigStart, sigEnd);
|
||||
return '0.0{$zeroCount}$significant';
|
||||
} catch (e) {
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
String formatAmount(String? value) {
|
||||
|
|
|
|||
|
|
@ -302,7 +302,24 @@ class _MiningRecordsListPageState extends ConsumerState<MiningRecordsListPage> {
|
|||
String _formatPrice(String price) {
|
||||
try {
|
||||
final value = double.parse(price);
|
||||
return value.toStringAsFixed(8);
|
||||
if (value >= 1) return value.toStringAsFixed(4);
|
||||
if (value >= 0.0001) return value.toStringAsFixed(6);
|
||||
if (value <= 0) return '0';
|
||||
// 0.00000980 → 0.0{5}980
|
||||
final str = value.toStringAsFixed(18);
|
||||
final dotIndex = str.indexOf('.');
|
||||
int zeroCount = 0;
|
||||
for (int i = dotIndex + 1; i < str.length; i++) {
|
||||
if (str[i] == '0') {
|
||||
zeroCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final sigStart = dotIndex + 1 + zeroCount;
|
||||
final sigEnd = sigStart + 3 > str.length ? str.length : sigStart + 3;
|
||||
final significant = str.substring(sigStart, sigEnd);
|
||||
return '0.0{$zeroCount}$significant';
|
||||
} catch (e) {
|
||||
return price;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -511,7 +511,24 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> with Si
|
|||
String _formatPrice(String price) {
|
||||
try {
|
||||
final value = double.parse(price);
|
||||
return value.toStringAsFixed(4);
|
||||
if (value >= 1) return value.toStringAsFixed(4);
|
||||
if (value >= 0.0001) return value.toStringAsFixed(6);
|
||||
if (value <= 0) return '0';
|
||||
// 0.00000980 → 0.0{5}980
|
||||
final str = value.toStringAsFixed(18);
|
||||
final dotIndex = str.indexOf('.');
|
||||
int zeroCount = 0;
|
||||
for (int i = dotIndex + 1; i < str.length; i++) {
|
||||
if (str[i] == '0') {
|
||||
zeroCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final sigStart = dotIndex + 1 + zeroCount;
|
||||
final sigEnd = sigStart + 3 > str.length ? str.length : sigStart + 3;
|
||||
final significant = str.substring(sigStart, sigEnd);
|
||||
return '0.0{$zeroCount}$significant';
|
||||
} catch (e) {
|
||||
return price;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -730,7 +730,22 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
String _formatPrice(double price) {
|
||||
if (price >= 1) return price.toStringAsFixed(4);
|
||||
if (price >= 0.0001) return price.toStringAsFixed(6);
|
||||
return price.toStringAsExponential(2);
|
||||
if (price <= 0) return '0';
|
||||
// 0.00000980 → 0.0{5}980
|
||||
final str = price.toStringAsFixed(18);
|
||||
final dotIndex = str.indexOf('.');
|
||||
int zeroCount = 0;
|
||||
for (int i = dotIndex + 1; i < str.length; i++) {
|
||||
if (str[i] == '0') {
|
||||
zeroCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final sigStart = dotIndex + 1 + zeroCount;
|
||||
final sigEnd = math.min(sigStart + 3, str.length);
|
||||
final significant = str.substring(sigStart, sigEnd);
|
||||
return '0.0{$zeroCount}$significant';
|
||||
}
|
||||
|
||||
String _formatVolume(double volume) {
|
||||
|
|
@ -746,17 +761,30 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
}
|
||||
|
||||
double _calcPriceY(double price, double chartHeight) {
|
||||
if (widget.klines.isEmpty) return chartHeight / 2;
|
||||
if (widget.klines.isEmpty || _chartWidth == 0) return chartHeight / 2;
|
||||
|
||||
// 与 KlinePainter 保持一致:只基于可见K线计算Y轴范围
|
||||
const leftPadding = 8.0;
|
||||
const rightPadding = 50.0;
|
||||
final drawableWidth = _chartWidth - leftPadding - rightPadding;
|
||||
|
||||
final visibleStart = math.max(0, (_scrollX / _candleWidth - 2).floor());
|
||||
final visibleEnd = math.min(widget.klines.length - 1, ((_scrollX + drawableWidth) / _candleWidth + 1).ceil());
|
||||
|
||||
double minPrice = double.infinity;
|
||||
double maxPrice = double.negativeInfinity;
|
||||
for (final kline in widget.klines) {
|
||||
for (int i = visibleStart; i <= visibleEnd; i++) {
|
||||
final kline = widget.klines[i];
|
||||
final low = double.tryParse(kline.low) ?? 0;
|
||||
final high = double.tryParse(kline.high) ?? 0;
|
||||
if (low < minPrice) minPrice = low;
|
||||
if (high > maxPrice) maxPrice = high;
|
||||
}
|
||||
|
||||
if (minPrice == double.infinity || maxPrice == double.negativeInfinity) {
|
||||
return chartHeight / 2;
|
||||
}
|
||||
|
||||
final range = maxPrice - minPrice;
|
||||
// 防止 range 为 0 导致 NaN
|
||||
final safeRange = range > 0 ? range : (maxPrice > 0 ? maxPrice * 0.1 : 1.0);
|
||||
|
|
|
|||
|
|
@ -59,21 +59,31 @@ class KlinePainter extends CustomPainter {
|
|||
final chartWidth = size.width - leftPadding - rightPadding;
|
||||
final chartHeight = size.height - topPadding - bottomPadding;
|
||||
|
||||
// 计算价格范围
|
||||
// 计算K线宽度(提前计算,用于确定可见范围)
|
||||
final actualCandleWidth = candleWidth ?? (chartWidth / klines.length);
|
||||
final bodyWidth = math.max(actualCandleWidth * 0.7, 2.0);
|
||||
|
||||
// 计算可见K线的索引范围
|
||||
final visibleStart = math.max(0, (scrollOffset / actualCandleWidth - 2).floor());
|
||||
final visibleEnd = math.min(klines.length - 1, ((scrollOffset + chartWidth) / actualCandleWidth + 1).ceil());
|
||||
|
||||
// 计算价格范围(只基于可见K线,实现Y轴动态缩放)
|
||||
double minPrice = double.infinity;
|
||||
double maxPrice = double.negativeInfinity;
|
||||
|
||||
for (final kline in klines) {
|
||||
for (int i = visibleStart; i <= visibleEnd; i++) {
|
||||
final kline = klines[i];
|
||||
final low = double.tryParse(kline.low) ?? 0;
|
||||
final high = double.tryParse(kline.high) ?? 0;
|
||||
if (low < minPrice) minPrice = low;
|
||||
if (high > maxPrice) maxPrice = high;
|
||||
}
|
||||
|
||||
// 考虑MA/EMA/BOLL线的范围
|
||||
// 考虑可见区域内MA/EMA/BOLL线的范围
|
||||
if (maData != null) {
|
||||
for (final values in maData!.values) {
|
||||
for (final v in values) {
|
||||
for (int i = visibleStart; i <= visibleEnd && i < values.length; i++) {
|
||||
final v = values[i];
|
||||
if (v != null) {
|
||||
if (v < minPrice) minPrice = v;
|
||||
if (v > maxPrice) maxPrice = v;
|
||||
|
|
@ -84,7 +94,8 @@ class KlinePainter extends CustomPainter {
|
|||
|
||||
if (emaData != null) {
|
||||
for (final values in emaData!.values) {
|
||||
for (final v in values) {
|
||||
for (int i = visibleStart; i <= visibleEnd && i < values.length; i++) {
|
||||
final v = values[i];
|
||||
if (v != null) {
|
||||
if (v < minPrice) minPrice = v;
|
||||
if (v > maxPrice) maxPrice = v;
|
||||
|
|
@ -95,7 +106,8 @@ class KlinePainter extends CustomPainter {
|
|||
|
||||
if (bollData != null) {
|
||||
for (final values in bollData!.values) {
|
||||
for (final v in values) {
|
||||
for (int i = visibleStart; i <= visibleEnd && i < values.length; i++) {
|
||||
final v = values[i];
|
||||
if (v != null) {
|
||||
if (v < minPrice) minPrice = v;
|
||||
if (v > maxPrice) maxPrice = v;
|
||||
|
|
@ -104,6 +116,12 @@ class KlinePainter extends CustomPainter {
|
|||
}
|
||||
}
|
||||
|
||||
// 防止无可见数据时的异常
|
||||
if (minPrice == double.infinity || maxPrice == double.negativeInfinity) {
|
||||
minPrice = 0;
|
||||
maxPrice = 1;
|
||||
}
|
||||
|
||||
// 添加上下留白
|
||||
final priceRange = maxPrice - minPrice;
|
||||
// 避免 priceRange 为 0 导致 NaN
|
||||
|
|
@ -122,10 +140,6 @@ class KlinePainter extends CustomPainter {
|
|||
// 绘制网格和价格标签
|
||||
_drawGrid(canvas, size, minPrice, maxPrice, leftPadding, rightPadding, topPadding, chartHeight);
|
||||
|
||||
// 计算K线宽度 - 使用传入的宽度或自动计算
|
||||
final actualCandleWidth = candleWidth ?? (chartWidth / klines.length);
|
||||
final bodyWidth = math.max(actualCandleWidth * 0.7, 2.0);
|
||||
|
||||
// 裁剪绘图区域,防止K线超出边界
|
||||
canvas.save();
|
||||
canvas.clipRect(Rect.fromLTRB(leftPadding, 0, size.width - rightPadding, size.height));
|
||||
|
|
@ -540,7 +554,22 @@ class KlinePainter extends CustomPainter {
|
|||
String _formatPrice(double price) {
|
||||
if (price >= 1) return price.toStringAsFixed(4);
|
||||
if (price >= 0.0001) return price.toStringAsFixed(6);
|
||||
return price.toStringAsExponential(2);
|
||||
if (price <= 0) return '0';
|
||||
// 0.00000980 → 0.0{5}980
|
||||
final str = price.toStringAsFixed(18);
|
||||
final dotIndex = str.indexOf('.');
|
||||
int zeroCount = 0;
|
||||
for (int i = dotIndex + 1; i < str.length; i++) {
|
||||
if (str[i] == '0') {
|
||||
zeroCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final sigStart = dotIndex + 1 + zeroCount;
|
||||
final sigEnd = math.min(sigStart + 3, str.length);
|
||||
final significant = str.substring(sigStart, sigEnd);
|
||||
return '0.0{$zeroCount}$significant';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
Loading…
Reference in New Issue