Поверхность оптимизации

SurfaceWidget показывает результаты перебора параметров как трёхмерный ландшафт: одна метрика над двумя дискретными осями, где значение метрики задаёт и высоту, и цвет. Контрол принимает те же данные, что и тепловая карта оптимизации, поэтому один набор результатов можно показать плоской картой, поверхностью или обеими сразу.

Поверхность оптимизации: результат как ландшафт по двум параметрам

Создание и обновление

import {
  HeatDirections,
  SurfaceWidget,
  type SurfaceData,
  type TradingHost,
} from '@stocksharp/trading-controls';

declare const host: TradingHost;

const surface = SurfaceWidget.create(
  document.querySelector<HTMLElement>('#surface')!,
  {},
  { host },
);

const sweep: SurfaceData = {
  xLabel: 'Fast',
  yLabel: 'Slow',
  metricLabel: 'Net profit',
  betterWhen: HeatDirections.Higher,
  cells: [
    { x: '10', y: '50', value: 1_250 },
    { x: '10', y: '80', value: -320 },
    { x: '20', y: '50', value: 2_480 },
    { x: '20', y: '80', value: 640 },
  ],
};

surface.update(sweep);

Единственная зависимость — host; интерфейс SurfaceDeps других полей не содержит. Второй аргумент create — сохранённое состояние экземпляра: поверхность его не читает и ничего в него не пишет.

update заменяет весь набор целиком. Поверхность — это один прогон перебора, поэтому частичного обновления у неё нет: половина одного прогона поверх половины другого дала бы ландшафт из двух разных сводок.

Статическое свойство SurfaceWidget.TYPE равно ControlTypes.OptimizationSurface — идентификатору optimizationSurface.

Данные

Ячейка HeatCell — это пара значений осей и измеренная метрика: { x, y, value }. Значения осей строковые, потому что ось дискретна: 10, 00:05:00 и True — равноправные позиции на ней. Числовые значения упорядочиваются как числа, остальные — как текст, поэтому перебор 5, 8, 12, 40 остаётся последовательностью.

Поле betterWhen обязательно и принимает HeatDirections.Higher или HeatDirections.Lower. Без него ландшафт просадок поднял бы худший угол в вершину.

Несколько прогонов на одной паре сворачиваются в среднее — это одна ячейка. Пара, которую перебор не проходил, остаётся дырой: грань рисуется, только когда известны все четыре её угла, а пропуск не интерполируется — отсутствующий результат не равен нулевому. Если ячеек нет вовсе или хотя бы на одной оси меньше двух различных значений, вместо ландшафта показывается заглушка с текстом по ключу NoOptimizationResults.

Подписи xLabel, yLabel и metricLabel идут в подписи осей; metricLabel дополнительно выводится в заголовке панели.

Отображение

Высота грани — это положение значения относительно опорной точки шкалы, свёрнутое в диапазон от пола до верха: опорная точка приходится на середину высоты, поэтому пол означает не «худший результат», а нижний край шкалы. Цвет берётся из host.presentation.canvasPalette(): up для значений лучше опорной точки, down — хуже, насыщенность растёт с удалением от неё. Грани заливаются от дальних к ближним, поэтому ближний гребень перекрывает то, что за ним, и обводятся цветом grid, чтобы сетка читалась там, где две соседние грани почти одного оттенка.

Проекция ортографическая: поверхность читают, сравнивая высоты по всему полю, а перспектива укоротила бы дальнюю сторону гребня относительно ближней.

Под ландшафтом рисуются два ребра пола и вертикальная ось шкалы. На параметрических осях выводится до восьми меток: пока значения помещаются — все, дальше каждое второе, каждое третье и так далее, причём первое и последнее подписываются всегда. На вертикальной оси пять меток, подписанных значениями метрики. Подписи переносятся на те рёбра пола, которые ближе к наблюдателю, — при повороте это пересчитывается, чтобы числа не оказались поверх сетки.

Обзор и жесты

