03 июня 2019

Технический анализ: Исследование некоторых Технических индикаторов рынка с помощью Oracle Database

Эта статья про Oracle Database, PL/SQL, SQL, MATCH_RECOGNIZE, MODEL clause, aggregate и pipelined functions.

В качестве функциональной области использован Технический анализ рынка. Сначала небольшая поверхностная вводная о торговле на рынках, потом — расчёты.


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





Разработано много SQL-операторов, процедур и графиков. Часть — ниже, полностью код  на GitHub по ссылке внизу статьи.


Технические индикаторы рынка (ТИР) — дополнительные графики, к графикам цен, формируемые на основе пересчёта значений, содержащихся в базовом графике цены. Обычно это различные виды усреднений (очередная точка графика рассчитывается как усреднённое значение некоторого количества предыдущих значений цены, например, скользящая средняя), отношений (очередная точка является результатом сопоставления некоторого числа предыдущих цен — их разность, производная от изменения за период), или лагов (задержек). Индикаторы наглядно показывают некоторую неочевидную информацию, содержащуюся в статистике изменения цены, и могут формировать рекомендации о торговых приказах — BUY/SELL. Индикаторы имеют минимум один изменяемый параметр, от значения которого будет изменяться результат. Для открытия реальных сделок обычно используется несколько индикаторов в комплексе, плюс дополнительная информация, на усмотрение трейдера.
Один из постулатов Технического анализа — «история повторяется»: Участники рынка в схожих обстоятельствах ведут себя примерно одинаково, формируя похожую динамику изменения цен. И естественно предположить, что поведение рынка в будущем, в основном, повторит наметившиеся в прошлом модели. Следуя этому утверждению, инвестор может выбрать из возможных параметров ТИР те, что наилучшим образом зарекомендовали себя в прошлые периоды.
В этой статье строятся график результативности торговли при использовании каждого индикатора (ось Y) от значения параметра ТИР (ось X), это в двумерном представлении. Но фактически я буду строить в трёхмерном представлении, чтобы оценить также и влияние запаздывания исполнения приказа на результат, поэтому результат будет ось Z (аппликата), параметр — ось X (абсцисса), задержка — ось Y (ордината). Это некоторая попытка оценить влияние проскальзываний (“slippage”), которые, к сожалению, всегда случаются. Вместо реального «проскальзывания» по оси Y я откладываю исполнение приказа BUY/SELL на срок от 1 до 5 периодов.
Если на этом графике выделится глобальный максимум и сам график будет похож на шляпу (вообще, это называется «нормальное распределение», но оно предполагает строгую симметрию относительно вертикальной оси), конус, пирамида, тоже подходят — значит для этого ТИР можно подобрать определённое значение параметра, который покажет самый высокий результат, и с этим индикатором можно попробовать торговать. Если же график результативности в зависимости от параметра будет напоминать «частокол» — подобрать оптимальное значение параметра невозможно и нет смысла использовать этот ТИР в торговле.
Можно сказать, что в этой статье я рассчитываю лишь «процент попадания» индикаторов. Задача, оценить, сколько можно «заработать», мною не решалась.

Успешное прохождение описанной проверки является одним из необходимых, но не достаточных факторов для результативной торговли.


Для информации...  В противоположность Техническому анализу, Фундаментальный анализ (ФА) — термин для обозначения методов прогнозирования рыночной (биржевой) стоимости инструментов, основанных на анализе финансовых и производственных показателей деятельности.
«Внутренняя стоимость» по ФА в большинстве случаев не совпадает с ценой акций компании, которая определяется соотношением спроса и предложения на фондовом рынке. Инвесторов, использующих в своей деятельности ФА, интересуют в первую очередь ситуации, когда «внутренняя стоимость» акций компании превышает цену акций на бирже. Такие акции считаются недооцененными, значит их цена будет расти, и они являются потенциальными объектами инвестиций.
Один из наиболее известных инвесторов, использующих Фундаментальный анализ — Уоррен Баффетт.

Посмотрите отрывок из фильма: https://www.youtube.com/watch?v=SqE8fnvmV1Y
Таким образом мы имеем два диаметрально противоположных подхода — ТА и ФА.
ФА обычно интересует долгосрочных инвесторов, ТА — кратко- и среднесрочных и используется для сделок спекулятивного характера, когда сам предмет торговли трейдера не интересует.


И целей этой статьи две.
Кроме описанных выше рассуждений о ТА и ФА я хотел исследовать и показать возможности Oracle Database по выполнению расчётов Технических индикаторов рынка.
Эти возможности я и представляю на суд читателей.

Создание общих объектов

Таблицы и view

create table LOAD_YAHOO (
      STOCK_NAME    varchar2(128)
    , ADATE         date
    , AOPEN         number
    , AHIGH         number
    , ALOW          number
    , ACLOSE        number check (ACLOSE > 0)
    , AVOLUME       number
    , constraint LOAD_YAHOO_PKIOT primary key (STOCK_NAME, ADATE)
) organization index compress 1;
 
create or replace view LOAD_YAHOO_V as
select STOCK_NAME, ADATE
     , row_number () over (partition by STOCK_NAME order by ADATE) as RN
     , AOPEN, AHIGH, ALOW, ACLOSE, AVOLUME, round ((AHIGH + ALOW + ACLOSE) / 3, 20) as TYPICAL_PRICE
from LOAD_YAHOO a;
 
create type HABR_MARKETINDEXES_RESULT_T
as object (STOCK_NAME varchar2(128), ADATE date, ACLOSE number
         , IND_VALUE number, IND_VALUE2 number, IND_VALUE3 number, IND_VALUE4 number, IND_VALUE5 number
         , AACTION varchar(4));
create type HABR_MARKETINDEXES_RESULT_LIST_T as table of HABR_MARKETINDEXES_RESULT_T;

create table HABR_MARKETINDEXES_PARMSEL_RESULTS (
      INDICATOR_NAME        varchar2(256)
    , PARM1                 number
    , PARM2                 number
    , STOCK_NAME            varchar2(128)
    , ADATE_MIN             date
    , ADATE_MAX             date
    , DEALS_COUNT           number
    , BALANCE_RESULT        number
    , DEALS_PROFIT_AMOUNT   number
    , DEALS_LOSS_AMOUNT     number
    , DEALS_PROFIT_COUNT    number
    , DEALS_LOSS_COUNT      number
    , IN_STOCK              varchar2(16)
    , constraint HABR_MARKETINDEXES_PARMSEL_RESULTS_PKIOT primary key (INDICATOR_NAME, PARM1, PARM2, STOCK_NAME)
) organization index compress 3;



Пакет моделирования сделок:

Все функции расчёта всех ТИР будут возвращать курсор со следующими полями: STOCK_NAME, ADATE, ACLOSE (цена закрытия дня), AACTION (приказ продавать/покупать)

Пакет содержит три табличные функции моделирования, которые принимают на вход курсор из описанных выше функций расчёта ТИР, смещение (задержку, лаг), и начальный капитал, по умолчанию равен 1000 USD. Функции пакета вызываются так:


