Поиск ассоциативных правил

Методы Data Mining и Machine Learning позволяют автоматизировать работу трейдера-исследователя, который пытается найти закономерности в котировках финансовых инструментов, чтобы использовать эти закономерности в своей торговле.

Исследуем, как ведут себя дневные свечи фьючерса S&P 500 в зависимости от дня недели.

Загрузим данные по индексу S&P 500 с сайта Yahoo Finance:

#from pandas.io.data import DataReader  # Для старых версий pandas.
from pandas_datareader.data import DataReader
import datetime
symbol = "^GSPC"  # Код финансового инструмента на сайте Yahoo Finance (индекс S&P 500).
start_date = datetime.datetime(2013, 12, 27) # Начальная дата - 27 декабря 2013 (пятница).
end_date = datetime.datetime(2015, 12, 25)   # Конечная дата - 25 декабря 2015 (пятница).
ts1 = DataReader(symbol, "yahoo", start_date, end_date) # Отправили запрос на сервер и получили данные.

import pandas as pd
pd.set_option('display.width', 500) # Чтобы не переносились строки при выводе широких таблиц.
print(ts1.head(6)) # Вывели первые 6 строк полученных данных.
print(90*'.')      # Вывели строку из 90 точек.
print(ts1.tail(3)) # Вывели последние 3 строки данных.

Получим:

                   Open         High          Low        Close      Volume    Adj Close
Date
2013-12-27  1842.969971  1844.890015  1839.810059  1841.400024  2052920000  1841.400024
2013-12-30  1841.469971  1842.469971  1838.770020  1841.069946  2293860000  1841.069946
2013-12-31  1842.609985  1849.439941  1842.410034  1848.359985  2312840000  1848.359985
2014-01-02  1845.859985  1845.859985  1827.739990  1831.979980  3080600000  1831.979980
2014-01-03  1833.209961  1838.239990  1829.130005  1831.369995  2774270000  1831.369995
2014-01-06  1832.310059  1837.160034  1823.729980  1826.770020  3294850000  1826.770020
..........................................................................................
                   Open         High          Low        Close      Volume    Adj Close
Date
2015-12-22  2023.150024  2042.739990  2020.489990  2038.969971  3520860000  2038.969971
2015-12-23  2042.199951  2064.729980  2042.199951  2064.290039  3484090000  2064.290039
2015-12-24  2063.520020  2067.360107  2058.729980  2060.989990  1411860000  2060.989990

Разделим данные по дням недели. Сначала выясним, какой тип имеет индекс полученного временного ряда:

type(ts1.index[0])
pandas.tslib.Timestamp

Для работы с датами в Python имеется модуль datetime.date. Воспользуемся функцией isocaledar, которая для заданной даты возвращает три числа: год, номер недели в году (начиная с 1) и номер дня недели (начиная с понедельника, которому соответсвует число 1). Первой неделей считается та, что содержит четверг.

dt1 = ts1.index[0] # Дата и время для самой первой записи.
print(dt1)
print(dt1.weekday())     # Номер дня недели для первой записи (0 - понедельник,..., 6 - воскресенье).
print(dt1.isocalendar()) # Год, номер недели в году (начиная с 1) и номер дня недели (начиная с понедельника = 1).
2013-12-27 00:00:00
4
(2013, 52, 5)

Число записей (строк) в полученном наборе данных можно узнать с помощью стандартной функции len():

mm = len(ts1)
print(mm)
503

Далее нам потребуется список названий дней недели:

dowNames = [u"Пд", u"Вт", u"Ср", u"Чт", u"Пт"]

Для доступа к отдельных записям временного ряда будем использовать функцию ix():

ts1.ix[0]
Open         1.842970e+03
High         1.844890e+03
Low          1.839810e+03
Close        1.841400e+03
Volume       2.052920e+09
Adj Close    1.841400e+03
Name: 2013-12-27 00:00:00, dtype: float64
ts1.ix[0, 5] # или: ts1.ix[0, 'Adj Close']
1841.4000239999998

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