Все жесты приходят через pointer-события, поэтому мышь, перо и палец идут одним путём:

  • перетаскивание одним указателем поворачивает поверхность: по горизонтали меняется yaw, по вертикали — pitch;
  • два указателя меняют масштаб расстоянием между ними; поворот при этом остаётся за одним указателем;
  • колесо тоже меняет масштаб. Дельта приводится к «щелчкам» независимо от того, в пикселях, строках или страницах её сообщил браузер, и ограничивается двумя щелчками на событие, чтобы мышь и трекпад давали сопоставимый шаг.

Жест над холстом контрол забирает себе, иначе перетаскивание на телефоне и колесо на настольном браузере прокручивали бы страницу вместо ландшафта.

Наклон и масштаб ограничены: pitch — от MIN_PITCH (0,12) до MAX_PITCH (1,45), масштаб — от 0,4 до 4. При наклоне в ноль каждая грань выродилась бы в линию, а при прямом угле поверхность стала бы плоской картой, то есть другим контролом. Поворот yaw не ограничен, а замыкается: развернуть ландшафт кругом, чтобы посмотреть на обратный склон гребня, — осмысленный жест. Начальный обзор — DEFAULT_VIEW; кнопка в заголовке панели (ResetView) возвращает к нему.

Когда указатель не поворачивает поверхность, контрол ищет ближайшую измеренную точку в радиусе 22 CSS-пикселей. Найденная точка обводится кольцом цветом up, а в полосе над холстом появляется строка: значение по оси xLabel, значение по оси yLabel и метрика. Полоса лежит поверх холста, а не в заголовке панели: показание относится к точке под указателем и должно быть рядом с ней. Пара, которую перебор не проходил, указателю не предлагается; при равном расстоянии выбирается точка, которая ближе к наблюдателю.

Что делает контрол и что остаётся хосту

Контрол сам обрабатывает жесты, следит за размером холста через ResizeObserver и перерисовывает ландшафт под текущий размер и плотность пикселей экрана, обслуживает кнопку сброса обзора и кнопку закрытия панели, вызывающую host.close(), а также регистрируется через host.register и снимается с регистрации в dispose. Цвета и шрифт холста приходят из host.presentation.canvasPalette(). Весь видимый текст запрашивается через host.t: OptimizationSurface, ResetView, ClosePanel, OptimizationSurfaceChart, NoOptimizationResults.

Данные подаёт хост: контрол не запускает оптимизацию, не подписывается на её ход и не знает, чем получены результаты, — что передано в update, то и нарисовано. Клика по грани нет: грань соответствует ячейке, а не отдельному прогону, поэтому открывать по ней нечего — нажатие поворачивает ландшафт.

Собственных настроек в host.preferences контрол не хранит, ключей у него нет, host.persistState не вызывает. Текущий обзор доступен методом view() — если его нужно восстанавливать между сеансами, хост сохраняет и возвращает эти значения сам.

Публичные методы

  • update(data) — показать набор результатов целиком.
  • view() — вернуть копию текущего обзора (yaw, pitch, zoom).
  • resetView() — вернуть обзор к DEFAULT_VIEW.
  • dispose() — отключить наблюдатель размера, вызвать host.unregister и убрать корневой элемент.

Вспомогательные функции

Геометрия вынесена из контрола в отдельный модуль и экспортируется пакетом — на ней можно построить свою отрисовку:

  • surfaceLayout(input) — разложить ячейки в грани, оси и вершины для заданных размеров и обзора; null, когда рисовать нечего.
  • project(nx, ny, nz, view, box) — спроецировать точку единичного куба на холст.
  • dragView(view, dx, dy) — обзор после перетаскивания на столько пикселей.
  • zoomView(view, factor) — обзор после изменения масштаба.
  • clampView(view) — обзор, приведённый к допустимым пределам.
  • DEFAULT_VIEW, MIN_PITCH, MAX_PITCH — начальный обзор и границы наклона.

Смотрите также