select * from HABR_TRADEMODELLING_P.TRADE_LOG (cursor (select STOCK_NAME, ADATE, ACLOSE, AACTION from table (HABR_MARKETINDEXES_XXXXXXXX_F_CALC (10))), p_lag => 1) order by 1, 2;
 
select * from HABR_TRADEMODELLING_P.CALC_ACTIONS (cursor (select STOCK_NAME, ADATE, ACLOSE, AACTION from table (HABR_MARKETINDEXES_XXXXXXXX_F_CALC (10))), p_lag => 1);
 
select * from HABR_TRADEMODELLING_P.CALC_ACTIONS_TOTALS (cursor (select STOCK_NAME, ADATE, ACLOSE, AACTION from table (HABR_MARKETINDEXES_XXXXXXXX_F_CALC (10))), p_lag => 1);


где XXXXXXXX — название ТИР.


Все функции рассчитывают результат торговли при условии полного реинвестирования прибыли, без учета расходов на транзакции и налоги и без учёта инфляции и дисконтирования.

Первая функция TRADE_LOG формирует упрощённый журнал сделок трейдера. Функция позволяет проследить всю цепочку сделок. Если на конец периода инвестор «в бумагах», для расчёта баланса в валюте (финансового результата) функция имитирует продажу всех бумаг по последней цене закрытия и формирует соответствующую пометку в поле IN_STOCK.

Вторая функция CALC_ACTIONS возвращает те-же колонки, что и вызванный курсор расчёта ТИР, плюс добавляет следующие колонки: AACTION_LAG (приказ со смещением), BALANCE_CURRENCY (баланс счёта трейдера в наличной валюте), BALANCE_STOCK (количество инструментов в открытых позициях). В зависимости от значения поля AACTION_LAG имитируется покупка или продажа по цене закрытия, и соответственно изменяются баланс в валюте и сумма открытых позиций.

В последней строке каждого инструмента можно увидеть итог торговли по каждому инструменту, также, как и в предыдущей функции, если инвестор «в бумагах», имитируется продажа для расчёта баланса в валюте

Третья функция CALC_ACTIONS_TOTALS делает то же самое, что и вторая, но возвращает только последнюю строку — итог торговли по каждому инструменту. Она и будет использована в моделировании.

create or replace package HABR_TRADEMODELLING_P as
 type ACTION_T is record (STOCK_NAME varchar2(128), ADATE date, APRICE number, AACTION varchar2(32));
 type CURSOR_ACTIONS_T is ref cursor return ACTION_T;

-- Trade Log 

type TRADE_LOG_T is record (STOCK_NAME varchar2(128), ADATE_LONG_OPEN date, ADATE_LONG_CLOSE date, DAYS_HELD integer, STOCK_COUNT number                             , APRICE_BUY number, APRICE_SELL number, AAMOUNT_BUY number, AAMOUNT_SELL number, DEAL_PROFIT_AMOUNT number, DEAL_LOSS_AMOUNT number, IN_STOCK varchar2(32), BALANCE_RESULT number);


type TRADE_LOG_LIST_T is table of TRADE_LOG_T;
 
function TRADE_LOG (p_cursor CURSOR_ACTIONS_T, p_lag integer, p_initial_balance number default 1000) return TRADE_LOG_LIST_T
pipelined order p_cursor by (STOCK_NAME, ADATE) parallel_enable (partition p_cursor by hash (STOCK_NAME));
 

-- Calc Actions 
 
type CALC_ACTIONS_T is record (STOCK_NAME varchar2(128), ADATE date, APRICE number, AACTION varchar2(32), AACTION_LAG varchar2(32), BALANCE_CURRENCY number, BALANCE_STOCK number);

type CALC_ACTIONS_LIST_T is table of CALC_ACTIONS_T;
 
function CALC_ACTIONS (p_cursor CURSOR_ACTIONS_T, p_lag integer, p_initial_balance number default 1000) return CALC_ACTIONS_LIST_T


pipelined order p_cursor by (STOCK_NAME, ADATE) parallel_enable (partition p_cursor by hash (STOCK_NAME));

-- Calc Actions Result

type CALC_ACTIONS_TOTALS_T

is record (STOCK_NAME varchar2(128), ADATE_MIN date, ADATE_MAX date, DEALS_COUNT integer, BALANCE_RESULT number
         , DEALS_PROFIT_AMOUNT number, DEALS_LOSS_AMOUNT number, DEALS_PROFIT_COUNT integer, DEALS_LOSS_COUNT integer
         , IN_STOCK varchar2(16));
type CALC_ACTIONS_TOTALS_LIST_T is table of CALC_ACTIONS_TOTALS_T;
 
function CALC_ACTIONS_TOTALS (p_cursor CURSOR_ACTIONS_T, p_lag integer, p_initial_balance number default 1000) return CALC_ACTIONS_TOTALS_LIST_T


pipelined order p_cursor by (STOCK_NAME, ADATE) parallel_enable (partition p_cursor by hash (STOCK_NAME));

end;


create or replace package body HABR_TRADEMODELLING_P AS

function TRADE_LOG (p_cursor CURSOR_ACTIONS_T, p_lag integer, p_initial_balance number default 1000) return TRADE_LOG_LIST_T

pipelined order p_cursor by (STOCK_NAME, ADATE) parallel_enable (partition p_cursor by hash (STOCK_NAME)) is
    l_aaction_lag               varchar2(10);
    c1                          ACTION_T;
    prev_c1                     ACTION_T;
    retval                      TRADE_LOG_T;
    type ACTION_HISTORY_T is table of varchar2(16);
    l_aaction_history ACTION_HISTORY_T := ACTION_HISTORY_T();
    procedure f_sale (p_sell_price number, p_sell_date date) is
    begin
        retval.APRICE_SELL        := p_sell_price;
        retval.ADATE_LONG_CLOSE   := p_sell_date;
        retval.AAMOUNT_SELL       := round (retval.APRICE_SELL * retval.STOCK_COUNT, 2);
        if - retval.AAMOUNT_BUY + retval.AAMOUNT_SELL > 0

        then retval.DEAL_PROFIT_AMOUNT := - retval.AAMOUNT_BUY + retval.AAMOUNT_SELL;
             retval.DEAL_LOSS_AMOUNT   := null;
        elsif - retval.AAMOUNT_BUY + retval.AAMOUNT_SELL < 0
        then retval.DEAL_PROFIT_AMOUNT := null;
             retval.DEAL_LOSS_AMOUNT   := retval.AAMOUNT_BUY - retval.AAMOUNT_SELL;
        else retval.DEAL_PROFIT_AMOUNT := null;
             retval.DEAL_LOSS_AMOUNT   := null;
        end if;


        retval.BALANCE_RESULT     := retval.BALANCE_RESULT - retval.AAMOUNT_BUY + retval.AAMOUNT_SELL;


    end;