prev1 = ts1.ix[0,5] # Самый первый день, для которого нет предыдущего.
nweekprev1 = ts1.index[1].isocalendar()[1] # Номер недели в году.
nrow1 = -1 # Номер строки.
x = list() # Список строк-недель.
y = [0,0,0,0,0] # Одна неделя.
delta1 = 0.05
for index, row in ts1[1:].iterrows():
    open1, high1, low1, close1, volume1, adjclose1 = row
    chg1 = adjclose1 - prev1
    prev1 = adjclose1
    year1, nweek1, dow1 = index.isocalendar()
    dow1 -= 1
    if(chg1>delta1):
        y[dow1] = 1
    elif(chg1<-delta1):
        y[dow1] = -1
    if nweekprev1 != nweek1:
        x.append(y)
        y = [0,0,0,0,0]
    nweekprev1 = nweek1
x
[[-1, 1, 0, -1, -1],
 [-1, 1, -1, 1, 1],
 [0, 1, 1, -1, -1],
 ... ... ...
 [1, -1, -1, 1, -1],
 [1, 1, 1, -1, -1]]

Нули соответствуют выходным дням (праздникам) или тем дням, когда изменение цены закрытия не превысило установленный порог.

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

Преобразуем полученный динамический список списков x в матрицу, чтобы ускорить вычисления:

import numpy as np
x = np.matrix(x, dtype='int')
print(x)
[[-1  1  0 -1 -1]
 [-1  1 -1  1  1]
 [ 0  1  1 -1 -1]
 ... ... ...
 [ 1 -1 -1  1 -1]
 [ 1  1  1 -1 -1]]

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

Теперь попытаемся выяснить, сколько раз в понедельник цена закрытия упала по сравнению с ценой закрытия предыдущего дня:

# Сколько раз в понедельник цена закрытия упала по сравнению с ценой закрытия предыдущего дня?
num_monday = 0  # Счётчик.
m = len(x)
for row in x:
    if row.item(0) > delta1:
        num_monday += 1
print(u"Цена в понедельник выросла {0} раз(а) из {1}, т.е. в {2:.1f}% случаях.".format(num_monday, m, 100.0*num_monday/m))
Цена в понедельник выросла 49 раз(а) из 103, т.е. в 47.6% случаях.

Если бы вместо матрицы numpy мы использовали обычный список Python, то вместо row.item(0) мы бы написали row[0].

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

prev1 = ts1.ix[0,5] # Самый первый день, для которого нет предыдущего.
delta1 = 0.05
num_monday1 = 0 # Счётчик числа понедельников, когда цена росла.
num_monday2 = 0 # Счётчик числа понедельников, когда цена падала.
num_monday3 = 0 # Счётчик числа понедельников, когда изменение цены не превысило заданый порог.
for index, row in ts1[1:].iterrows():
    open1, high1, low1, close1, volume1, adjclose1 = row
    chg1 = adjclose1 - prev1
    prev1 = adjclose1
    year1, nweek1, dow1 = index.isocalendar()
    if dow1 == 1:
        if chg1 > delta1:
            num_monday1 += 1
        elif chg1 < -delta1:
            num_monday2 += 1
        elif abs(chg1) <= delta1:
            num_monday3 += 1
print( u"По понедельникам цена закрытия росла {0} раз, падала - {1} раз, изменение не превысило порог {2} раз."
      .format(num_monday1,num_monday2,num_monday3) )
По понедельникам цена закрытия росла 49 раз, падала - 47 раз, изменение не превысило порог 0 раз.

Теперь проведём то же самое исследование для всех дней недели: сколько раз цена росла, сколько раз падала и сколько раз изменение цены закрытия не превысило заданный порог.

