Azure Data Explorer (Kusto/KQL) Остановка тестирования финансовых активов на исторических данных

У меня есть набор данных о ценах на финансовые активы с течением времени, и я хотел бы имитировать трейл-стоп для тестирования стратегий на основе этого набора данных.

Трейл-стоп — это тип торгового ордера, поддерживаемый некоторыми онлайн-брокерами, который используется в качестве стоп-лосса или защиты прибыли при открытии позиции. Трейл-стоп размещается для автоматического стоп-лосса при выполнении ценового условия.

Ордер трейл-стоп будет следовать за ценой актива по мере ее увеличения и оставаться на максимуме в течение всего времени, пока позиция открыта. Как только цена актива упадет ниже макс. трейл-стопа, позиция будет закрыта брокером.

В этом случае трейл-стоп представляет собой процент от цены актива. т.е. цена актива меньше 3%.

Я пробовал несколько подходов, включая суммирование и оператора сканирования, и, похоже, не смог найти работающий прототип.

Ниже приведен пример таблицы данных актива с изменением цены с течением времени.

//Trail Stop Properties:
//Trail stop will follow an asset price as it increases 
//  and remain at the max of the asset price increase during an open position
//  the position will be closed when the price is less than 
//    or equal to the trail stop value.

//Usually the stop is set with a percentage of loss from the trailing price.
//i.e. in the below example the trailing stop is 0.03 or 3% of the asset price.

let trailstop = double(0.03);
let assets = datatable 
(
  Timestamp:datetime, Symbol:string, StrikePrice:double, CallPremium:double, 
  PositionId:int
)
[
    datetime(2022-03-16T13:57:55.815Z), 'SPY' ,432, 2.46, 1,
    datetime(2022-03-16T14:00:55.698Z), 'SPY' ,432, 2.48, 1,
    datetime(2022-03-16T14:01:15.876Z), 'SPY' ,432, 2.49, 1,
    datetime(2022-03-16T14:08:25.536Z), 'SPY' ,431, 2.45, 1,
    datetime(2022-03-16T14:18:25.675Z), 'SPY' ,434, 2.40, 1,
    datetime(2022-03-16T14:21:50.887Z), 'SPY' ,434, 2.40, 2,
    datetime(2022-03-16T14:35:00.835Z), 'SPY' ,434, 2.33, 2
]
;
assets
| sort by Timestamp asc
| extend TrailStop = round(CallPremium - (CallPremium * trailstop),2)
| extend rn = row_number()

Выход

2022-03-16T13:57:55.815Z    SPY 432 2.46    1   2.39    1
2022-03-16T14:00:55.698Z    SPY 432 2.48    1   2.41    2
2022-03-16T14:01:15.876Z    SPY 432 2.49    1   2.42    3
2022-03-16T14:08:25.536Z    SPY 431 2.45    1   2.38    4
2022-03-16T14:18:25.675Z    SPY 434 2.4     1   2.33    5
2022-03-16T14:21:50.887Z    SPY 434 2.4     2   2.33    6
2022-03-16T14:35:00.835Z    SPY 434 2.33    2   2.26    7

Если бы трейл-стоп работал правильно и имелись столбцы открытия и закрытия позиций, указывающие, когда произошла трейл-стоп, в результате чего позиция была закрыта, набор результатов выглядел бы как вывод следующей таблицы данных.

let outcomes = datatable 
(
    Timestamp:datetime, Symbol:string, StrikePrice:double, CallPremium:double, 
    PositionId:int, TrailStop:double, PositionOpen:int, PositionClose:int
)
[
    datetime(2022-03-16T13:57:55.815Z), 'SPY', 432, 2.46, 1, 2.39, 1, 0,
    datetime(2022-03-16T14:00:55.698Z), 'SPY', 432, 2.48, 1, 2.41, 1, 0,
    datetime(2022-03-16T14:01:15.876Z), 'SPY', 432, 2.49, 1, 2.42, 1, 0,
    datetime(2022-03-16T14:08:25.536Z), 'SPY', 431, 2.45, 1, 2.42, 1, 0,
    datetime(2022-03-16T14:18:25.675Z), 'SPY', 434, 2.40, 1, 2.42, 0, 1,
    datetime(2022-03-16T14:21:50.887Z), 'SPY', 434, 2.40, 2, 2.33, 1, 0,
    datetime(2022-03-16T14:35:00.835Z), 'SPY', 434, 2.33, 2, 2.26, 0, 1
]
;
outcomes
| sort by Timestamp asc
| extend rn = row_number()

Выход

    2022-03-16T13:57:55.815Z    SPY 432 2.46    1   2.39    1   0   1
    2022-03-16T14:00:55.698Z    SPY 432 2.48    1   2.41    1   0   2
    2022-03-16T14:01:15.876Z    SPY 432 2.49    1   2.42    1   0   3
    2022-03-16T14:08:25.536Z    SPY 431 2.45    1   2.42    1   0   4
    2022-03-16T14:18:25.675Z    SPY 434 2.4     1   2.42    0   1   5
    2022-03-16T14:21:50.887Z    SPY 434 2.4     2   2.33    1   0   6
    2022-03-16T14:35:00.835Z    SPY 434 2.33    2   2.26    0   1   7