begin
 
    loop


        fetch p_cursor into c1;
        exit when p_cursor%notfound;
    
        if c1.STOCK_NAME is null or c1.ADATE is null or c1.APRICE is null
        then
            raise_application_error (-20001, 'Fields STOCK_NAME, ADATE, APRICE must be not null.');
        end if;
       
        if prev_c1.STOCK_NAME is not null
          and (   (c1.STOCK_NAME < prev_c1.STOCK_NAME)
               or (c1.STOCK_NAME = prev_c1.STOCK_NAME and c1.ADATE < prev_c1.ADATE))
        then
            raise_application_error (-20001, 'Rowset must be ordered by STOCK_NAME, ADATE.');
        end if;
     
        if c1.STOCK_NAME <> prev_c1.STOCK_NAME or prev_c1.STOCK_NAME is null
        then
 
            if (retval.ADATE_LONG_OPEN is not null and retval.ADATE_LONG_CLOSE is null)
            then
                f_sale (prev_c1.APRICE, prev_c1.ADATE);
                retval.IN_STOCK       := 'In Stock';
                pipe row (retval);
            end if;
 
            retval.ADATE_LONG_OPEN    := null;
            retval.ADATE_LONG_CLOSE   := null;
            retval.BALANCE_RESULT   := p_initial_balance;
            retval.STOCK_NAME       := c1.STOCK_NAME;
            retval.IN_STOCK       := null;
            l_aaction_history.delete;           
        end if;

        l_aaction_history.extend(1);
        l_aaction_history(l_aaction_history.last) := c1.AACTION;
 
        if l_aaction_history.last > p_lag
        then
            l_aaction_lag := l_aaction_history (l_aaction_history.last - p_lag);
            if     l_aaction_lag = 'BUY'
            then
                retval.ADATE_LONG_OPEN    := c1.ADATE;
                retval.ADATE_LONG_CLOSE   := null;
                retval.STOCK_COUNT        := floor (1000 * retval.BALANCE_RESULT / c1.APRICE) / 1000;
                retval.APRICE_BUY         := c1.APRICE;
                retval.AAMOUNT_BUY        := round (c1.APRICE * retval.STOCK_COUNT, 2);
                retval.APRICE_SELL        := null;
                retval.AAMOUNT_SELL       := null;
            elsif l_aaction_lag = 'SELL' and retval.ADATE_LONG_OPEN is not null
            then
                f_sale (c1.APRICE, c1.ADATE);
                retval.DAYS_HELD := retval.ADATE_LONG_CLOSE - retval.ADATE_LONG_OPEN;
                pipe row (retval);
                retval.ADATE_LONG_OPEN    := null;
                retval.ADATE_LONG_CLOSE   := null;
            end if;
        end if;
 
        prev_c1 := c1;

    end loop;

    if (retval.ADATE_LONG_OPEN is not null and retval.ADATE_LONG_CLOSE is null)
    then
        f_sale (prev_c1.APRICE, prev_c1.ADATE);
        retval.IN_STOCK       := 'In Stock';
        pipe row (retval);
    end if;

    return;

end; 
function CALC_ACTIONS (p_cursor CURSOR_ACTIONS_T, p_lag integer, p_initial_balance number default 1000) return CALC_ACTIONS_LIST_T

pipelined order p_cursor by (STOCK_NAME, ADATE) parallel_enable (partition p_cursor by hash (STOCK_NAME)) is
    l_deal_currency             number;
    l_deal_stock                number;
    c1                          ACTION_T;
    prev_c1                     ACTION_T;
    retval                      CALC_ACTIONS_T;
    type ACTION_HISTORY_T is table of varchar2(16);
    l_aaction_history ACTION_HISTORY_T := ACTION_HISTORY_T();
begin
 
    loop
 
        fetch p_cursor into c1;
        exit when p_cursor%notfound;
       
        if c1.STOCK_NAME is null or c1.ADATE is null or c1.APRICE is null
        then
            raise_application_error (-20001, 'Fields STOCK_NAME, ADATE, APRICE must be not null.');
        end if;
 
        if prev_c1.STOCK_NAME is not null
          and (   (c1.STOCK_NAME < prev_c1.STOCK_NAME)
               or (c1.STOCK_NAME = prev_c1.STOCK_NAME and c1.ADATE < prev_c1.ADATE))
        then
            raise_application_error (-20001, 'Rowset must be ordered by STOCK_NAME, ADATE.');
        end if;
 
        if c1.STOCK_NAME <> prev_c1.STOCK_NAME or prev_c1.STOCK_NAME is null
        then
            retval.BALANCE_CURRENCY := p_initial_balance;
            retval.BALANCE_STOCK := 0;
            retval.STOCK_NAME := c1.STOCK_NAME;
            l_aaction_history.delete;           
        end if;

        l_aaction_history.extend(1);
        l_aaction_history(l_aaction_history.last) := c1.AACTION;

        if l_aaction_history.last > p_lag
        then
            retval.AACTION_LAG := l_aaction_history(l_aaction_history.last - p_lag);
        
            if     retval.AACTION_LAG = 'BUY'
            then
                l_deal_stock := floor (1000 * retval.BALANCE_CURRENCY / c1.APRICE) / 1000;
                l_deal_currency := round (l_deal_stock * c1.APRICE, 2);
         
                retval.BALANCE_CURRENCY := retval.BALANCE_CURRENCY - l_deal_currency;
                retval.BALANCE_STOCK := retval.BALANCE_STOCK + l_deal_stock;
             elsif retval.AACTION_LAG = 'SELL'
             then
                l_deal_currency := round (retval.BALANCE_STOCK * c1.APRICE, 2);
                l_deal_stock := retval.BALANCE_STOCK;
                retval.BALANCE_CURRENCY := retval.BALANCE_CURRENCY + l_deal_currency;
                retval.BALANCE_STOCK := retval.BALANCE_STOCK - l_deal_stock;
            end if;
        else
            retval.AACTION_LAG := null;
        end if;
 
        retval.ADATE := c1.ADATE;
        retval.APRICE := c1.APRICE;
        retval.AACTION := c1.AACTION;

        pipe row (retval);
 
        prev_c1 := c1;
 
    end loop;

    return;
 
end;




function CALC_ACTIONS_TOTALS (p_cursor CURSOR_ACTIONS_T, p_lag integer, p_initial_balance number default 1000)
return CALC_ACTIONS_TOTALS_LIST_T
pipelined order p_cursor by (STOCK_NAME, ADATE) parallel_enable (partition p_cursor by hash (STOCK_NAME)) is
    l_deal_amount_of_buy       number;
    l_deal_stock                number;
    l_deal_amount               number;
    l_aaction_lag               varchar2(10);
    c1                          ACTION_T;
    prev_c1                     ACTION_T;
    retval                      CALC_ACTIONS_TOTALS_T;
    type ACTION_HISTORY_T is table of varchar2(16);
    l_aaction_history ACTION_HISTORY_T := ACTION_HISTORY_T();
    procedure f_sale (p_sale_price number) is
    begin
        if l_deal_stock > 0
        then
            l_deal_amount := - l_deal_amount_of_buy + round (l_deal_stock * p_sale_price, 2);
            l_deal_stock := 0;
 
            if    l_deal_amount > 0 then -- profit
 
                retval.DEALS_PROFIT_AMOUNT := retval.DEALS_PROFIT_AMOUNT + l_deal_amount;
                retval.DEALS_PROFIT_COUNT  := retval.DEALS_PROFIT_COUNT + 1;
 
            elsif l_deal_amount < 0 then -- loss
 
                retval.DEALS_LOSS_AMOUNT   := retval.DEALS_LOSS_AMOUNT   - l_deal_amount;
                retval.DEALS_LOSS_COUNT    := retval.DEALS_LOSS_COUNT + 1;
 
            end if;
 
            retval.BALANCE_RESULT  := retval.BALANCE_RESULT + l_deal_amount;
            retval.IN_STOCK := 'In stock';
        else
            retval.IN_STOCK := null;
        end if;
 
    end;