# m = len(x)
delta1 = 0.05
for i in range(0, 5):
    num1 = 0
    num2 = 0
    num3 = 0
    for row in x:
        if row.item(i) > delta1:
            num1 += 1
        elif row.item(i) < -delta1:
            num2 += 1
        elif abs(row.item(i)) <= delta1:
            num3 += 1
    m = num1 + num2 + num3
    print(u"Цена в {0} выросла {1:3d} раз(а) из {2:3d}, т.е. в {3:4.1f}% случаях.".format(dowNames[i],num1, m, 100.0*num1/m))
    print(u"Цена в {0} упала   {1:3d} раз(а) из {2:3d}, т.е. в {3:4.1f}% случаях.".format(dowNames[i],num2, m, 100.0*num2/m))
    print(u"Цена в {0} не изм. {1:3d} раз(а) из {2:3d}, т.е. в {3:4.1f}% случаях.".format(dowNames[i],num3, m, 100.0*num3/m))
Цена в Пд выросла  49 раз(а) из 103, т.е. в 47.6% случаях.
Цена в Пд упала    46 раз(а) из 103, т.е. в 44.7% случаях.
Цена в Пд не изм.   8 раз(а) из 103, т.е. в  7.8% случаях.
Цена в Вт выросла  49 раз(а) из 103, т.е. в 47.6% случаях.
Цена в Вт упала    46 раз(а) из 103, т.е. в 44.7% случаях.
Цена в Вт не изм.   8 раз(а) из 103, т.е. в  7.8% случаях.
Цена в Ср выросла  50 раз(а) из 103, т.е. в 48.5% случаях.
Цена в Ср упала    50 раз(а) из 103, т.е. в 48.5% случаях.
Цена в Ср не изм.   3 раз(а) из 103, т.е. в  2.9% случаях.
Цена в Чт выросла  54 раз(а) из 103, т.е. в 52.4% случаях.
Цена в Чт упала    44 раз(а) из 103, т.е. в 42.7% случаях.
Цена в Чт не изм.   5 раз(а) из 103, т.е. в  4.9% случаях.
Цена в Пт выросла  52 раз(а) из 103, т.е. в 50.5% случаях.
Цена в Пт упала    47 раз(а) из 103, т.е. в 45.6% случаях.
Цена в Пт не изм.   4 раз(а) из 103, т.е. в  3.9% случаях.

Другим способом:

prev1 = ts1.ix[0,5] # Самый первый день, для которого нет предыдущего.
delta1 = 0.05
for i in range(0, 5):
    n1 = 0 # Счётчик числа случаев, когда цена росла.
    n2 = 0 # Счётчик числа случаев, когда цена падала.
    n3 = 0 # Счётчик числа случаев, когда изменение цены не превысило заданый порог.
    for index, row in ts1[1:].iterrows():
        open1, high1, low1, close1, volume1, adjclose1 = row
        chg1 = adjclose1 - prev1
        prev1 = adjclose1
        year1, nweek1, dow1 = index.isocalendar()
        dow1 -= 1
        if dow1 == i:   # Если это нужный день недели.
            if chg1 > delta1:
                n1 += 1
            elif chg1 < -delta1:
                n2 += 1
            elif abs(chg1) <= delta1:
                n3 += 1
    print( u"В {0} цена закрытия росла {1} раз, падала - {2} раз, изменение не превысило порог {3} раз."
          .format(dowNames[i],n1,n2,n3) )
В Пд цена закрытия росла 49 раз, падала - 47 раз, изменение не превысило порог 0 раз.
В Вт цена закрытия росла 54 раз, падала - 50 раз, изменение не превысило порог 0 раз.
В Ср цена закрытия росла 51 раз, падала - 50 раз, изменение не превысило порог 2 раз.
В Чт цена закрытия росла 54 раз, падала - 45 раз, изменение не превысило порог 1 раз.
В Пт цена закрытия росла 52 раз, падала - 47 раз, изменение не превысило порог 0 раз.