Конечным результатом будут две открытые и закрытые позиции.

  • Позиция 1 открыто (rn=1) при 2,46 и закрыто (rn=5) при 2,42

  • Позиция 2 открыта (rn=6) в 2.40 и закрыта (rn=7) в 2.33

Любая помощь, идеи или рекомендации будут высоко оценены.

2
0
43
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Инвестопедия за хорошее объяснение трейлинг-стопа

  • Упорядочить по положению и отметке времени
  • Используйте prev() для определения начала новой позиции.
  • Используйте scan() для расчета текущего максимума CallPremium (всегда повышается, сбрасывается для новой позиции).
  • Сравните каждый CallPremium с текущим максимумом и проверьте, достигнут ли трейлинг-стоп.

let trailstop = double(0.03);
let assets = datatable 
(
  Timestamp:datetime, Symbol:string, StrikePrice:double, CallPremium:double, 
  PositionId:int
)
[
    datetime(2022-03-16T13:57:55.815Z), 'SPY' ,432, 2.46, 1,
    datetime(2022-03-16T14:00:55.698Z), 'SPY' ,432, 2.48, 1,
    datetime(2022-03-16T14:01:15.876Z), 'SPY' ,432, 2.49, 1,
    datetime(2022-03-16T14:08:25.536Z), 'SPY' ,431, 2.45, 1,
    datetime(2022-03-16T14:18:25.675Z), 'SPY' ,434, 2.40, 1,
    datetime(2022-03-16T14:21:50.887Z), 'SPY' ,434, 2.40, 2,
    datetime(2022-03-16T14:35:00.835Z), 'SPY' ,434, 2.33, 2
]
;
assets
| sort by PositionId asc, Timestamp asc
| extend PositionId_start = prev(PositionId) != PositionId
| scan declare (CallPremium_running_max:double = double(null))
with
(
    step s1 : true => CallPremium_running_max = 
                        max_of(iff(PositionId_start,double(null),s1.CallPremium_running_max),CallPremium);
) 
| extend TrailStop = round(CallPremium_running_max*(1-trailstop),2)
| extend PositionOpen = iff(CallPremium <= TrailStop,1,0)
| extend PositionClose = 1 - PositionOpen
Отметка времениУсловное обозначениеСтрайкЦенаCallПремиумИдентификатор позицииPositionId_startCallPremium_running_maxТрейлСтопПозицияОткрытаПозицияЗакрыть
2022-03-16T13:57:55.815ZШПИОН4322,461истинный2,462,3901
2022-03-16T14:00:55.698ZШПИОН4322,481ЛОЖЬ2,482,4101
2022-03-16T14:01:15.876ZШПИОН4322,491ЛОЖЬ2,492,4201
2022-03-16T14:08:25.536ZШПИОН4312,451ЛОЖЬ2,492,4201
2022-03-16T14:18:25.675ZШПИОН4342,41ЛОЖЬ2,492,4210
2022-03-16T14:21:50.887ZШПИОН4342,42истинный2,42,3301
2022-03-16T14:35:00.835ZШПИОН4342,332ЛОЖЬ2,42,3310

Скрипка

Спасибо за отличный ответ @David דודו Markovitz. Я сделал одно небольшое изменение, чтобы выровнять значения столбцов Position Open/Close. | extend PositionOpen = iff(CallPremium > TrailStop,1,0)

Damien Johnston 18.03.2022 14:33

Tbh, исходные данные, которые я опубликовал, немного надуманы, реальные рыночные данные, используемые для тестирования на истории, не имеют столбца PositionId. Я создал его как часть примера набора данных. На самом деле используемые рыночные данные таковы; на основе времени, активов и цены. В этом случае цель бэк-тестирования состоит в том, чтобы определить, когда позиции будут открываться благодаря индикаторам рыночных условий и закрываться из-за определений ордеров трейл-стоп. Значения PositionId будут дополнительной группировкой, добавленной в процессе открытия позиции. т. е. когда позиция открыта, значение PositionId+1 для всех строк до стоп-ордера.

Damien Johnston 18.03.2022 14:53

С удовольствием, Дэмиен. Если необходимо изменить вопрос, я бы предложил сохранить исходную версию, утвердить ответ и открыть новый пост для измененной версии.

David דודו Markovitz 18.03.2022 15:43

Спасибо @David דודו Markovitz, я отметил этот вопрос как ответ и опубликовал новый вопрос в качестве продолжения этого вопроса. Для справки: идентификатор нового вопроса — 71529994, его можно найти по адресу Следовать за.

Damien Johnston 18.03.2022 17:12

Другие вопросы по теме