begin
 
    loop
 
        fetch p_cursor into c1;
        exit when p_cursor%notfound;
       
        if c1.STOCK_NAME is null or c1.ADATE is null or c1.APRICE is null
        then
            raise_application_error (-20001, 'Fields STOCK_NAME, ADATE, APRICE must be not null.');
        end if;
         
        if prev_c1.STOCK_NAME is not null
          and (   (c1.STOCK_NAME < prev_c1.STOCK_NAME)
               or (c1.STOCK_NAME = prev_c1.STOCK_NAME and c1.ADATE < prev_c1.ADATE))
        then
            raise_application_error (-20001, 'Rowset must be ordered by STOCK_NAME, ADATE.');
        end if;
       
        if c1.STOCK_NAME <> prev_c1.STOCK_NAME or prev_c1.STOCK_NAME is null
        then
 
            if prev_c1.STOCK_NAME is not null then
                f_sale (prev_c1.APRICE);
                pipe row (retval);
            end if;
           
            retval.BALANCE_RESULT := p_initial_balance;
 
            retval.DEALS_COUNT := 0;
            retval.STOCK_NAME := c1.STOCK_NAME;
 
            retval.DEALS_PROFIT_AMOUNT := 0;
            retval.DEALS_LOSS_AMOUNT   := 0;
            retval.DEALS_PROFIT_COUNT  := 0;
            retval.DEALS_LOSS_COUNT    := 0;
            l_aaction_history.delete;           
        end if;
   
        l_aaction_history.extend(1);
        l_aaction_history(l_aaction_history.last) := c1.AACTION;
 
        if l_aaction_history.last > p_lag
        then
            l_aaction_lag := l_aaction_history (l_aaction_history.last - p_lag);
            if     l_aaction_lag = 'BUY'
            then
                l_deal_stock        := floor (1000 * retval.BALANCE_RESULT / c1.APRICE) / 1000;
                l_deal_amount_of_buy := round (l_deal_stock * c1.APRICE, 2);
        
                retval.DEALS_COUNT := retval.DEALS_COUNT + 1;
                                
            elsif l_aaction_lag = 'SELL'
            then
                f_sale (c1.APRICE);
            end if;
        end if;
 
        prev_c1 := c1;
  
    end loop;
   
    if prev_c1.STOCK_NAME is not null
    then
        f_sale (prev_c1.APRICE);
        pipe row (retval);
    end if;
   
    return;
 
end;
 
end;


Загрузите данные

Расчёты приведены для таких рынков и индексов: S&P500, NYSE, Brent, BTCUSD, EURUSD.
Первые 4 загружены с сайта Yahoo Finance, последний — из другого источника. Результаты подобных расчётов для курсов из других источников могут отличаться.

Обратите внимание, что периоды курсов каждого инструмента различаются, а именно:

  • S&P500 —03 /01/1950…29/01/2019, 69 лет;
  • NYSE — 31/12/1965…22/03/2019, 54 года;
  • Brent — 17/05/1991…06/02/2019, 18 лет;
  • BTCUSD — 16/07/2010…29/01/2019, 9 лет;
  • EURUSD — 16/02/2001…27/05/2019, 19 лет.


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

Файл для загрузки (SQL Loader) можно тоже взять с GitHub по ссылке внизу статьи.



Скользящие средние


Есть по крайней мере три основных вида скользящих средних: линейная (Simple Мoving Average, SMA), экспоненциальная (Exponential Moving Average, EMA) и линейно-взвешенная (Weighted Moving Average, WMA). Они отличаются весами входящих в них составляющих. Для линейной скользящей средней веса равны, для экспоненциальной и линейно-взвешенной веса убывают по мере отдаления составляющей от правого края окна — по экспоненциальному закону или линейно.
Линейную скользящую среднюю вычислить проще всего. В Oracle это функция avg (VALUE) over (partition by STOCK_NAME order by ADATE rows between 9 preceding and current row) — скользящая средняя с величиной окна усреднения равным 10 значениям.

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

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

В расчётах ТИР используются все три вида скользящих средних. Для экспоненциальной и линейно взвешенной в этой статье разработаны агрегатные функции EMA и WMA, которые используются в аналитической форме. Кроме того, эти скользящие средние можно вычислить рекурсией или моделькой (фраза MODEL).

Расчёт EMA и WMA, без рекурсии или модельки, одной лишь аналитикой, в Oracle, судя по всему, невозможен. Только процедурно.

Но ещё оговорки касательно скользящих средних:
  • чем короче период усреднения и чем более чутко метод реагирует на развороты, тем больше он даёт ложных сигналов;
  • чем короче период усреднения, тем больше сигналов генерируется, тем больше накладные расходы на транзакции, которые могут стать весьма значительными.


Технические индикаторы рынка (ТИР)

Для почти каждого индикатора будут приведены несколько методов расчёта: CALC — расчёт с помощью PL/SQL кода, SIMP — расчёт одним оператором, RECU — расчёт рекурсией, AGRF  расчёт с использованием агрегатной функции, MODE — расчёт моделькой.

Для разработки нескольких методов есть свои причины. Во-первых, рассчитав ТИР несколькими методами и сравнив показатели, если показатели совпадают, можно быть уверенным, что расчёты проведены верно (учитывая разные методы округления и обработки значений NULL и «0»). В этой статье я буду сравнивать хэши выборок различных методов, поэтому гарантируется совпадение до бита и одинаковая обработка всеми алгоритмами.


Полагаю, в Oracle начинать разработку ТИР нужно с метода SIMP — расчёт одним оператором. Когда это сделано, у «ораклоида» складывается в голове план и алгоритм расчёта, и его можно легко переложить на PL/SQL или на другой процедурный язык.


Замечу, что метод расчёта CALC на PL/SQL здесь оказывается быстрее метода SIMP (одним оператором) в том случае, если весь расчёт можно выполнить за один проход по курсору. А вот если для расчёта придётся формировать временные таблицы или коллекции, или более, чем 1 проход по курсору — полагаю, метод «одним оператором» окажется и быстрее, и менее ресурсоёмким.


Для всех методов, включая метод SIMP («одним оператором») я буду помещать операторы в функции, для того, чтобы расчёт можно было вызвать с параметром для подбора оптимального значения.