Небольшая разница в числах, полученных вторым способом, объясняется тем, что мы выбрали другой способ подсчёта дней. В первом способе мы учитываем все дни недели (кроме субботы и воскресенья), независимо от того, проводилась в эти дни торговля или нет, поэтому все праздники увеличивают число случаев, когда изменение цены не превысило заданный порог. Во втором способе мы считаем только те дни, в которые фьючерс торговался (праздничные дни в наборе данных, полученных с сервера yahoo Financе, отсутствуют).

# Как часто выполняется условие: рост цены в понедельник влечёт за собой рост цены в четверг?
rule_valid = 0    # Сколько раз это правило выполнилось.
rule_invalid = 0  # Сколько раз это правило нарушилось.
delta1 = 0.05
for row in x:
    if row.item(0) > delta1:        # В понедельник цена росла.
        if row.item(3) > delta1:    # В четверг цена росла.
            rule_valid += 1
        elif row.item(3) < -delta1: # В четверг цена падала.
            rule_invalid += 1
print(u"В {0} случаях правило выполнилось, а в {1} случаях нарушилось.".format(rule_valid, rule_invalid))
n = rule_valid + rule_invalid
support = n / float(m)   # Поддержка правила.
confidence = rule_valid / float(n)  # Достоверность правила.
print(u"Поддержка = {0:.1f}%; достоверность = {1:.1f}%.".format(support*100, confidence*100))
В 27 случаях правило выполнилось, а в 21 случаях нарушилось.
Поддержка = 46.6%; достоверность = 56.2%.

Является ли это правило статистически значимым или отличие от 50% объясняется случайным отклонением? На этот вопрос отвечает критерий знаков:

from scipy.stats import binom_test
pvalue = binom_test(rule_valid, n)
print(pvalue)    # Вывели на экран p-значение.
if(pvalue<0.05): # Если p-значение меньше выбранного уровня значимости.
    print(u"Правило является статистически значимым.")
else:
    print(u"Правило не является статистически значимым.")
0.470879013622
Правило не является статистически значимым.

Если бы из 49 случаев, когда цена в понедельник росла, правило выполнилось хотя бы 32 раза, то мы посчитали бы такое правило статистически значимым (при том же выбранном уровне значимости 0.05), поскольку полученное p-значение оказалось бы меньше 0.05:

print(binom_test(32, 49))
print(binom_test(31, 49))
0.0443841609871
0.0854331331574

Кроме критерия знаков, можно воспользоваться критерием \(\chi^2\) Пирсона. Он позволяет опровергнуть гипотезу о независимости двух случайных переменных. Для этого построим таблицу сопряжённости:

Событие Цена растёт Цена падает
Есть сигнал \(n_{11}\) \(n_{12}\)
Нет сигнала \(n_{21}\) \(n_{22}\)

Мы уже знаем, сколько раз цена в четверг росла или падала при условии, что рассматриваемый сигнал наблюдался (в понедельник цена росла). Но нам ещё надо выяснить, как часто в четверг цена росла или падала при отсутствии сигнала (т.е. в те недели, когда цена в понедельник падала). Сделаем это.

n11 = 0    # Сколько раз цена в четверг падала при наличии сигнала (при росте цены в понедельник).
n12 = 0    # Сколько раз цена в четверг росла при наличии сигнала.
n21 = 0    # Сколько раз цена в четверг падала при отсутствии сигнала (при падении цены в понедельник).
n22 = 0    # Сколько раз цена в четверг росла при отсутствии сигнала.
for row in x:
    if row.item(0) > delta1:       # В понедельник цена росла.
        if row.item(3) > delta1:   # В четверг цена росла.
            n11 += 1
        elif row.item(3) < delta1: # В четверг цена падала.
            n12 += 1
    elif row.item(0) < -delta1:    # В понедельник цена падала.
        if row.item(3) > delta1:   # В четверг цена росла.
            n21 += 1
        elif row.item(3) < delta1: # В четверг цена падала.
            n22 += 1
print(u"Таблица сопряжённости: \n{0:3d}, {1:3d};\n{2:3d}, {3:3d}.".format(n11,n12,n21,n22))
Таблица сопряжённости:
 27,  22;
 22,  24.

