Страницы

Поиск по вопросам

вторник, 2 октября 2018 г.

C# и chart для финансовых рынков

Хочу реализовать отображение графиков используя wpf.
Приблизительно как на картинке. Интересует только View(разметка xaml).
Массив на вход: list. Price содержит: open price, close price.

Как это можно отобразить во View? Не используя сторонние библиотеки?
P. S. Смотрел на stackoverflow — данная тема поднималась уже много раз и много лет. Но нигде не было ни одного рабочего примера. Только ответы в духе: «это сложно», «за вас никто не сделает».
Поэтому критерием закрытия темы объявляю реальный рабочий пример! Давайте добьем эту задачу! Кто то ленится, а кто-то просто не может сделать — отнеситесь с пониманием.


Ответ

Не понимаю, что развели за разборки: сложно, невозможно, много кода... Фыр. Это же WPF, тут всё просто.
Для начала сгенерируем случайные данные:
using System; using System.Collections.Generic; using System.Linq; using static System.Math;
namespace CandlestickChartApp { public partial class MainWindow { public MainWindow() { DataContext = new Candlestick(); InitializeComponent(); } }
public class Candlestick { private const int PriceCount = 500; private const int PricesPerCandle = 10;
public List Prices { get; } = new List(PriceCount + 1); public List Candles { get; } = new List(PriceCount / PricesPerCandle); public List Labels { get; } = new List(); public double PriceCurrent { get; } public double PriceMin { get; } public double PriceMax { get; } public double PriceHeight { get; }
public Candlestick() { var rnd = new Random(1); var today = DateTime.Today; var date = DateTime.Today; var value = 300; for (var i = 0; i < Prices.Capacity; i++) Prices.Add(new Price { Date = date = date.AddMinutes(5), Value = value += rnd.Next(-9, 10) }); for (var i = 0; i < Candles.Capacity; i++) { var prices = Prices.Select(p => p.Value).Skip(i * PricesPerCandle).Take(PricesPerCandle + 1); Candles.Add(new Candle { Date = (Prices[i * PricesPerCandle].Date - today).TotalMinutes / 5, Min = prices.Min(), Max = prices.Max(), Height = prices.Max() - prices.Min(), DeltaMin = prices.First(), DeltaMax = prices.Last(), DeltaHeight = Abs(prices.Last() - prices.First()), IsPositive = prices.First() < prices.Last(), }); } Candles.ForEach(c => c.Fix()); PriceCurrent = Prices.Last().Value; PriceMin = Prices.Min(p => p.Value) - 20; PriceMax = Prices.Max(p => p.Value) + 20; PriceHeight = PriceMax - PriceMin - 40; for (double price = Round(PriceMin / 10) * 10; price < PriceMax; price += 50) Labels.Add(price); } }
public class Price { public DateTime Date { get; set; } public double Value { get; set; } }
public class Candle { public double Date { get; set; } public double Min { get; set; } public double Max { get; set; } public double Height { get; set; } public double DeltaMin { get; set; } public double DeltaMax { get; set; } public double DeltaHeight { get; set; } public bool IsPositive { get; set; }
public void Fix() { if (!IsPositive) { var min = DeltaMin; DeltaMin = DeltaMax; DeltaMax = min; } } } }
А потом остаётся только описать несколько шаблонов и стилей:

И пожалуйста:

Даты преобразуются к double, чтобы использовать как координату X. Цены генерируются в виде double и используются как Y (в доменной области они должны быть decimal).
Списки свечек и надписей служат источником элементов для ItemsControl. В качестве панели для размещения элементов используется Canvas вместо стандартной VirtualizingStackPanel, что позволяет размещать элементы не стопкой, а по координатам. Координаты прикрепляются через стилизацию ContentPresenter, которые оборачивают каждый элемент.
Возникает загвоздка с координатой Y, так как в WPF она считается от левого верхнего угла, а в графике — от левого нижнего. Это решается при помощи преобразования вертикального отражения относительно центра: сначала отражается всё содержимое контейнера, потом текстовые надписи отражаются ещё раз, чтобы их можно было прочитать.
Маргины вроде 0 8 0 -8 — это сдвиги элементов относительно исходной позиции без изменения "внешнего" размера (в данном случае сдвиг вниз на 8 пунктов). Этот приём используется для центрирования надписей по вертикали и сдвига самого графика.
Так как WPF не умеет рисовать прямоугольники с отрицательной высотой, то надо удостовериться, что "минимальное" значение DeltaMin меньше "максимального" DeltaMax
Запросы LINQ в коде не очень оптимальные, совершается много избыточных проходов по коллекции. Это сделано исключительно для простоты кода, в реальном приложении придётся написать менее кратко.
Также к имени класса Candle можно дописать суффикс ViewModel, так как этот класс — не часть домена, а используется исключительно как удобный источник данных для XAML. По вкусу можно сделать свойства свечки более доменными, но тогда понадобится писать конвертеры IValueConverter для преобразования DateTime и decimal к double. Это уже вопрос вкуса.

Всего-то 80 строк XAML.
Разумеется, это пример, и многое зашито в коде. Доведение до ума, в том числе дорисовывание вертикальных линий сетки — домашнее задание.
Реальный контрол должен учитывать, что диапазоны значений бывают разные по масштабу, давать возможность перематывать и масштабировать график и т.п. Но вот отобразить свечки — ну вообще никаких проблем. Развели разборки на пустом месте.
Человеку хочется потратить 500 репы на непонятно зачем нужную задачку — его воля. Может, он изучает WPF и хочет красивый простой пример. Может, у него неадекватный заказчик, который запрещает пользоваться библиотеками. Разные бывают ситуации. Если человек готов пожертвовать потом и кровью добытую репу, то, наверное, очень надо.

Комментариев нет:

Отправить комментарий