Расчёты приведены для семи ТИР: Пересечение скользящей средней (EMASIMPLE), «Золотой» и «Смертельный кресты (CROSSES), Балансовый объём (OBV), Канал Кельтнера, Тренд цены и объёма (PVT), Индикатор лёгкости движения Армса (EMV), Индекс товарного канала (CCI).

Для семи индикаторов этих двух статей будет один параметр «величина окна усреднения», и второй параметр — сдвиг (lag). Сдвиг говорит, на сколько баров нужно отложить исполнение приказа BUY/SELL — на сколько баров сдвинуть цену закрытия (все приказы совершаются по цене закрытия бара). Это подобно «проскальзыванию» (“slippage”), но это не совсем «проскальзывание». Проскальзывание обычно оказывается не в пользу клиента, а наш лаг, может оказаться как не в пользу клиента, так и в пользу. Тем не менее, использование моделирования с лагом от 1 до 5 баров для некоторых индикаторов показывает, что проскальзывание оказывает значительный эффект на результат. А для некоторых индикаторов лаг и проскальзывание не так важны.


Методы, кроме SIMP (одним оператором) я буду помещать под спойлеры.


В методе SIMP («одним оператором») активно используется фраза MATCH_RECOGNIZE, для формирования торгового сигнала BUY/SELL на основании вхождения/покидания рассчитанного ТИР в заданный диапазон или его поведения относительно своего скользящего среднего.


Подробные описания всех ТИР можно посмотреть в Википедии или в книге Роберта Колби «Энциклопедия технических индикаторов рынка».




Пересечение экспоненциальной скользящей средней


Метод пересечения экспоненциальных скользящих средних является простейшим ТИР.

Метод предполагает: покупку (открытие длинной позиции), если значение цены пересекает снизу-вверх свою экспоненциальную скользящую среднюю (ЭСС); продажу (закрытие длинной позиции), если цена пересекает свою ЭСС сверху вниз.

Короткие позиции, как и маржинальная торговля в целом, в этой статье не рассматриваются.

В дальнейшем, именно этот индикатор можно использовать как эталон для сравнения с другими. Сравнение с этим ТИР лучше, чем сравнение со стратегией «Купи и держи», потому что эта стратегия не выгодна на падающих рынках.

Единственный параметр ТИР — длина скользящей средней

Расчёт с помощью PL/SQL


create or replace function HABR_MARKETINDEXES_EMASIMPLE_F_CALC (p_averaging_window_width integer)

return HABR_MARKETINDEXES_RESULT_LIST_T pipelined is

    l_result HABR_MARKETINDEXES_RESULT_LIST_T;

    EMA number;

    prev_EMA number;

    prev_TYPICAL_PRICE number;

    retval HABR_MARKETINDEXES_RESULT_T := HABR_MARKETINDEXES_RESULT_T (null, null, null, null, null, null, null, null, null);
    prev_STOCK_NAME varchar2(256);
    l_alpha number;
begin

    l_alpha := 2 / (p_averaging_window_width + 1);

    for c1 in (select STOCK_NAME, ADATE, TYPICAL_PRICE, ACLOSE from LOAD_YAHOO_V order by 1, 2)
    loop
   
        retval.ADATE        := c1.ADATE;
        retval.ACLOSE       := c1.ACLOSE;

        if prev_STOCK_NAME is null or prev_STOCK_NAME <> c1.STOCK_NAME
        then
            retval.STOCK_NAME   := c1.STOCK_NAME;
            EMA                 := c1.TYPICAL_PRICE;
            prev_EMA            := null;
        else
            EMA := round (c1.TYPICAL_PRICE * l_alpha + EMA * (1 - l_alpha), 20);
        end if;
       
        if    prev_TYPICAL_PRICE < prev_EMA and c1.TYPICAL_PRICE > EMA then retval.AACTION := 'BUY';
        elsif prev_TYPICAL_PRICE > prev_EMA and c1.TYPICAL_PRICE < EMA then retval.AACTION := 'SELL';
        else  retval.AACTION := null;
        end if;

        retval.IND_VALUE  := EMA;
       
        pipe row (retval);
       
        prev_STOCK_NAME := c1.STOCK_NAME;
        prev_EMA := EMA;
        prev_TYPICAL_PRICE := c1.TYPICAL_PRICE;
       
    end loop;
end;

Расчёт одним рекурсивным оператором


create or replace function HABR_MARKETINDEXES_EMASIMPLE_F_RECU (p_averaging_window_width integer)

return HABR_MARKETINDEXES_RESULT_LIST_T is

    l_result HABR_MARKETINDEXES_RESULT_LIST_T;

begin



    with

      T1 (STOCK_NAME, ADATE, TYPICAL_PRICE, EMA, ACLOSE, RN) as
           (select STOCK_NAME, ADATE, TYPICAL_PRICE, round (TYPICAL_PRICE, 20), ACLOSE, RN from LOAD_YAHOO_V where RN = 1
            union all
            select b.STOCK_NAME
                 , b.ADATE
                 , b.TYPICAL_PRICE
                 , round (b.TYPICAL_PRICE * 2 / (p_averaging_window_width + 1) + a.EMA * (1 - 2 / (p_averaging_window_width + 1)), 20)
                 , b.ACLOSE
                 , b.RN
            from T1 a, LOAD_YAHOO_V b
            where b.RN = a.RN + 1 and b.STOCK_NAME = a.STOCK_NAME)
    select HABR_MARKETINDEXES_RESULT_T (STOCK_NAME, ADATE, ACLOSE, EMA, null, null, null, null, AACTION)
    bulk collect into l_result
    from T1 match_recognize (partition by STOCK_NAME order by ADATE
                             measures classifier() as AACTION
                             all rows per match with unmatched rows
                             pattern (BUY+ | SELL+)
                             define BUY  as (prev (TYPICAL_PRICE) < prev (EMA) and TYPICAL_PRICE > EMA)
                                  , SELL as (prev (TYPICAL_PRICE) > prev (EMA) and TYPICAL_PRICE < EMA)
                             ) MR;

    return l_result;
end;

Расчёт моделькой

create or replace function HABR_MARKETINDEXES_EMASIMPLE_F_MODE (p_averaging_window_width integer)
return HABR_MARKETINDEXES_RESULT_LIST_T is
    l_result HABR_MARKETINDEXES_RESULT_LIST_T;
begin

    with
      T1 as (select * from LOAD_YAHOO_V
             model dimension by (STOCK_NAME, RN) measures (ADATE, TYPICAL_PRICE, ACLOSE, to_number(null) as EMA)
             rules (EMA[any, any] = round (TYPICAL_PRICE [cv(), cv()] * 2 / (p_averaging_window_width     + 1) + nvl(EMA [cv(), cv() - 1], TYPICAL_PRICE [cv(), cv()]) * (1 - 2 / (p_averaging_window_width     + 1)), 20)))
    , T2 as (select STOCK_NAME, ADATE, ACLOSE
                  , TYPICAL_PRICE, LAG (TYPICAL_PRICE) over (partition by STOCK_NAME order by ADATE) as PREV_TYPICAL_PRICE
                  , EMA, lag (EMA) over (partition by STOCK_NAME order by ADATE) as PREV_EMA
             from T1)
    select HABR_MARKETINDEXES_RESULT_T (STOCK_NAME, ADATE, ACLOSE, EMA, null, null, null, null
                                      , case when prev_TYPICAL_PRICE < prev_EMA and TYPICAL_PRICE > EMA then 'BUY'
                                             when prev_TYPICAL_PRICE > prev_EMA and TYPICAL_PRICE < EMA then 'SELL' end)
    bulk collect into l_result
    from T2 order by STOCK_NAME, ADATE;
   
    return l_result;