Тест Пирсона реализован в пакете scipy.stats в виде функции chi2_contingency():

import scipy.stats
scipy.stats.chi2_contingency ( [ [ 27 , 22 ] , [ 22 , 24 ] ] )
(0.25378598354970244,
 0.61442177643420948,
 1L,
 array([[ 25.27368421,  23.72631579],
        [ 23.72631579,  22.27368421]]))

Второе число в результате – это p-значение. Если оно меньше наперёд заданного уровня значимости (обычно выбирают 0.05), то гипотеза о независимости рассматриваемых переменных отклоняется, т.е. считается, что между переменными имеется статистическая связь (которая может быть обусловлена причинно-следственной связью или нет, это уже другой вопрос). В данном случае можно считать, что связь не выявлена.

Как видим, оба критерия (и тест знаков, и тест Пирсона) дали одинаковый результат. Поэтому нам придётся продолжить исследование, мы ещё надеемся найти какую-нибудь закономерность.

Запрограммируем функцию, которая принимает два числа (номера дней недели: 1 - понедельник и т.д.) и вычисляет поддержку, достоверность и статистическую значимость соответствующего правила. Рост цены в заданный день будем кодировать положительным числом, а падение – отрицательным. Например, два параметра -2 и 3 будут означать, что мы проверяем правило: “падение цены во вторник влечёт за собой рост цены в среду”. (Теперь понятно, почему мы были вынуждены считать понедельник единицей, а не нулём?)

Далее нам потребуется функция, которая вычисляет знак числа, т.е. возвращает 1 для положительного аргумента, -1 – для отрицательного:

# Функция для определения знака числа:
from numpy import sign
sign([-5, 0, 3])
array([-1,  0,  1])
# Как часто выполняется условие: рост/падение цены в день i влечёт за собой рост/падение цены в день j?
# Понедельник считаем 1,.., пятница - 5.
# Положительное число - рассматривается рост; отрицательное - падение.
pvalue0 = 0.07 # Зададим уровень значимости 7%.
def testHypothesis1(i, j):
    rule_valid = 0    # Сколько раз правило выполнилось.
    rule_invalid = 0  # Сколько раз правило нарушилось.
    i2 = abs(i)-1     # Положительныё номер дня недели, начиная с 0.
    j2 = abs(j)-1     # Положительныё номер дня недели, начиная с 0.
    for row in x:
        if row.item(i2)*sign(i)>0:
            if row.item(j2)*sign(j)>0:
                rule_valid += 1
            elif row.item(j2)*sign(j)<0:
                rule_invalid += 1
    if i>0:
        s1 = u"рост"
    else:
        s1 = u"падение"
    if j>0:
        s2 = u"рост"
    else:
        s2 = u"падение"
    print(u"Правило: {0} цены в {1} влечёт за собой {2} цены в {3}.".format(s1,dowNames[i2],s2,dowNames[j2]))
    print(u"В {0} случаях правило выполнилось, а в {1} случаях нарушилось.".format(rule_valid, rule_invalid))
    n = rule_valid + rule_invalid
    support = n / float(m)              # Поддержка правила.
    confidence = rule_valid / float(n)  # Достоверность правила.
    print(u"Поддержка = {0:.1f}%; достоверность = {1:.1f}%.".format(support*100, confidence*100))
    pvalue = binom_test(rule_valid, n)
    print(u"p-значение = {0:.2f}.".format(pvalue))
    if pvalue<pvalue0:
        print(u"Найдено значимое правило!")
        print(80*"-")
    else:
        print(u"Правило не является значимым.\n")
    return pvalue

Проверим, как работает функция:

testHypothesis1(-2, 3)
Правило: падение цены в Вт влечёт за собой рост цены в Ср.
В 26 случаях правило выполнилось, а в 18 случаях нарушилось.
Поддержка = 42.7%; достоверность = 59.1%.
p-значение = 0.29.
Правило не является значимым.

