Условни разпределения, персонализирани графики на свещници и трикове за визуализация

В този интензивен курс ще научите как да:

  • Тествайте насочващите сигнали за статистическа значимост
  • Направете графики на свещници от нулата и визуализирайте сигнали със засенчване
  • Импортирайте данни от безплатни източници и ги индексирайте правилно

Уверете се, че сте преминали през „първата част“ на този курс, за да сте в час. Можете да вземете тетрадката за този интензивен курс тук.

За още по-напреднал анализ, не забравяйте да разгледате част 3!

Нека се потопим.

Тестване на сигнала — условни разпределения

Един разумен начин за тестване на търговски сигнал е да се разгледа разпределението на бъдещата възвръщаемост (или някаква друга бъдеща променлива от интерес) за цялата популация и да се сравни това с разпределението на бъдещата възвръщаемост, когато сигналът за търговия е включен.

Ако даден сигнал има стойност, трябва да видим забележимо различно разпределение на бъдещата възвръщаемост, когато е активен в сравнение с общата популация на бъдеща възвръщаемост.

Ако искаме да оптимизираме параметрите на нашия сигнал, трябва да го направим само по време на частта за „обучение“ от нашия набор от данни (за времеви серии, обикновено най-ранните 70% от периода) и след това да видим ефективността на сигнала в оставащата проба . Това обаче не е толкова важно, ако не сме оптимизирали параметрите на сигнала.

За да проверим дали разпределенията се различават значително, можем да изчислим статистиката на Колмогоров–Смирнов (KS) с помощта на scipy.stats, която определя количествено разликата между две разпределения. Освен това можем да разгледаме стойностите на 20-ия, 50-ия и 80-ия процентил на нашата целева променлива (обикновено бъдеща възвръщаемост) за общата популация, както и за филтрираната популация. Един ценен сигнал би трябвало да причини забележима промяна в тези процентилни стойности.

Като цяло искаме парцел като този по-долу. Той показва разпределението на популацията на бъдещите възвръщаемости (в синьо), разпределението на бъдещите възвръщаемости, когато сигналът е активен (в оранжево), броя на наблюденията в оранжевото разпределение и KS статистиката. Неговите вертикални линии показват процентилите на населението спрямо периодите на „активен сигнал“; можете да изброите интересните процентили. И накрая, това ни позволява да разделим нашия времеви диапазон на множество части с еднакъв размер (в примера по-долу, две части). По този начин можем да видим как силата на сигнала се е променила с времето.

Ще приемем, че нашата рамка с данни е форматирана правилно; има индекс за дата и час, сортиран във възходящ ред и съдържа колоните „Отворено“, „Високо“, „Ниско“ и „Затваряне“. Нека започнем, като дефинираме параметрите на нашата функция.

Преди да продължим, трябва да импортираме някои полезни инструменти и пакети.

from matplotlib.ticker import FormatStrFormatter
import matplotlib 
from itertools import permutations
import scipy.stats as stats
from scipy.stats import skew, kurtosis
import matplotlib.pyplot as plt
from datetime import timedelta
from datetime import datetime
from datetime import date
import scipy.stats as stats
from cycler import cycler
import matplotlib as mpl
import pandas as pd
import numpy as np
import seaborn as sns
import statistics
import random
import math
import re 
sns.set()

Пример от реалния свят: Тестването затваря под линия на тенденция

Нека създадем случаен времеви ред и да проведем някои тестове върху него. Ще използваме същия метод, който използвахме в предишния интензивен курс.