end;

Расчёт агрегатной функцией в аналитической форме

create or replace type EMA_DATA_T as object (AVALUE number, AVERAGING_WINDOW integer);

create or replace type EMA_IMPL_T as object
(
    l_window_width integer,
    l_ema  number,
    static function ODCIAggregateInitialize   (sctx in out EMA_IMPL_T) return number,
    member function ODCIAggregateIterate      (self in out EMA_IMPL_T, value in EMA_DATA_T) return number,
    member function ODCIAggregateMerge        (self in out EMA_IMPL_T, ctx2 in EMA_IMPL_T) return number,
    member function ODCIAggregateTerminate    (self in EMA_IMPL_T, returnValue out number, flags in number) return number
);

create or replace type body EMA_IMPL_T is

static function ODCIAggregateInitialize (sctx in out EMA_IMPL_T) return number is
begin
    sctx := EMA_IMPL_T (null, null);
    return ODCIConst.Success;
end;

member function ODCIAggregateIterate (self in out EMA_IMPL_T, value in EMA_DATA_T) return number is
begin

    if value.AVALUE is not null
    then

        if l_window_width is null
        then
            l_window_width := value.AVERAGING_WINDOW;
            self.l_ema := value.AVALUE;
        else
            self.l_ema := round (value.AVALUE * 2 / (l_window_width  + 1) + self.l_ema * (1 - 2 / (l_window_width  + 1)), 20);
        end if;
    end if;
   
    return ODCIConst.Success;
end;

member function ODCIAggregateMerge(self in out EMA_IMPL_T, ctx2 in EMA_IMPL_T) return number is
begin
    return ODCIConst.Error;
end;

member function ODCIAggregateTerminate(self in EMA_IMPL_T, returnValue out number, flags in number) return number is
begin
    returnValue := self.l_ema;
    return ODCIConst.Success;
end;

end;

create or replace function EMA (input EMA_DATA_T) return number aggregate using EMA_IMPL_T;
/


Создадим функцию для расчёта собственно ТИР:

create or replace function HABR_MARKETINDEXES_EMASIMPLE_F_AGRF (p_averaging_window_width integer)
return HABR_MARKETINDEXES_RESULT_LIST_T is
    l_result HABR_MARKETINDEXES_RESULT_LIST_T;
begin

    with
      T1 as (select STOCK_NAME, ADATE, TYPICAL_PRICE, ACLOSE
                  , round (EMA (EMA_DATA_T (TYPICAL_PRICE, p_averaging_window_width)) over (partition by STOCK_NAME order by ADATE), 20) as EMA
             from LOAD_YAHOO_V)
    select HABR_MARKETINDEXES_RESULT_T (STOCK_NAME, ADATE, ACLOSE, EMA, null, null, null, null, AACTION)
    bulk collect into l_result
    from T1 match_recognize (partition by STOCK_NAME order by ADATE
                             measures classifier() as AACTION
                             all rows per match with unmatched rows
                             pattern (BUY+ | SELL+)
                             define BUY  as (prev (TYPICAL_PRICE) < prev (EMA) and TYPICAL_PRICE > EMA)
                                  , SELL as (prev (TYPICAL_PRICE) > prev (EMA) and TYPICAL_PRICE < EMA)
                             ) MR;

    return l_result;
end;

Сравним результаты расчёта с одним параметром

select COLUMN_VALUE as ALG, dbms_sqlhash.gethash (COLUMN_VALUE, 2) as RECORDSET_HASH
from table (sys.odcivarchar2list ('select * from table (HABR_MARKETINDEXES_EMASIMPLE_F_CALC (15)) order by 1, 2'
                                , 'select * from table (HABR_MARKETINDEXES_EMASIMPLE_F_RECU (15)) order by 1, 2'
                                , 'select * from table (HABR_MARKETINDEXES_EMASIMPLE_F_MODE (15)) order by 1, 2'
                                , 'select * from table (HABR_MARKETINDEXES_EMASIMPLE_F_AGRF (15)) order by 1, 2'));

Все хэши должны совпадать для всех четырёх методов

Если хэши не сошлись, разобраться, где именно образовалось расхождение, можно таким оператором (подставьте имена таблиц, которые надо сравнить):

select coalesce (a.STOCK_NAME, b.STOCK_NAME) as STOCK_NAME, coalesce (a.ADATE, b.ADATE) as ADATE
     , a.ACLOSE as CALC_ACLOSE, b.ACLOSE as AGRF_CLOSE
     , a.IND_VALUE as CALC_EMA, b.IND_VALUE as AGRF_EMA
     , a.AACTION as CALC_AACTION, b.AACTION as AGRF_AACTION
from table (HABR_MARKETINDEXES_EMASIMPLE_F_CALC (15)) a
full outer join table (HABR_MARKETINDEXES_EMASIMPLE_F_AGRF (15)) b on a.STOCK_NAME = b.STOCK_NAME and a.ADATE = b.ADATE
--where sys_op_map_nonnull (a.ACLOSE) <> sys_op_map_nonnull (b.ACLOSE)
--   or sys_op_map_nonnull (a.IND_VALUE) <> sys_op_map_nonnull (b.IND_VALUE)
--   or sys_op_map_nonnull (a.AACTION) <> sys_op_map_nonnull (b.AACTION)
order by 1, 2;
;

Подбор параметров

Сам ТИР использует один параметр, для этого и для всех других ТИР я изменяю его в диапазоне от 1 до 200, но для расчёта трёхмерной картинки для зависимости и от лага тоже, мы введём второй параметр, который будет изменяться от 1 до 5.

Оператор открывает 200 * 5 = 1000 курсоров, поэтому может потребоваться изменение параметра Oracle OPEN_CURSORS

Запрос ниже выполняет декартово произведение таблицы с числами от 1 до 200, с таблицей с числами от 1 до 5, и декартово умножает всё это на вызов табличной функции HABR_TRADEMODELLING_P.CALC_ACTIONS_TOTALS.

Путём некоторых дальнейших манипуляций в MATLAB далее получим матрицу 200*5, где в ячейках матрицы будет итоговая величина капитала при каждом значении каждого из двух параметров. Далее в MATLAB строим трёхмерную картинку.

rollback;

delete HABR_MARKETINDEXES_PARMSEL_RESULTS where INDICATOR_NAME = 'HABR_MARKETINDEXES_EMASIMPLE_F_CALC';
commit;