Осталось вызвать нашу функцию для проверки всех возможных правил:

for i, j in ((1,2), (1,-2), (-1,2), (-1,-2),
             (1,3), (1,-3), (-1,3), (-1,-3),
             (1,4), (1,-4), (-1,4), (-1,-4),
             (1,5), (1,-5), (-1,5), (-1,-5),
             (2,3), (2,-3), (-2,3), (-2,-3),
             (2,4), (2,-4), (-2,4), (-2,-4),
             (2,5), (2,-5), (-2,5), (-2,-5),
             (3,4), (3,-4), (-3,4), (-3,-4),
             (3,5), (3,-5), (-3,5), (-3,-5),
             (4,5), (4,-5), (-4,5), (-4,-5)):
    pvalue = testHypothesis1(i, j)
Правило: рост цены в Пд влечёт за собой рост цены в Вт.
В 23 случаях правило выполнилось, а в 22 случаях нарушилось.
Поддержка = 43.7%; достоверность = 51.1%.
p-значение = 1.00.
Правило не является значимым.

Правило: рост цены в Пд влечёт за собой падение цены в Вт.
В 22 случаях правило выполнилось, а в 23 случаях нарушилось.
Поддержка = 43.7%; достоверность = 48.9%.
p-значение = 1.00.
Правило не является значимым.

Правило: падение цены в Пд влечёт за собой рост цены в Вт.
В 20 случаях правило выполнилось, а в 22 случаях нарушилось.
Поддержка = 40.8%; достоверность = 47.6%.
p-значение = 0.88.
Правило не является значимым.

... ... ...

Правило: падение цены в Ср влечёт за собой рост цены в Чт.
В 30 случаях правило выполнилось, а в 16 случаях нарушилось.
Поддержка = 44.7%; достоверность = 65.2%.
p-значение = 0.05.
Найдено значимое правило!
--------------------------------------------------------------------------------
Правило: падение цены в Ср влечёт за собой падение цены в Чт.
В 16 случаях правило выполнилось, а в 30 случаях нарушилось.
Поддержка = 44.7%; достоверность = 34.8%.
p-значение = 0.05.
Найдено значимое правило!
--------------------------------------------------------------------------------
Правило: рост цены в Ср влечёт за собой рост цены в Пт.
В 20 случаях правило выполнилось, а в 27 случаях нарушилось.
Поддержка = 45.6%; достоверность = 42.6%.
p-значение = 0.38.
Правило не является значимым.

... ... ...

Правило: падение цены в Чт влечёт за собой падение цены в Пт.
В 23 случаях правило выполнилось, а в 20 случаях нарушилось.
Поддержка = 41.7%; достоверность = 53.5%.
p-значение = 0.76.
Правило не является значимым.

Когда достоверность правила менее 50%, тест знаков на самом деле возвращает значимость обратного правила при том же сигнале.

Итак, мы нашли одно правило: “падение цены в среду влечёт за собой рост цены в четверг”, значимость которого на границе заданного порога. Если бы мы выбрали уровень значимости 0.05, то правило оказалось бы не значимым.

Можно проверить и более сложные условия. Довольно легко найти правила, значимость которых окажется ниже 0.05 или даже ниже 0.03. Разумеется, использовать эти правила в реальной торговле вы можете только на свой страх и риск.

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


Теги: Python




Комментарии

Комментариев пока нет.

* Обязательные поля
(Не публикуется)
 
Жирный Курсив Подчеркнутый Перечеркнутый Степень Индекс Код PHP Код Кавычки Вставить линию Вставить маркированный список Вставить нумерованный список Вставить ссылку Вставить e-mail Вставить изображение Вставить видео
 
Улыбка Печаль Удивление Смех Злость Язык Возмущение Ухмылка Подмигнуть Испуг Круто Скука Смущение Несерьёзно Шокирован
 
1000
Captcha
Refresh
 
Введите код:
 
Запомнить информацию введенную в поля формы.