forked from EllieBotDevs/elliebot
200 lines
No EOL
6.5 KiB
C#
200 lines
No EOL
6.5 KiB
C#
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.Drawing.Processing;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using SixLabors.ImageSharp.Processing;
|
|
using System.Runtime.CompilerServices;
|
|
using Color = SixLabors.ImageSharp.Color;
|
|
|
|
namespace EllieBot.Modules.Searches;
|
|
|
|
public sealed class ImagesharpStockChartDrawingService : IStockChartDrawingService, IEService
|
|
{
|
|
private const int WIDTH = 300;
|
|
private const int HEIGHT = 100;
|
|
private const decimal MAX_HEIGHT = HEIGHT * 0.8m;
|
|
|
|
private static readonly Rgba32 _backgroundColor = Rgba32.ParseHex("17181E");
|
|
private static readonly Rgba32 _lineGuideColor = Rgba32.ParseHex("212125");
|
|
private static readonly Rgba32 _sparklineColor = Rgba32.ParseHex("2961FC");
|
|
private static readonly Rgba32 _greenBrush = Rgba32.ParseHex("26A69A");
|
|
private static readonly Rgba32 _redBrush = Rgba32.ParseHex("EF5350");
|
|
|
|
private static float GetNormalizedPoint(decimal max, decimal point, decimal range)
|
|
=> (float)((MAX_HEIGHT * ((max - point) / range)) + HeightOffset());
|
|
|
|
private PointF[] GetSparklinePointsInternal(IReadOnlyCollection<CandleData> series)
|
|
{
|
|
var candleStep = WIDTH / (series.Count + 1);
|
|
var max = series.Max(static x => x.High);
|
|
var min = series.Min(static x => x.Low);
|
|
|
|
var range = max - min;
|
|
|
|
var points = new PointF[series.Count];
|
|
|
|
var i = 0;
|
|
foreach (var candle in series)
|
|
{
|
|
var x = candleStep * (i + 1);
|
|
|
|
var y = GetNormalizedPoint(max, candle.Close, range);
|
|
points[i++] = new(x, y);
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static decimal HeightOffset()
|
|
=> (HEIGHT - MAX_HEIGHT) / 2m;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static Image<Rgba32> CreateCanvasInternal()
|
|
=> new Image<Rgba32>(WIDTH, HEIGHT, _backgroundColor);
|
|
|
|
private CandleDrawingData[] GetChartDrawingDataInternal(IReadOnlyCollection<CandleData> series)
|
|
{
|
|
var candleMargin = 2;
|
|
var candleStep = (WIDTH - (candleMargin * series.Count)) / (series.Count + 1);
|
|
var max = series.Max(static x => x.High);
|
|
var min = series.Min(static x => x.Low);
|
|
|
|
var range = max - min;
|
|
|
|
var drawData = new CandleDrawingData[series.Count];
|
|
|
|
var candleWidth = candleStep;
|
|
|
|
var i = 0;
|
|
foreach (var candle in series)
|
|
{
|
|
var offsetX = (i - 1) * candleMargin;
|
|
var x = (candleStep * (i + 1)) + offsetX;
|
|
var yOpen = GetNormalizedPoint(max, candle.Open, range);
|
|
var yClose = GetNormalizedPoint(max, candle.Close, range);
|
|
var y = candle.Open > candle.Close
|
|
? yOpen
|
|
: yClose;
|
|
|
|
var sizeH = Math.Abs(yOpen - yClose);
|
|
|
|
var high = GetNormalizedPoint(max, candle.High, range);
|
|
var low = GetNormalizedPoint(max, candle.Low, range);
|
|
drawData[i] = new(candle.Open < candle.Close,
|
|
new(x, y, candleWidth, sizeH),
|
|
new(x + (candleStep / 2), high),
|
|
new(x + (candleStep / 2), low));
|
|
++i;
|
|
}
|
|
|
|
return drawData;
|
|
}
|
|
|
|
private void DrawChartData(Image<Rgba32> image, CandleDrawingData[] drawData)
|
|
=> image.Mutate(ctx =>
|
|
{
|
|
foreach (var data in drawData)
|
|
ctx.DrawLine(data.IsGreen
|
|
? _greenBrush
|
|
: _redBrush,
|
|
1,
|
|
data.High,
|
|
data.Low);
|
|
|
|
|
|
foreach (var data in drawData)
|
|
ctx.Fill(data.IsGreen
|
|
? _greenBrush
|
|
: _redBrush,
|
|
data.BodyRect);
|
|
});
|
|
|
|
private void DrawLineGuides(Image<Rgba32> image, IReadOnlyCollection<CandleData> series)
|
|
{
|
|
var max = series.Max(x => x.High);
|
|
var min = series.Min(x => x.Low);
|
|
|
|
var step = (max - min) / 5;
|
|
|
|
var lines = new float[6];
|
|
|
|
for (var i = 0; i < 6; i++)
|
|
{
|
|
var y = GetNormalizedPoint(max, min + (step * i), max - min);
|
|
lines[i] = y;
|
|
}
|
|
|
|
image.Mutate(ctx =>
|
|
{
|
|
// draw guides
|
|
foreach (var y in lines)
|
|
ctx.DrawLine(_lineGuideColor, 1, new PointF(0, y), new PointF(WIDTH, y));
|
|
|
|
// // draw min and max price on the chart
|
|
// ctx.DrawText(min.ToString(CultureInfo.InvariantCulture),
|
|
// SystemFonts.CreateFont("Arial", 5),
|
|
// Color.White,
|
|
// new PointF(0, (float)HeightOffset() - 5)
|
|
// );
|
|
//
|
|
// ctx.DrawText(max.ToString("N1", CultureInfo.InvariantCulture),
|
|
// SystemFonts.CreateFont("Arial", 5),
|
|
// Color.White,
|
|
// new PointF(0, HEIGHT - (float)HeightOffset())
|
|
// );
|
|
});
|
|
}
|
|
|
|
public Task<ImageData?> GenerateSparklineAsync(IReadOnlyCollection<CandleData> series)
|
|
{
|
|
if (series.Count == 0)
|
|
return Task.FromResult<ImageData?>(default);
|
|
|
|
using var image = CreateCanvasInternal();
|
|
|
|
var points = GetSparklinePointsInternal(series);
|
|
|
|
image.Mutate(ctx =>
|
|
{
|
|
ctx.DrawLine(_sparklineColor, 2, points);
|
|
});
|
|
|
|
return Task.FromResult<ImageData?>(new("png", image.ToStream()));
|
|
}
|
|
|
|
public Task<ImageData?> GenerateCombinedChartAsync(IReadOnlyCollection<CandleData> series)
|
|
{
|
|
if (series.Count == 0)
|
|
return Task.FromResult<ImageData?>(default);
|
|
|
|
using var image = CreateCanvasInternal();
|
|
|
|
DrawLineGuides(image, series);
|
|
|
|
var chartData = GetChartDrawingDataInternal(series);
|
|
DrawChartData(image, chartData);
|
|
|
|
var points = GetSparklinePointsInternal(series);
|
|
image.Mutate(ctx =>
|
|
{
|
|
ctx.DrawLine(Color.ParseHex("00FFFFAA"), 1, points);
|
|
});
|
|
|
|
return Task.FromResult<ImageData?>(new("png", image.ToStream()));
|
|
}
|
|
|
|
public Task<ImageData?> GenerateCandleChartAsync(IReadOnlyCollection<CandleData> series)
|
|
{
|
|
if (series.Count == 0)
|
|
return Task.FromResult<ImageData?>(default);
|
|
|
|
using var image = CreateCanvasInternal();
|
|
|
|
DrawLineGuides(image, series);
|
|
|
|
var drawData = GetChartDrawingDataInternal(series);
|
|
DrawChartData(image, drawData);
|
|
|
|
return Task.FromResult<ImageData?>(new("png", image.ToStream()));
|
|
}
|
|
} |