insert into HABR_MARKETINDEXES_PARMSEL_RESULTS (INDICATOR_NAME, PARM1, PARM2, STOCK_NAME, ADATE_MIN, ADATE_MAX, DEALS_COUNT, BALANCE_RESULT, DEALS_PROFIT_AMOUNT, DEALS_LOSS_AMOUNT, DEALS_PROFIT_COUNT, DEALS_LOSS_COUNT, IN_STOCK)
with
  TP1 as (select rownum as PARM1 from dual connect by level <= &&AVERAGING_INTERVAL)
, TP2 as (select rownum as PARM2 from dual connect by level <= &&LAG_MODELLING_DEPTH)
select --+ parallel(8)
       'HABR_MARKETINDEXES_EMASIMPLE_F_CALC', PARM1, PARM2, STOCK_NAME, ADATE_MIN, ADATE_MAX, DEALS_COUNT, BALANCE_RESULT, DEALS_PROFIT_AMOUNT, DEALS_LOSS_AMOUNT, DEALS_PROFIT_COUNT, DEALS_LOSS_COUNT, IN_STOCK
from TP1
cross join TP2
cross join table (HABR_TRADEMODELLING_P.CALC_ACTIONS_TOTALS (cursor (select STOCK_NAME, ADATE, ACLOSE, AACTION from table (HABR_MARKETINDEXES_EMASIMPLE_F_CALC (PARM1))), PARM2))
;
commit;


Все расчёты в этой статье выполняются медленно, до 20 минут, это связано с открытием большого числа курсоров

Я разработал и более быстрый метод моделирования с одним курсором, без открытия 1000 курсоров, но он настолько объёмный, что занял бы половину статьи. Поэтому я его здесь приводить не буду.

Итог моделирования пересечения скользящей средней



Вторая строка графиков  это то-же, что и первая строка, но графики немного повёрнуты, графики с различным смещением располагаются один за другим. Это позволяет оценить влияние лага на результат

В целом, индикатор не очень чувствителен к лагам. Для рынков S&P500 и NYSE параметр ТИР нужно выбирать чем больше, тем лучше. Для рынка Brent - в районе 25. На остальных двух рынках зависимости между прибыльностью и параметром нет.

Смертельный и Золотой кресты

В реализации на Oracle этот индикатор очень похож на предшествующий, только здесь используется две скользящие средние, а не одна. Поэтому я приведу только один вариант расчёта.

В Вики описан «Индикатор Ишимоку». Это сложный индикатор. «Кресты» являются одной из составных частей. Но индикатор описан на Вики плохо, в частности, обратите внимание, что линии Tenkan и Kijun описаны совершенно разными словами, хотя по сути это одно и то-же, но с разными периодами.

В книге Роберта Колби этот индикатор тоже не описан.

Японские аналитики называют пересечение средних, когда краткосрочная средняя пересекает долгосрочную снизу вверх, Золотой крест (Golden Cross), а обратную ситуацию, когда краткосрочная скользящая средняя пересекает долгосрочную сверху вниз — Мёртвый крест (Dead Cross).

Автор обратил вниманием, что этот индикатор описан в статье «Фьючерсы на американскую нефть сформировали «смертельный крест»»
и стал гуглить его описание и использование.

Этот индикатор считается серьёзным на фондовом рынке, отчасти потому, что он подаёт сигналы редко.

Наиболее часто используются 50-ти и 200-периодные скользящие средние.

При моделировании результативности ниже мы примем период длинной скользящей средней равным учетверённому периоду короткой скользящей средней, и смоделируем длину короткой от 1 до 200 дней (получится от 4 до 800 дней для длинной).

Расчёт PL/SQL кодом

Эта реализация индикатора тоже выдаёт сигналы BUY и SELL. Это нужно для того, чтобы этот расчёт можно было подключить к процедуре моделирования, описанной выше (она понимает поля AACTION только “BUY” и “SELL”). Но интерпретировать сигналы нужно так: BUY — Golden Cross, SELL — Dead Cross.

create or replace function HABR_MARKETINDEXES_CROSSES_F_CALC (p_averaging_window_width integer)
return HABR_MARKETINDEXES_RESULT_LIST_T pipelined is
    l_result HABR_MARKETINDEXES_RESULT_LIST_T;
    EMAS number;
    prev_EMAS number;
    EMAL number;
    prev_EMAL number;
    retval HABR_MARKETINDEXES_RESULT_T := HABR_MARKETINDEXES_RESULT_T (null, null, null, null, null, null, null, null, null);
    prev_STOCK_NAME varchar2(256);
    l_alpha_short number;
    l_alpha_long number;
begin

    l_alpha_short := 2 / (p_averaging_window_width     + 1);
    l_alpha_long  := 2 / (p_averaging_window_width * 4 + 1);

    for c1 in (select STOCK_NAME, ADATE, TYPICAL_PRICE, ACLOSE from LOAD_YAHOO_V order by 1, 2)
    loop
   
        retval.ADATE        := c1.ADATE;
        retval.ACLOSE       := c1.ACLOSE;

        if prev_STOCK_NAME is null or prev_STOCK_NAME <> c1.STOCK_NAME
        then
            retval.STOCK_NAME   := c1.STOCK_NAME;
            EMAS      := c1.TYPICAL_PRICE;
            EMAL      := c1.TYPICAL_PRICE;
            prev_EMAS := null;
            prev_EMAL := null;
        else
            EMAS := round (c1.TYPICAL_PRICE * l_alpha_short + EMAS * (1 - l_alpha_short), 20);
            EMAL := round (c1.TYPICAL_PRICE * l_alpha_long  + EMAL * (1 - l_alpha_long), 20);

        end if;

        if    prev_EMAS < prev_EMAL and EMAS > EMAL then retval.AACTION := 'BUY';
        elsif prev_EMAS > prev_EMAL and EMAS < EMAL then retval.AACTION := 'SELL';
        else  retval.AACTION := null;
        end if;      

        retval.IND_VALUE  := EMAS;
        retval.IND_VALUE2 := EMAL;
       
        pipe row (retval);
       
        prev_STOCK_NAME := c1.STOCK_NAME;
        prev_EMAS := EMAS;
        prev_EMAL := EMAL;

    end loop;
end;

Подбор параметров

rollback;

delete HABR_MARKETINDEXES_PARMSEL_RESULTS where INDICATOR_NAME = 'HABR_MARKETINDEXES_CROSSES_F_CALC';
commit;

insert into HABR_MARKETINDEXES_PARMSEL_RESULTS (INDICATOR_NAME, PARM1, PARM2, STOCK_NAME, ADATE_MIN, ADATE_MAX, DEALS_COUNT, BALANCE_RESULT, DEALS_PROFIT_AMOUNT, DEALS_LOSS_AMOUNT, DEALS_PROFIT_COUNT, DEALS_LOSS_COUNT, IN_STOCK)
with
  TP1 as (select rownum as PARM1 from dual connect by level <= &&AVERAGING_INTERVAL)