##### Generating simulated stock price
our_index = pd.date_range(start='2021-01-01', periods=25000*24*2, freq='30T') # create an index, 30min time increments
df1 = pd.DataFrame(index = our_index)
R_old = 0.0002
stdev_old = 0.008
R_new = (1+R_old)**(1/48)-1
stdev_new = (stdev_old)*(1**0.5)*((1/48)**0.5)
### Get list of decimal values. 
# These are the log changes of each increment
df1['Close'] = np.random.normal(loc=R_new, scale=stdev_new, size=25000*24*2)
df1['Close'] = np.exp(df1['Close'])
df1['Close'].iloc[0] = 1
df1['Close'] = df1['Close'].cumprod()
### Summarize intraday simulated moves as daily increments, OHLC.
# Only keep rows that take place betw 930am and 4pm
df1 = df1.between_time('09:30', '16:00') # Downsample - summarize those rows as daily data
df1 = df1['Close'].resample('1D').ohlc()
df1.rename(columns={'open':'Open','high':'High','low':'Low','close':'Close'}, inplace=True)
df1.head(5) # show first 5 rows

Нека направим бърз график на данните, за да видим дали изглежда естествено. Ще създадем един голям участък на цената на затваряне и участък под него, който е само 30% висок. Долната графика ще покаже разстоянието между цената на затваряне и нейната средна стойност от 30 стъпки. Използването на „gridspec“ прави тези задачи за чертане доста лесни.

## Making columns to plot
i = 30 # lookback for moving average
name_ma = f'{i}ma'
name_madist = f'{i}madist'
df1[name_ma] = df1['Close'].rolling(i).mean()
df1[name_madist] = (df1['Close']/df1[name_ma]) - 1
df1.head(5) # show first 5 rows
#### plotting
from matplotlib import gridspec
fig = plt.figure(figsize=(9,6))
gs = gridspec.GridSpec(2, 1, height_ratios=[1, 0.3]) 
# 2 rows, one column. Second plot is 30% as tall as first.
ax1 = plt.subplot(gs[0])
ax2 = plt.subplot(gs[1])
dfplot = df1.head(500)
dfplot[['Close',name_ma]].plot(ax=ax1, logy=True, alpha=0.8)
## plot dist from ma as several circular dots connected by a line
dfplot[[name_madist]].plot(ax=ax2, style=['.-'], markevery=30, logy=False, alpha=0.7, color='tab:purple', sharex=True) 
## plot dist from ma as an area under a curve.
dfplot[[name_madist]].plot.area(ax=ax2, stacked=False, logy=False, alpha=0.5, color='tab:purple', sharex=True)
ax1.legend(loc='upper left')
ax2.legend(loc='upper left')
import matplotlib.ticker
ax2.yaxis.set_major_locator(matplotlib.ticker.MultipleLocator(0.05))
plt.tight_layout()

Ето как изглежда това.

Сега да се върнем към тестването на нашия сигнал. Да предположим, че сигналът за интерес възниква, когато колоната Затваряне падне под средната стойност за последните 50 стъпки. Първо трябва да създадем колона, която е „1“, когато се появи нашият сигнал, и 0 в противен случай.

Първата стъпка е да създадем нашата колона с тренд линия. Името му ще бъде низ, който съхраняваме в променлива, наречена name_trendline. В зависимост от нашия параметър за ретроспекция (в този случай 50), името ни ще се промени. Използваме f-низ, така че името да се промени, ако променим променливата „i“.

След това поставяме условие, което търси нашия сигнал за интерес и избягва припокриващи се показания.

### Testing a trend line signal - making key columns
i = 50 # lookback param, for finding avg over past 50 increments
name_trendline = f'{i}EMA'
df1[name_trendline] = df1['Close'].ewm(span=i, min_periods=i).mean()
### Can also create simple moving average (shown below)
# name_trendline = f'{i}SMA'
# df1[name_trendline] = df1['Close'].rolling(i).mean()
futparam = 20 # looking at returns or vol over next 20 increments. 
# Observations must be 20 days apart (avoid overlap)
### Signal is active if recent increment's close < trendline today 
### *and* prior increment's close was >= trendline
condition_on = (df1['Close'] < df1[name_trendline]) & (df1['Close'].shift(1) >= df1[name_trendline].shift(1))
### Need to avoid overlapping observations of future variable
# Note, doing condition*1 creates a series (column)
# In that series, it is 1 when condition is True, 0 otherwise
condition_nooverlap = ((condition_on)*1).rolling(futparam).sum()==1
condition_signal = (condition_on & condition_nooverlap)
df1['Signal'] = condition_signal*1 # column is 1 when condition_signal is true, 0 otherwise 

