293 lines
9.5 KiB
Dart
293 lines
9.5 KiB
Dart
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
import '../../../../core/theme/app_theme.dart';
|
|
import '../../models/technical_analysis_model.dart';
|
|
import 'chart_overlay_renderer.dart';
|
|
|
|
class CandlestickPainter extends CustomPainter {
|
|
final List<CandleModel> candles;
|
|
final List<ChartPatternModel> patterns;
|
|
final List<StrategySignalModel> signals;
|
|
final List<IndicatorModel> indicators;
|
|
final double scale;
|
|
final double panOffset;
|
|
final ThemePreset theme;
|
|
final bool showPatterns;
|
|
final bool showSma50;
|
|
final bool showSma200;
|
|
final bool showEma;
|
|
final bool showSignals;
|
|
final bool showSupertrend;
|
|
final Offset? tapPosition;
|
|
|
|
final double rightPadding = 60.0;
|
|
final double bottomPadding = 20.0;
|
|
|
|
CandlestickPainter({
|
|
required this.candles,
|
|
required this.patterns,
|
|
required this.signals,
|
|
required this.indicators,
|
|
required this.scale,
|
|
required this.panOffset,
|
|
required this.theme,
|
|
required this.showPatterns,
|
|
required this.showSma50,
|
|
required this.showSma200,
|
|
required this.showEma,
|
|
required this.showSignals,
|
|
required this.showSupertrend,
|
|
this.tapPosition,
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
if (candles.isEmpty) return;
|
|
|
|
final double chartWidth = size.width - rightPadding;
|
|
final double baseWidth = 10.0;
|
|
final double spacing = 5.0;
|
|
final double candleWidth = baseWidth * scale;
|
|
final double totalCandleSpace = candleWidth + (spacing * scale);
|
|
|
|
double maxPrice = 0;
|
|
double minPrice = double.infinity;
|
|
|
|
int visibleCount = 0;
|
|
for (int i = 0; i < candles.length; i++) {
|
|
final dx = (i * totalCandleSpace) + panOffset;
|
|
if (dx + candleWidth > -100 && dx < chartWidth + 100) {
|
|
visibleCount++;
|
|
final c = candles[i];
|
|
if (c.high > maxPrice) maxPrice = c.high;
|
|
if (c.low < minPrice) minPrice = c.low;
|
|
}
|
|
}
|
|
|
|
if (visibleCount == 0 || minPrice == double.infinity || maxPrice <= 0) {
|
|
for (var c in candles) {
|
|
if (c.high > maxPrice) maxPrice = c.high;
|
|
if (c.low < minPrice) minPrice = c.low;
|
|
}
|
|
}
|
|
|
|
if (minPrice == double.infinity || maxPrice <= 0) return;
|
|
|
|
final range = maxPrice - minPrice;
|
|
maxPrice += max(range * 0.1, 1.0);
|
|
minPrice -= max(range * 0.1, 1.0);
|
|
final paddedRange = maxPrice - minPrice;
|
|
if (paddedRange <= 0) return;
|
|
|
|
final double chartHeight = size.height - bottomPadding;
|
|
final double volumeHeight = chartHeight * 0.15;
|
|
final double candleAreaHeight = chartHeight - volumeHeight;
|
|
|
|
double maxVolume = 0;
|
|
for (int i = 0; i < candles.length; i++) {
|
|
if (candles[i].volume > maxVolume) maxVolume = candles[i].volume;
|
|
}
|
|
if (maxVolume == 0) maxVolume = 1;
|
|
|
|
ChartOverlayRenderer.drawGridAndAxis(
|
|
canvas: canvas,
|
|
size: size,
|
|
chartWidth: chartWidth,
|
|
candleAreaHeight: candleAreaHeight,
|
|
minPrice: minPrice,
|
|
maxPrice: maxPrice,
|
|
range: paddedRange,
|
|
candles: candles,
|
|
scale: scale,
|
|
panOffset: panOffset,
|
|
bottomPadding: bottomPadding,
|
|
theme: theme,
|
|
);
|
|
|
|
final paintBullish = Paint()..color = theme.primaryColor..style = PaintingStyle.fill;
|
|
final paintBearish = Paint()..color = theme.accentRed..style = PaintingStyle.fill;
|
|
final paintWickBullish = Paint()..color = theme.primaryColor..strokeWidth = 1.5;
|
|
final paintWickBearish = Paint()..color = theme.accentRed..strokeWidth = 1.5;
|
|
|
|
final ema20Path = Path();
|
|
final sma50Path = Path();
|
|
final sma200Path = Path();
|
|
final supertrendPath = Path();
|
|
bool firstEma20 = true;
|
|
bool firstSma50 = true;
|
|
bool firstSma200 = true;
|
|
bool firstSupertrend = true;
|
|
|
|
double getXForTime(DateTime t) {
|
|
if (candles.isEmpty) return 0.0;
|
|
final lastCandle = candles.last;
|
|
if (t.isAfter(lastCandle.timestamp) && candles.length > 1) {
|
|
final totalSpan = lastCandle.timestamp.difference(candles.first.timestamp).inSeconds;
|
|
final secPerCandle = totalSpan / (candles.length - 1);
|
|
if (secPerCandle > 0) {
|
|
final futureSecs = t.difference(lastCandle.timestamp).inSeconds;
|
|
final futureCandles = futureSecs / secPerCandle;
|
|
final lastDx = ((candles.length - 1) * totalCandleSpace) + panOffset + candleWidth / 2;
|
|
return lastDx + (futureCandles * totalCandleSpace);
|
|
}
|
|
}
|
|
|
|
int bestIndex = 0;
|
|
int minDiff = 999999999;
|
|
for (int i = 0; i < candles.length; i++) {
|
|
final diff = candles[i].timestamp.difference(t).inSeconds.abs();
|
|
if (diff < minDiff) {
|
|
minDiff = diff;
|
|
bestIndex = i;
|
|
}
|
|
}
|
|
return (bestIndex * totalCandleSpace) + panOffset + candleWidth / 2;
|
|
}
|
|
|
|
double getYForPrice(double price) {
|
|
return candleAreaHeight - ((price - minPrice) / paddedRange) * candleAreaHeight;
|
|
}
|
|
|
|
canvas.save();
|
|
canvas.clipRect(Rect.fromLTWH(0, 0, chartWidth, chartHeight));
|
|
|
|
for (int i = 0; i < candles.length; i++) {
|
|
final candle = candles[i];
|
|
final isBullish = candle.close >= candle.open;
|
|
|
|
final dx = (i * totalCandleSpace) + panOffset;
|
|
if (dx < -candleWidth || dx > chartWidth) continue;
|
|
|
|
final yHigh = getYForPrice(candle.high);
|
|
final yLow = getYForPrice(candle.low);
|
|
final yOpen = getYForPrice(candle.open);
|
|
final yClose = getYForPrice(candle.close);
|
|
|
|
canvas.drawLine(
|
|
Offset(dx + candleWidth / 2, yHigh),
|
|
Offset(dx + candleWidth / 2, yLow),
|
|
isBullish ? paintWickBullish : paintWickBearish,
|
|
);
|
|
|
|
final top = min(yOpen, yClose);
|
|
final bottom = max(yOpen, yClose);
|
|
final bodyHeight = max(bottom - top, 1.0);
|
|
|
|
canvas.drawRect(
|
|
Rect.fromLTWH(dx, top, candleWidth, bodyHeight),
|
|
isBullish ? paintBullish : paintBearish,
|
|
);
|
|
|
|
final vHeight = (candle.volume / maxVolume) * volumeHeight;
|
|
final vTop = chartHeight - vHeight;
|
|
canvas.drawRect(
|
|
Rect.fromLTWH(dx, vTop, candleWidth, vHeight),
|
|
Paint()..color = (isBullish ? theme.primaryColor : theme.accentRed).withValues(alpha: 0.3)..style = PaintingStyle.fill,
|
|
);
|
|
|
|
if (indicators.isNotEmpty) {
|
|
final cx = dx + candleWidth / 2;
|
|
IndicatorModel? match;
|
|
for (var ind in indicators) {
|
|
if (ind.timestamp.isAtSameMomentAs(candle.timestamp) || ind.timestamp.difference(candle.timestamp).inHours.abs() < 12) {
|
|
match = ind;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (match != null) {
|
|
if (showEma && match.ema20 != null) {
|
|
final y = getYForPrice(match.ema20!);
|
|
if (firstEma20) { ema20Path.moveTo(cx, y); firstEma20 = false; }
|
|
else { ema20Path.lineTo(cx, y); }
|
|
}
|
|
if (showSma50 && match.sma50 != null) {
|
|
final y = getYForPrice(match.sma50!);
|
|
if (firstSma50) { sma50Path.moveTo(cx, y); firstSma50 = false; }
|
|
else { sma50Path.lineTo(cx, y); }
|
|
}
|
|
if (showSma200 && match.sma200 != null) {
|
|
final y = getYForPrice(match.sma200!);
|
|
if (firstSma200) { sma200Path.moveTo(cx, y); firstSma200 = false; }
|
|
else { sma200Path.lineTo(cx, y); }
|
|
}
|
|
|
|
if (showSupertrend) {
|
|
final stVal = match.supertrendDirection == 'BULLISH' ? match.supertrendLower : match.supertrendUpper;
|
|
if (stVal != null) {
|
|
final y = getYForPrice(stVal);
|
|
if (firstSupertrend) { supertrendPath.moveTo(cx, y); firstSupertrend = false; }
|
|
else { supertrendPath.lineTo(cx, y); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (showEma && !firstEma20) {
|
|
canvas.drawPath(ema20Path, Paint()..color = Colors.blueAccent..style = PaintingStyle.stroke..strokeWidth = 1.5);
|
|
}
|
|
if (showSma50 && !firstSma50) {
|
|
canvas.drawPath(sma50Path, Paint()..color = Colors.orangeAccent..style = PaintingStyle.stroke..strokeWidth = 1.5);
|
|
}
|
|
if (showSma200 && !firstSma200) {
|
|
canvas.drawPath(sma200Path, Paint()..color = Colors.redAccent..style = PaintingStyle.stroke..strokeWidth = 2.0);
|
|
}
|
|
if (showSupertrend && !firstSupertrend) {
|
|
canvas.drawPath(supertrendPath, Paint()..color = AppTheme.primaryEmerald..style = PaintingStyle.stroke..strokeWidth = 2.0);
|
|
}
|
|
|
|
if (showPatterns) {
|
|
ChartOverlayRenderer.drawPatterns(
|
|
canvas: canvas,
|
|
patterns: patterns,
|
|
getX: getXForTime,
|
|
getY: getYForPrice,
|
|
);
|
|
ChartOverlayRenderer.drawFutureProjectionZone(
|
|
canvas: canvas,
|
|
candles: candles,
|
|
patterns: patterns,
|
|
chartWidth: chartWidth,
|
|
candleAreaHeight: candleAreaHeight,
|
|
getX: getXForTime,
|
|
getY: getYForPrice,
|
|
theme: theme,
|
|
);
|
|
}
|
|
|
|
if (showSignals) {
|
|
ChartOverlayRenderer.drawSignals(
|
|
canvas: canvas,
|
|
signals: signals,
|
|
getX: getXForTime,
|
|
getY: getYForPrice,
|
|
theme: theme,
|
|
);
|
|
}
|
|
|
|
if (tapPosition != null && tapPosition!.dx < chartWidth) {
|
|
ChartOverlayRenderer.drawCrosshair(
|
|
canvas: canvas,
|
|
tapPosition: tapPosition!,
|
|
chartWidth: chartWidth,
|
|
chartHeight: chartHeight,
|
|
theme: theme,
|
|
);
|
|
}
|
|
|
|
canvas.restore();
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CandlestickPainter oldDelegate) {
|
|
return oldDelegate.scale != scale ||
|
|
oldDelegate.panOffset != panOffset ||
|
|
oldDelegate.candles != candles ||
|
|
oldDelegate.patterns != patterns ||
|
|
oldDelegate.signals != signals ||
|
|
oldDelegate.indicators != indicators ||
|
|
oldDelegate.tapPosition != tapPosition;
|
|
}
|
|
}
|