, TP2 as (select rownum as PARM2 from dual connect by level <= &&LAG_MODELLING_DEPTH)
select --+ parallel(8)
       'HABR_MARKETINDEXES_CROSSES_F_CALC', PARM1, PARM2, STOCK_NAME, ADATE_MIN, ADATE_MAX, DEALS_COUNT, BALANCE_RESULT, DEALS_PROFIT_AMOUNT, DEALS_LOSS_AMOUNT, DEALS_PROFIT_COUNT, DEALS_LOSS_COUNT, IN_STOCK
from TP1
cross join TP2
cross join table (HABR_TRADEMODELLING_P.CALC_ACTIONS_TOTALS (cursor (select STOCK_NAME, ADATE, ACLOSE, AACTION from table (HABR_MARKETINDEXES_CROSSES_F_CALC (PARM1))), PARM2)) a
;
commit;


Итог моделирования "Крестов" (Индикатора Ишимоку)




Индикатор не очень чувствителен к лагам. Для рынка S&P500 есть максимумы на 48 (192 для длинной СС) и 98 дней (392 для длинной СС). Заметьте, первый максимум очень близко к числам 50х200. Можно предположить, что если этот параметр выбрать на 1 и 2 меньше, чем другие участники рынка, можно попробовать их обыграть на одном этом индикаторе.

На NYSE 4 максимума. На рынках Brent и BTCUSD индикатор не работает.
Для рынка EURUSD тоже параметр надо выбирать чуть меньше, чем 50 для короткой СС. Но вот прибыли на этом рынке индикатор не даёт. Его можно использовать только как дополнительный индикатор.



Для остальных индикаторов я не буду приводить код в статье.
Весь код и данные можно скачать с github по ссылке внизу статьи


Балансовый объём, Равновесный объём, On-Balance Volume (OBV)

Индикатор OBV представляет собой кумулятивную скользящую среднюю объёма торгов, взятого со знаком плюс в случае растущего рынка и со знаком минус в случае падающего.

Здесь будет использован такой способ толкования значения индикаторов: купить, когда OBV пересечёт свою скользящую среднюю снизу-вверх, продать, когда OBV пересечёт свою скользящую среднюю сверху-вниз.

Подробнее на Вики или у Колби
https://ru.wikipedia.org/wiki/%D0%91%D0%B0%D0%BB%D0%B0%D0%BD%D1%81%D0%BE%D0%B2%D1%8B%D0%B9_%D0%BE%D0%B1%D1%8A%D1%91%D0%BC


На рынке S&P500 индикатор очень чувствителен к задержкам (лагам), но можно попробовать выбирать параметр по принципу «чем больше, тем лучше», некоторая прибыльность достигается. На рынке NYSE прибыльности достигнуть не удастся. На рынке Brent можно выбирать значение параметра от 20 до 100. На рынке BTCUSD чёткой линейной зависимости нет, но выбирать значение параметра меньше 40 нецелесообразно. Для рынка EURUSD значение параметра нужно выбирать «чем больше, тем лучше», но прибыльности достигнуть не удастся.

Канал Кельтнера, Keltner Channel

ТИР состоит из двух полос над и под скользящей средней цены, ширина которых определяется как доля от среднего изменения цены за период. В качестве цены берётся типичная цена.

Средняя линии индикатора является простой скользящей средней от типичной цены.

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

В наших расчётах сигнал ТИР будет формироваться так: Покупай, если цена пересекает верхнюю линию, Продавай, если цена пересекает нижнюю




Для рынков S&P500 и NYSE есть максимумы. На рынке Brent зависимости результата от параметра нет, и вся торговля идёт в убыток. На рынке BTCUSD максимумы есть, но узкие, и в них трудно попасть. На рынке EURUSD зависимость результата от параметра есть, но опять же, вся торговля идёт в убыток

Тренд цены и объёма, Тенденция цены и объёма, PriceVolume Trend, PVT

Значение индикатора PVT представляет собой кумулятивную сумму произведения текущего объёма торгов на приведённый рост цены относительно предыдущего периода.


Формировать торговые сигналы здесь мы будем следующим образом: Покупай, когда PVT пересечёт свою скользящую среднюю снизу-вверх, Продавай, когда PVT пересечёт свою скользящую среднюю сверху-вниз.

Подробнее на Вики


У Колби не нашёл.



Для рынка S&P500 значение параметра «чем больше, тем лучше», на NYSE почти нет зависимости, для Brent есть максимум в районе 50, на BTCUSD индикатор эффективен только при минимальных лагах, 1-2, и с минимальными значениями параметра, до 50-ти, для EURUSD зависимость возрастающая

Лёгкость движения, Индикатор лёгкости движения Армса, Arms’ Ease of Movement Value, EMV

ТИР EMV — это численное выражение того, насколько легко изменяется цена на рынке. Чем сильнее изменение цены и чем ниже при этом оборот, тем легче растет или падает рынок.


Торговые приказы здесь будем формировать так: Покупай, когда скользящая средняя EMV поднимается выше нуля, Продавай, когда скользящая средняя EMV опускается ниже нуля.


Подробнее о ТИР на Вики или у Колби
https://ru.wikipedia.org/wiki/%D0%9B%D1%91%D0%B3%D0%BA%D0%BE%D1%81%D1%82%D1%8C_%D0%B4%D0%B2%D0%B8%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F


На рынках S&P, NYSE есть максимумы, на рынке Brent индикатор эффективен в широком диапазоне значений параметров. На рынках BTCUSD и EURUSD индикатор неэффективен.


Индекс товарного канала, Сommodity channel index, CCI

ТИР CCI — индикатор, основанный на анализе текущего изменения отклонения цены от её среднего значения за определённый период и среднестатистического абсолютного значения этого параметра. ТИР применим к любым финансовым рынкам.


Торговые приказы будем формировать так: Покупай, когда CCI поднимается выше 100, Продавай, когда CCI опускается ниже 100.


Подробнее на Вики или у Колби
https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%B4%D0%B5%D0%BA%D1%81_%D1%82%D0%BE%D0%B2%D0%B0%D1%80%D0%BD%D0%BE%D0%B3%D0%BE_%D0%BA%D0%B0%D0%BD%D0%B0%D0%BB%D0%B0




На рынках S&P500, NYSE, Brent нет зависимости результата торговли от значения параметра. На рынке BTCUSD зависимость есть (два чётких максимума), и в реальной торговле её можно попробовать поймать. На рынке EURUSD индикатор результата не принесёт.



Исходники доступны на https://github.com/yaroslavbat/habr_article2/

Создайте общие объекты,
загрузите данные, либо мои (как я говорил раньше, все рынки кроме последнего взяты с Yahoo Finance), либо свои,
создайте функции для расчёта,
рассчитайте данные результативности торгов (как я говорил раньше, расчёт каждого индикатора до 20 минут)


Общий вывод относительно ТИР: использование одного только индикатора для извлечения прибыли невозможно. Разные индикаторы по разному эффективны на разных рынках. Возможность получения прибыли при использовании нескольких хорошо настроенных индикаторов плюс дополнительной внешней информации не исключена.

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

Выполнять расчёты и сложное моделирование ТИР и финансовой деятельности вообще на Oracle Database весьма удобно.

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

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