За сигнали, които не се появяват на предвидими интервали, трябва да отчетем факта, че ще има забавяне между момента, в който видим сигнала, и момента, в който можем да действаме по него. Ако в края на увеличение 3, затварянето пресича под линията на тренда, не можем да извършваме транзакции до началото (отваряне) на увеличение 4. За да отчетем това забавяне, можем да изместим нашата сигнална колона с 1. Имайте предвид, че за сезонни сделки (купуване когато започва определен месец от часа), не е нужно да се тревожим за забавяне.

df1['Signal'] = df1['Signal'].shift(1)

Бърз съвет: за данни в рамките на деня трябва да се опитаме да разгледаме увеличения, които са дълги поне 1 час, и трябва да използваме TWAP вместо „Затвори“, защото е трудно да се оцени точността на показанията на OHLC за по-малки увеличения. Това помага да се намалят шансовете да бъдете подведени от шума от измерването. По време на бектестовете ще включим ефекта на приплъзване (транзакции на неоптимални цени), но за първоначалното проучване ще тестваме само необработения сигнал.

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

### Creating dependent variable of interest
j = 20 # looking 20 increments ahead
name_yvar = f'{j}futret'
df1[name_yvar] = df1['Close'].pct_change(j).shift(-j)

Ако искаме да се съсредоточим само върху първите 65% от нашата извадка, можем да изберем тази част с кода по-долу.

### looking only at first 65% of data 
### (may not be necessary if we did not try to optimize parameters)
train_portion = 0.65 # look at first 65% of sample
# Find row number where training portion ends
train_portion_end = int((len(df1)*100*train_portion)//100)
df0 = df1.copy().iloc[:train_portion_end, :] 
# keep rows from row number 0 to train_portion_end
print(df1.tail())
print(df0.tail())

След това трябва да уточним дали искаме да разделим нашата обща извадка на подпериоди, известни още като „парчета“, което ни позволява да тестваме колко стабилна е производителността на сигнала през целия период от време. Когато начертаваме множество части, ще създадем мрежа от графики и трябва да посочим колко колони искаме да има тази мрежа.

### Creating variable to plot
yvar = name_yvar
df = df0.copy()
df[f'{yvar}_pop'] = df[yvar]
df[f'{yvar}_filt'] = df[(condition_signal)][yvar]
# if condition not true, yvar is nan.
### creating grid 
nchunks = 2
fig_cols = 3 # dimension of grid of subplots (number of cols)
# number of rows needed to stay neat
fig_rows = 1 + (nchunks - (nchunks % fig_cols))*(1/fig_cols)
fig_height = fig_rows * 5.1 
# each grid row has a height of 5.1 so figures are modestly sized
fig_width = fig_cols * 7.1
# Create a figure that contains our grid of plots
fig = plt.figure(figsize=(fig_width,fig_height))
nrows = math.ceil(len(df)/ (nchunks))
list_df = []
# create a list, each element is a subperiod of the main df
list_df = [df.copy()[i:i+nrows] for i in range(0,df.shape[0],nrows)]

Ще искаме да направим вертикални линии, които показват процентилите на съвкупността спрямо периодите на „активен сигнал“ и да можем да изброим процентилите, представляващи интерес. Нека дефинираме този списък.

list_percentiles = [0.35, 0.50, 0.65]

Сега можем да преминем през списъка с кадри с данни, които ще използваме, за да тестваме и начертаем ефективността на сигнала. Всяка рамка от данни представлява интересен подпериод (като първата трета от общия период от време). Един от начините за преминаване през списък е използването на „изброяване“. Позволява ни да следим на кой номер на артикул се намираме в момента.

За всеки подпериод, който начертаваме, искаме да следим кога започва и спира. Можем да съхраняваме информацията за датата като низове и след това да ги използваме в нашия график по-късно. Ние също така създаваме подплот в конкретна позиция в мрежата; add_subplot се справя с това вместо нас. Ако имаме 3 колони и сме на 5-ия елемент от списъка, той автоматично ще го добави на втория ред във втората колона.

for index, element in enumerate(list_df, start = 1): 
# default setting is for first element to have index of 0.
  dfz = element.copy() # element is a df in the list
  startstring = str(dfz.index[0].year)+'-'+str(dfz.index[0].month)+'-'+str(dfz.index[0].day)
  endstring = str(dfz.index[-1].year)+'-'+str(dfz.index[-1].month)+'-'+str(dfz.index[-1].day)
  ax = fig.add_subplot(fig_rows, fig_cols, index)

В рамките на същия цикъл искаме да получим брой наблюдения на y променливи в популацията, за разлика от това, когато сигналът е активен.

num_popobservs = dfz[f'{yvar}_pop'].count()
num_filtobservs = dfz[f'{yvar}_filt'].count()
nbins = 20

За да оценим разликата между разпределението на населението и филтрираното разпределение, можем да изчислим KS статистиката и свързаната с нея p-стойност. Също така е полезно да се изчислят характеристиките от по-висок ред, известни още като „моменти“, на разпределенията. Те включват средна стойност, дисперсия, изкривяване и ексцес. Все още работим в същия цикъл, както преди.

# import scipy.stats as stats # in case you haven't imported yet
KSi, pval_KS = stats.ks_2samp(dfz[f'{yvar}_pop'],dfz[f'{yvar}_filt'])
# Anderson-Darling test to see if dists are different
ADi, _, pval_AD = stats.anderson_ksamp((dfz[f'{yvar}_pop'],dfz[f'{yvar}_filt']))
pop_mean = dfz[f'{yvar}_pop'].mean()
pop_var = dfz[f'{yvar}_pop'].var()
pop_skew = skew(dfz[f'{yvar}_pop'], nan_policy='omit')
pop_kurtosis = kurtosis(dfz[f'{yvar}_pop'], fisher=False, nan_policy='omit')
filt_mean = dfz[f'{yvar}_filt'].mean()
filt_var = dfz[f'{yvar}_filt'].var()
filt_skew = skew(dfz[f'{yvar}_filt'], nan_policy='omit')
filt_kurtosis = kurtosis(dfz[f'{yvar}_filt'], fisher=False, nan_policy='omit')
title_moments = f'''FiltVsPop- mean:{filt_mean:.2f},{pop_mean:.2f}, var:{filt_var:.1e},{pop_var:.1e}, 
FiltVsPop- skew:{filt_skew:.2f},{pop_skew:.2f}, kurtosis:{filt_kurtosis:.2f},{pop_kurtosis:.2f}'''

В рамките на същия цикъл ще създадем под цикъл, за да намерим всеки интересен процентил и ще изградим низ за съхраняване на резултатите. Ще начертаем вертикална линия в синьо, за да представим процентила на общата съвкупност, и в червено, за да представим персентила на филтрираната съвкупност.

titleQ_text = 'Qchng(Filt-Pop): '
for num, Q in enumerate(list_percentiles, start = 0):
  ypop_Q = dfz[f'{yvar}_pop'].quantile(Q)
  yfilt_Q = dfz[f'{yvar}_filt'].quantile(Q)
  changeQ = yfilt_Q - ypop_Q
  titleQ_text = titleQ_text + f' {Q*100:.0f}%ile:{changeQ*100:.2f}%,'
  ax.axvline(ypop_Q, linewidth=0.6, alpha=0.5, color='blue', linestyle='--') 
  ax.axvline(yfilt_Q, linewidth=0.9, alpha=0.7, color='orangered', linestyle='-')

Това е всичко за втория цикъл. Сега продължаваме с първия цикъл, който започнахме. Последната стъпка е да начертаете и добавите многоредов надпис, който съдържа ключовата информация. За малко по-чиста визуализация няма да изобразяваме наблюдения, които са в долния или горния 1% от разпределенията. За числа, които може да са много големи, за да спестим място, ще ги покажем в научна нотация, като използваме „:.1e“ вътре в f-низа (това означава, че искаме научна нотация и показан 1 знак след десетичната запетая).

cut_upper = dfz[f'{yvar}_pop'].quantile(0.99)
cut_lower = dfz[f'{yvar}_pop'].quantile(0.01)
dfz = dfz[(dfz[f'{yvar}_pop'] < cut_upper) & (dfz[f'{yvar}_pop'] > cut_lower)]
dfz[[f'{yvar}_pop',f'{yvar}_filt']].plot.hist(alpha=0.6, density=1, bins=nbins, ax=ax, title=titleQ_text)
ax.set_xlabel(f'''Dates:{str(startstring)} to {str(endstring)}, #observs(Pop,Filt): {num_popobservs},{num_filtobservs:},
pval_KS:{pval_KS:.0e}, KS:{KSi:.1e}, 
pval_AD:{pval_AD:.0e}, AD:{ADi:.1e},
{title_moments}''')
plt.tight_layout()

И това е. Сега нека опитаме. Трябва да получим нещо, което изглежда така.

Левият график ни показва първата половина от периода, който изследвахме. Както можете да видите от заглавието на този график, филтрираното разпределение има медиана (50-ти персентил), която е с 0,0034 по-висока от разпределението на населението. Статистиката на KS е доста висока, около 0,98 (близо до максимума, 1), а нейната p-стойност е по същество нула. Това означава, че има голяма вероятност нашето филтрирано разпределение да е значително по-различно от нашето разпределение на населението. Филтрираното разпределение е отрицателно изкривено (наклонено надясно), така че неговата медиана е по-висока от съвкупността и изкривяването му е -0,08, докато изкривяването на популацията е +0,2.

Създаване на диаграми със свещници и визуализиране на сигнали със засенчване

Инструментите на трети страни, които ви помагат да рисувате свещници, могат да бъдат полезни, но намирам, че не ви дават пълен контрол върху външния вид и настройките. За да мога свободно да персонализирам графиката, реших да опитам да изградя свещници от нулата. Оказва се, че това е доста просто.

Първо, приемаме, че имаме рамка с данни с индекс за дата и час и с колони, включващи Open, High, Low и Close. Трябва да отбележим колко минути са между всяко наблюдение (за ден, да речем 24*60) и трябва да посочим коя ос да попълним със свещниците. Също така трябва да посочим какви ще бъдат цветовете на нашите свещи „нагоре“ и „надолу“.

Нека дефинираме функция, която съдържа тези параметри. Също така ще посочим стойности по подразбиране за параметрите color_up и color_down. Ние наричаме нашата рамка с данни „данни“.

def draw_candlesticks(axis, data, MinutesPerCandle, color_up='cornflowerblue', color_down='gold'):

Вътре в тази функция изчертаваме всеки един ред от данни като свещник, който съдържа правоъгълник, както и вертикална линия. Следователно трябва да повторим всяко наблюдение. За да направим това, ще използваме „изброяване“, за да преминем през всяка от стойностите на индекса на рамката с данни. За достъп до стойността „Затвори“ за всяко наблюдение, бихме казали data.loc[index][‘Close’]. Първо, трябва да проверим дали това наблюдение е увеличение „нагоре“ или „надолу“ и съответно да зададем цвета на свещника.

from matplotlib.patches import Rectangle 
import matplotlib.dates as mdates
for num, index in enumerate(data.index):
        if data.loc[index]['Close'] > data.loc[index]['Open']:
            color = color_up
        
        if data.loc[index]['Close'] <= data.loc[index]['Open']:
            color = color_down

Искаме всеки свещник да бъде центриран над началния си час. Да предположим, че работим със стъпки от 30 минути. Редът, чийто индекс е 2021–03–01 12:00, се отнася за 1 март 2021 г. от 12:00 до 12:30. 12:00 е началният час на този ред и нашият свещник ще бъде центриран над тази стойност.

За да направим това, имаме правоъгълника да започне преди 12:00 и да продължи малко след 12:00. Един лесен начин да направите това е да разделите увеличението на времето (30 минути) на 3; изваждаме това количество минути от нашето начално време (12:00), за да получим началото на правоъгълника. Вътре в цикъла можем да кажем следното:

start = index - pd.DateOffset(hours=0, minutes=1*MinutesPerCandle//3)
end = index + pd.DateOffset(hours=0, minutes=1*MinutesPerCandle//3)
width = end - start
height = data.loc[index]['Close'] - data.loc[index]['Open']

За да начертаем правоъгълника, посочваме долния му ляв ъгъл, след което посочваме ширината и височината му. За да начертаем вертикалната линия, използваме „vline“ и посочваме индекса (datetimeindex), минималната стойност на y и максималната стойност на y. За да сме сигурни, че линията се показва *зад* правоъгълника, ние й даваме по-ниска стойност на „zorder“. Добавяме също два допълнителни реда, за да сме сигурни, че нашият datetimeindex се запазва, и коригираме въртенето на xticks. Така че вътре в цикъла добавяме следното:

rect = Rectangle((start, data.loc[index]['Open']), width=width, height=height, color=color, zorder=3)
axis.add_patch(rect)
axis.vlines(index, ymin=data.loc[index]['Low'], ymax=data.loc[index]['High'], linewidth=0.5, color='black', zorder=2)
axis.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d %H:%M"))
plt.xticks(rotation=75)

И това е! Сега нека опитаме да начертаем решетка от графики със свещници.

### creating grid 
df3 = df1.copy().head(200)
nchunks = 6
fig_cols = 3 # dimension of grid of subplots (number of cols)
# number of rows needed to stay neat
fig_rows = 1 + (nchunks - (nchunks % fig_cols))*(1/fig_cols)
fig_height = fig_rows * 5.1 
# each grid row has a height of 5.1 so figures are modestly sized
fig_width = fig_cols * 7.1
# Create a figure that contains our grid of plots
fig = plt.figure(figsize=(fig_width,fig_height))
# create a list, each element is a subperiod of the main df
nrows = math.ceil(len(df3)/ (nchunks))
list_df = []
list_df = [df3.copy()[i:i+nrows] for i in range(0,df3.shape[0],nrows)]
for index, element in enumerate(list_df, start = 1): 
# default setting is for first element to have index of 0.
  dfz = element.copy() # element is a df in the list
  ax = fig.add_subplot(fig_rows, fig_cols, index)
  
  MinutesPerCandle = 24*60
  draw_candlesticks(axis=ax, data=dfz[['Open','Close','High','Low']], MinutesPerCandle=MinutesPerCandle, color_up='cornflowerblue', color_down='gold')
  plt.tight_layout()

Резултатът трябва да изглежда така:

Можем също така да засенчваме определени области на всеки подплот, които отговарят на дадено условие. Ето как засенчваме в зелено региони, където Close е над средното за 5 дни. В рамките на нашия цикъл добавяме:

condition_fill = (dfz['Close']>dfz['Close'].rolling(5).mean())
ax.fill_between(dfz.index, dfz['Low'].min(), dfz['High'].max(), where=condition_fill, alpha=0.2, color='green')

Резултатът трябва да изглежда така:

Импортиране и форматиране на данни от безплатни източници

Щастливи сме, че можем да изтеглим безплатно много данни от исторически финансови времеви редове. Инвестирането в dot com е особено добър ресурс за повечето публично търгувани акции в САЩ, както и за нетъргуеми пазарни индекси (като VIX).

Когато изтегляме исторически данни за акции на САЩ от investing dot com, форматирането обикновено е доста последователно. След като изтеглим този csv файл, можем да го прочетем в рамка с данни, като използваме кода по-долу. Той форматира колоната „Дата“ и я използва, за да зададе индекс на дата и час. Всяко ежедневно наблюдение има индекс за дата и час и се отбелязва в 16:00 или 16:00 часа, когато пазарът в САЩ затваря.

def data_from_investing(url):
  name = pd.read_csv(url)
  if 'Vol.' in name.columns:
    name.rename(columns={"Price": "Close", 'Vol.':'Volume'}, inplace=True) 
    name = name[['Date','Open','High','Low','Close','Volume']]
  else: 
    name.rename(columns={"Price": "Close"}, inplace=True) 
    name = name[['Date','Open','High','Low','Close']] 
  name['Date'] = pd.to_datetime(name['Date'])
  name.sort_values(by=['Date'], inplace=True, ascending=True)
  name['Close']= name['Close'].astype(str).str.replace(',', '').astype(float)
  name['Open']= name['Open'].astype(str).str.replace(',', '').astype(float)
  name['High']= name['High'].astype(str).str.replace(',', '').astype(float)
  name['Low']= name['Low'].astype(str).str.replace(',', '').astype(float)
  if 'Volume' in name.columns:
    name['Volume'] = name['Volume'].astype(str).str.replace('-', '0')
    name['Volume'] = name['Volume'].replace({'K': '*1e3', 'M': '*1e6','B': '*1e9' }, regex=True).map(pd.eval).astype(float)
  name['ChngOC'] = -1 + (name['Close']/name['Open'])
  name['ChngON'] = (name['Open']/name['Close'].shift(1)).add(-1)
  name['ChngPrevC'] = name['Close'].pct_change()
  name.set_index('Date', inplace=True)
  name = name.asfreq('D').dropna() 
  name.index = name.index.tz_localize('US/Eastern', ambiguous=True) # get time zone information
  name.index = name.index + pd.DateOffset(hours=16, minutes=00) # specify time is 4:00 pm New York time, market close. 
  name.fillna(method='ffill', inplace=True) # fill NaNs due to index expansion with the most recent older row
  name.dropna(inplace=True)
  return name

За да използвате тази функция, просто изтеглете историческите данни като csv от investing dot com. Поставете този csv файл в същата папка, която съдържа вашия бележник на python. Използвайте името на файла „XYZ.csv“ като „url“ във функцията по-горе. Например, можем да получим исторически данни за цената на акциите на Apple тук. Името на файла, който изтегляме, е „Apple Historical Data.csv“. Това ще бъде нашият „url“.

След като изтеглим тези данни, можем да създадем подобна рамка с данни.

url = 'Apple Historical Data.csv'
df = data_from_investing(url)
df.head()

Когато поискате главата на вашата рамка с данни, тя трябва да изглежда по следния начин (този пример разглежда XLK, ETF на технологичния сектор):

Бектест, анализ на производителността и тестване на чувствителността на последователността

След като имаме сигнал, който смятаме, че има стойност, можем да се опитаме да го търгуваме. За да направим това, трябва да изградим функция, която може да тества нашата стратегия, да включва пропускане и да показва профила на печалба/риск (MFE/MAE) на всяка сделка. Трябва да създаде сюжет като този по-долу. За да научите как да направите това, можете да разгледате част трета от интензивния курс тук.

Можем също така да разгледаме как си взаимодействат две интересуващи ни x-променливи: дали високите стойности на xvar1, съчетани с ниските стойности на xvar2, водят до по-висока бъдеща възвръщаемост? В графиката по-долу височината на върховете показва бъдеща възвръщаемост.

За да получим повече яснота относно поведението на дадена стратегия, можем да анализираме нейното представяне, като разгледаме разпределението на нейната последваща коригирана спрямо риска възвръщаемост, получена от бенчмарка и активните портфейли.

Можем също така да начертаем подвижния спред между коригираното спрямо риска представяне на активното портфолио и бенчмарка във времето.

Ако нашата стратегия изглежда добре, трябва да се опитаме да измерим колко чувствителна е към реда на своите сделки. Няколко добри сделки в началото могат да направят дългосрочната крива на ефективността на стратегията да изглежда впечатляваща. Трябва да се опитаме да разместим реда на сделките стотици или хиляди пъти и да начертаем получените криви на капитала, за да видим каква е разликата в реда на сделките.

Ако ги разровите, можете да разгледате пълния преглед на кода в трета част тук.