Анализ акций

  • Анализ акций со стратегией Momentum
  • Анализ акций со стратегией пробоя
  • Кодирование пробоя акций на Python
  • Фундаментальный анализ акций с использованием Python
  • Фундаментальный анализ акций с использованием API Python
  • Панель мониторинга анализа акций на Python

Анализ акций со стратегией Momentum

В этой статье мы поговорим об искусстве прогнозирования цен на акции с помощью стратегии, основанной на импульсе. Мотивацией для этой статьи является путешествие в прошлое, когда я возвращаюсь к некоторым проектам из моего курса «ИИ для трейдинга» на Udacity.

Знакомство

Анализ фондовых рынков всегда был сложной и интригующей областью. Благодаря интеграции методов искусственного интеллекта и машинного обучения в этой области за последнее десятилетие произошли значительные успехи. Один из популярных методов основан на стратегии импульса, где основная предпосылка — «покупай дорого, продавай дороже». Сегодня мы рассмотрим эту стратегию, интегрировав сбор данных, моделирование, расчет метрик и визуализацию.

Получение биржевых данных

Прежде чем углубиться в эту тему, давайте поговорим о нашем источнике данных. Мы используем библиотеку yfinance для извлечения ежедневных биржевых данных из Yahoo Finance. Таким образом, мы можем извлечь цены закрытия наших целевых акций за последние пять лет. В данном случае в наш список наблюдения входят такие гиганты индустрии, как Apple (AAPL), Amazon (AMZN Microsoft (MSFT и семь других.def fetch_stock_data(ticker_list, years=5):
end_date = datetime.now()
start_date = end_date — timedelta(days=years * 365)
stock_data = pd.DataFrame()

for ticker in ticker_list:
stock = yf.Ticker(ticker)
hist_data = stock.history(period=’1d’, start=start_date, end=end_date)
close_data = hist_data[‘Close’].rename(ticker)
stock_data = pd.merge(stock_data, pd.DataFrame(close_data), left_index=True, right_index=True, how=’outer’)
return stock_data

# Fetch the data
ticker_list = [‘AAPL’, ‘AMZN’, ‘MSFT’, ‘GOOGL’, ‘META’, ‘TSLA’, ‘NVDA’, ‘ADBE’, ‘NFLX’, ‘INTC’]
years = 5
daily_data = fetch_stock_data(ticker_list, years)

Представление фрейма данных: daily_data

Симуляция стратегии Momentum

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

  1. Передискретизируйте данные о запасах в соответствии с желаемой периодичностью: ежедневно, еженедельно или ежемесячно.
  2. Определите наиболее эффективные акции на основе прошлых отчетов.
  3. Распределите средства по этим акциям и смоделируйте динамику портфеля с течением времени, учитывая потенциальные налоги на прибыль.

# Resample data to different frequencies: daily, weekly, monthly
def resample_data(data, period):
if period == ‘D’:
return data
elif period == ‘W’:
return data.resample(‘W’).last()
elif period == ‘M’:
return data.resample(‘M’).last()# Simulate a simple momentum strategy based on log returns
def simulate_momentum_strategy(data, initial_amount, top_n, tax_rate, period=’M’):
data = resample_data(data, period)
log_returns = np.log(data / data.shift(1))
simulation_details = pd.DataFrame(index=log_returns.index,
columns=[‘Selected Stocks’, ‘Profit Before Tax’, ‘Tax Paid’, ‘Portfolio Value’])
cash = initial_amount

# Logic to select top stocks and calculate portfolio value
for i in range(0, len(log_returns) — 1):
# Identify the top_n performing stocks based on past log returns
top_stocks = log_returns.iloc[i].sort_values(ascending=False).head(top_n)
# Filter out stocks with negative returns
top_stocks = top_stocks[top_stocks > 0]

if not top_stocks.empty:
simulation_details.loc[log_returns.index[i + 1], ‘Selected Stocks’] = json.dumps(top_stocks.index.tolist())
# Calculate the amount to allocate for each stock
num_stocks = len(top_stocks)
allocation_per_stock = cash / num_stocks
# Calculate new portfolio value based on the next day’s returns
new_value = sum(allocation_per_stock * np.exp(log_returns.loc[log_returns.index[i + 1], stock]) for stock in top_stocks.index)
# Calculate and deduct tax if there is a profit
profit = new_value — cash
simulation_details.loc[log_returns.index[i + 1], ‘Profit Before Tax’] = round(profit, 2)

if profit > 0:
tax = profit * tax_rate
new_value -= tax
simulation_details.loc[log_returns.index[i + 1], ‘Tax Paid’] = round(tax, 2)
simulation_details.loc[log_returns.index[i + 1], ‘Portfolio Value’] = round(new_value, 2)

else:
# No allocation, so portfolio value remains the same
simulation_details.loc[log_returns.index[i + 1], ‘Portfolio Value’] = cash
# Update cash amount for the next round
cash = simulation_details.loc[log_returns.index[i + 1], ‘Portfolio Value’]
# Assign the initial amount to the first row
simulation_details.loc[log_returns.index[0], ‘Portfolio Value’] = initial_amount
return simulation_details

# Configuration for the momentum strategy simulation
initial_amount = 100000
top_n = 3
tax_rate = 0.15
frequency = ‘M’
simulation_details = simulate_momentum_strategy(daily_data, initial_amount, top_n, tax_rate, frequency)

Представление фрейма данных: simulation_details

Отслеживание индивидуальных инвестиций и базового уровня

Импульсная стратегия – это лишь один из многих подходов к инвестированию на фондовом рынке. В то время как эта стратегия основана на следовании тенденциям активов, другой подход включает в себя инвестирование фиксированной суммы в отдельные акции, отслеживая их динамику с течением времени. Кроме того, распространенной стратегией является равномерное распределение инвестиций между желаемыми активами. В этом эксперименте я установил этот равномерно распределенный подход в качестве «базового уровня». Стоимость этого портфеля рассчитывается как средняя позиция других активов за каждый период. По сути, он представляет собой равное вложение во все активы на начало периода.# Simulate how each individual stock would have performed over the same period
def track_individual_investments(data, initial_amount, simulation_details, period=’W’):
# Resample data based on the specified period
data = resample_data(data, period)
# Calculate returns based on the resampled data
returns = data.pct_change()
# Create a new DataFrame to store individual stock values over time
individual_investments = pd.DataFrame(index=data.index, columns=data.columns)
for stock in data.columns:
# Simulate an investment in each stock
individual_investments[stock] = (1 + returns[stock]).cumprod() * initial_amount
# Include the Portfolio Value from the momentum strategy
individual_investments[‘Portfolio Value’] = simulation_details[‘Portfolio Value’]
individual_investments[‘Baseline’] = individual_investments.iloc[:, :-1].T.mean()
# Adjust the first values to match the Initial Amount.
individual_investments.iloc[0, :] = initial_amount
return individual_investments.fillna(0).astype(int)

individual_investments_df = track_individual_investments(daily_data, initial_amount, simulation_details, frequency)

Представление фрейма данных: individual_investments_df

Показатели производительности

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

Мы решили использовать 10% годовых в качестве ориентира в t-тесте. Это решение основано на исторической средней доходности индекса S&P 500. Как сообщает NerdWallet, средняя доходность фондового рынка составляет около 10% в год на протяжении почти последнего века. Используя этот показатель, мы стремимся контекстуализировать эффективность нашей стратегии по сравнению с общепринятым и установленным средним значением по рынку.

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

Коэффициент Шарпа:

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

T-критерий и p-значение:

Функция t_test_portfolio_returns использует t-критерий для определения того, значительно ли доходность портфеля отклоняется от базовой ставки в 10%.

  • p-значение: Оно представляет собой вероятность того, что доходность нашего портфеля и базовая ставка происходят из одного и того же статистического распределения. В этом контексте мы вдвое уменьшаем p-значение, переводя его из двустороннего теста в односторонний, ориентируясь на направление отклонения нашей стратегии от эталонной ставки. Меньшее p-значение (обычно ниже 0,05) указывает на то, что доходность нашей стратегии значительно отличается от ставки 10%. В нашем исследовании низкое p-значение подтверждает эффективность нашей инвестиционной стратегии по сравнению с традиционной годовой доходностью в 10%.

Общие показатели портфеля:

Функция calculate_metrics объединяет все эти показатели, получая конечные значения каждой инвестиции, относительный рост, среднегодовую доходность, коэффициенты Шарпа и t-статистику вместе с p-значением. Это дает полное представление о производительности нашего портфеля.from scipy.stats import ttest_1samp

def calculate_sharpe_ratio(returns, annual_risk_free_rate=0.01, frequency=’D’):
# Adjust the risk-free rate based on the frequency
if frequency == ‘D’:
adjusted_rfr = (1 + annual_risk_free_rate) ** (1/252) — 1
elif frequency == ‘W’:
adjusted_rfr = (1 + annual_risk_free_rate) ** (1/52) — 1
elif frequency == ‘M’:
adjusted_rfr = (1 + annual_risk_free_rate) ** (1/12) — 1

excess_returns = returns — adjusted_rfr
return excess_returns.mean() / excess_returns.std()

def t_test_portfolio_returns(portfolio_returns, bench_annual_rate=0.1, frequency=’D’):
# Adjust the risk-free rate based on the frequency
if frequency == ‘D’:
adjusted_rfr = (1 + bench_annual_rate) ** (1/252) — 1
elif frequency == ‘W’:
adjusted_rfr = (1 + bench_annual_rate) ** (1/52) — 1
elif frequency == ‘M’:
adjusted_rfr = (1 + bench_annual_rate) ** (1/12) — 1

t_stat, p_value = ttest_1samp(portfolio_returns[1:], adjusted_rfr) # [1:] to exclude the NaN from pct_change
return t_stat, p_value

def calculate_metrics(dataframe, initial_amount, bench_annual_rate, frequency=’D’):
# Calculate the final and relative values
final_values = dataframe.iloc[-1]
relative_values = final_values / initial_amount — 1 # Subtract 1 to get the growth proportion

# Calculate mean return and Sharpe Ratio
returns = dataframe.pct_change()

if frequency == ‘D’:
annualization_factor = 252
elif frequency == ‘W’:
annualization_factor = 52
elif frequency == ‘M’:
annualization_factor = 12

# Corrected annualization of mean returns
mean_returns = (1 + returns.mean()) ** annualization_factor — 1
sharpes = returns.apply(calculate_sharpe_ratio, annual_risk_free_rate=0.01, frequency=frequency)

# Test if the portfolio returns are greater than the adjusted risk-free rate
portfolio_returns = dataframe[‘Portfolio Value’].pct_change()
t_stat, p_value = t_test_portfolio_returns(portfolio_returns, bench_annual_rate, frequency=frequency)

return final_values, relative_values, mean_returns, sharpes, t_stat, p_value / 2

bench_annual_rate = 0.1

# Calculate the metrics
final_values, relative_values, mean_returns, sharpes, t_stat, p_value = calculate_metrics(individual_investments_df, initial_amount, bench_annual_rate, frequency)

Визуализация: раскрашивание картины

Используя библиотеку Plotly, наши инвестиционные результаты динамически отображаются, представляя эволюцию стоимости портфеля с течением времени и ключевые статистические данные, такие как T-критерий и P-значение. Кроме того, сравнительные гистограммы дают представление о конечной стоимости инвестиций, относительном росте, годовых коэффициентах Шарпа и средней доходности, предлагая консолидированный снимок производительности и эффективности стратегии.import plotly.graph_objects as go
from plotly.subplots import make_subplots

def plot_combined_charts(dataframe, final_values, relative_values, sharpes, mean_returns):
labels = final_values.index
colors = [‘#636EFA’, ‘#EF553B’, ‘#00CC96’, ‘#AB63FA’, ‘#FFA15A’]

fig = make_subplots(rows=3, cols=2,
subplot_titles=(‘Portfolio Value Over Time’,
»,
‘Final Investment Values’,
‘Relative Investment Growth’,
‘Annualized Sharpe Ratios’,
‘Annualized Mean Returns’),
vertical_spacing=0.08)

# Portfolio Value line chart
fig.add_trace(go.Scatter(x=dataframe.index,
y=dataframe[‘Portfolio Value’],
mode=’lines’,
name=’Portfolio Value’,
line=dict(color=colors[0], width=2.5)),
row=1, col=1)

# T-test and P-value
significance_text = f»<b>T-test:</b> {t_stat:.2f}<br><b>P-value:</b> {p_value:.5f}»
if t_stat > 2 and p_value < 0.05:
significance_text += f»<br><b>Significantly different from {bench_annual_rate:.0%} per year!</b>»

fig.add_annotation(
text=significance_text,
showarrow=False,
xref=»x2″, yref=»y2″,
x=0.5, y=0.5,
font=dict(size=15),
bgcolor=»white»,
align=»center»
)

# Final values
fig.add_trace(go.Bar(x=labels,
y=final_values.values,
name=’Final Values ($)’,
text=[f»${v:,.2f}» for v in final_values.values],
textposition=’outside’,
marker_color=colors[1]),
row=2, col=1)

# Relative Growth
fig.add_trace(go.Bar(x=labels,
y=relative_values.values,
name=’Relative Growth’,
text=[f»{v:.2%}» for v in relative_values.values],
textposition=’outside’,
marker_color=colors[2]),
row=2, col=2)

# Sharpe Ratios
fig.add_trace(go.Bar(x=labels,
y=sharpes.values,
name=’Annualized Sharpe Ratio’,
text=[f»{v:.2f}» for v in sharpes.values],
textposition=’outside’,
marker_color=colors[3]),
row=3, col=1)

# Mean Returns
fig.add_trace(go.Bar(x=labels,
y=mean_returns.values,
name=’Annualized Mean Returns’,
text=[f»{v:.2%}» for v in mean_returns.values],
textposition=’outside’,
marker_color=colors[4]),
row=3, col=2)

# Update layout
fig.update_layout(title_text=»Investment Results Overview»,
title_font=dict(size=24, color=’black’, family=»Arial Black»),
title_pad=dict(t=10),
showlegend=False,
height=1500,
title_x=0.5,
bargap=0.05,
)

fig.show()

plot_combined_charts(individual_investments_df, final_values, relative_values, sharpes, mean_returns)

Заключение

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

  1. Фиксированный период: ограничен пятью годами, результаты могут варьироваться в зависимости от продолжительности.
  2. Основные средства: Концентрация на таких гигантах, как Apple и Amazon, диверсификация может изменить результаты.
  3. Рыночная благоприятность: Текущий рынок выиграл от импульсной стратегии, которая может быть последовательной не во всех сценариях.

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

Анализ акций со стратегией пробоя

Мы не только познакомим вас с ядром стратегии прорыва, но и подчеркнем значение тестов Шапиро-Уилка и Колмогорова-Смирнова в анализе распределения доходности акций.

Знакомство

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

В отличие от стратегии, основанной на импульсе, стратегия прорыва по своей сути является упреждающей. Вместо того, чтобы извлекать выгоду из текущих рыночных тенденций, его цель состоит в том, чтобы точно определить моменты, которые предвещают надвигающиеся тенденции, с помощью ключевых ценовых сдвигов. Наша оценка этой стратегии будет опираться не только на единичный подход; Мы воспользуемся многогранным инструментарием, который подчеркивает важность понимания распределения доходности акций. Такие инструменты, как построение гистограмм, будут жизненно необходимы, но суть нашего исследования основана на двух сложных тестах: тесте Шапиро-Уилка и тесте Колмогорова-Смирнова. В то время как первый известен своей чувствительностью к различению нормальных распределений, второй обеспечивает универсальность для любого типа распределения. Оба теста будут играть важную роль в исследовании нюансов возврата сигнала, измерении их нормальности и подтверждении их соответствия общей картине.

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

Получение биржевых данных

Любой анализ начинается с данных. Используя пакет yfinance от Python, были получены данные об акциях за последние пять лет десяти компаний, включая таких гигантов, как Apple (AAPL), Amazon (AMZN Microsoft (MSFT и т. д. Для каждой акции были извлечены цены закрытия, максимальные и минимальные цены.def fetch_stock_data(ticker_list, years=5):
end_date = datetime.now()
start_date = end_date — timedelta(days=years * 365)

close_data_df = pd.DataFrame()
high_data_df = pd.DataFrame()
low_data_df = pd.DataFrame()

for ticker in ticker_list:
stock = yf.Ticker(ticker)

hist_data = stock.history(period=’1d’, start=start_date, end=end_date)

close_data = hist_data[‘Close’].rename(ticker)
close_data_df = pd.merge(close_data_df, pd.DataFrame(close_data), left_index=True, right_index=True, how=’outer’)

high_data = hist_data[‘High’].rename(ticker)
high_data_df = pd.merge(high_data_df, pd.DataFrame(high_data), left_index=True, right_index=True, how=’outer’)

low_data = hist_data[‘Low’].rename(ticker)
low_data_df = pd.merge(low_data_df, pd.DataFrame(low_data), left_index=True, right_index=True, how=’outer’)

return close_data_df, high_data_df, low_data_df

# Fetch the data
ticker_list = [‘AAPL’, ‘AMZN’, ‘MSFT’, ‘GOOGL’, ‘META’, ‘TSLA’, ‘NVDA’, ‘ADBE’, ‘NFLX’, ‘INTC’]
years = 5

close, high, low = fetch_stock_data(ticker_list, years)

Вычисление максимумов и минимумов в окне

Для стратегии, основанной на импульсе, одним из важнейших шагов является определение максимумов и минимумов для акции в течение заданного периода времени. Для этого анализа рассматривалось окно в 50 дней. Были рассчитаны скользящие максимальные и минимальные цены для каждой акции в этом окне.def get_high_lows_lookback(high, low, lookback_days):
lookback_high = high.shift(1).rolling(lookback_days).max()
lookback_low = low.shift(1).rolling(lookback_days).min()

return lookback_high, lookback_low

lookback_days = 50
lookback_high, lookback_low = get_high_lows_lookback(high, low, lookback_days)

Генерация сигналов на покупку и продажу

Суть стратегии, основанной на импульсе, заключается в определении того, когда покупать (или открывать длинную позицию), а когда продавать (или открывать короткую позицию) в зависимости от движения цены. Была использована простая логика: если текущая цена закрытия больше максимума за последние 50 дней, срабатывает сигнал на покупку. И наоборот, если он меньше минимума за последние 50 дней, то выдается сигнал на продажу.def get_long_short(close, lookback_high, lookback_low):
long_signal = (close-lookback_high > 0).astype(‘int’)
short_signal = -(close-lookback_low < 0).astype(‘int’)
long_short = short_signal + long_signal

return long_short

signal = get_long_short(close, lookback_high, lookback_low)

Фильтрация сигналов

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

  1. clear_signals отфильтровывает новые сигналы в течение заданного window_size, если существует предыдущий сигнал.
  2. filter_signals различает признаки покупки (long_signals) и продажи (short_signals), применяет функцию очистки, а затем консолидирует их.

Таким образом, сигналы упрощаются, что позволяет нам больше сосредоточиться на четких и потенциально более эффективных торговых сигналах.def clear_signals(signals, window_size):
clean_signals = [0]*window_size

for signal_i, current_signal in enumerate(signals):
has_past_signal = bool(sum(clean_signals[signal_i:signal_i+window_size]))
clean_signals.append(not has_past_signal and current_signal)

clean_signals = clean_signals[window_size:]

return pd.Series(np.array(clean_signals).astype(int), signals.index)

def filter_signals(signal, lookahead_days):

long_signals = (signal > 0 ).astype(‘int’)
short_signals = -(signal < 0 ).astype(‘int’)

long_signals = long_signals.apply(lambda s: clear_signals(s, window_size = lookahead_days))
short_signals = short_signals.apply(lambda s: clear_signals(s, window_size = lookahead_days))

filtered_signal = long_signals + short_signals

return filtered_signal

signal_5 = filter_signals(signal, 5)
signal_10 = filter_signals(signal, 10)
signal_20 = filter_signals(signal, 20)

Прогнозирование будущих цен и анализ доходности цен

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

Примечание: В данном анализе не учитывались затраты на торговые операции.def get_lookahead_prices(close, lookahead_days):
lookahead_prices = close.shift(-lookahead_days)

return lookahead_prices

lookahead_5 = get_lookahead_prices(close, 5)
lookahead_10 = get_lookahead_prices(close, 10)
lookahead_20 = get_lookahead_prices(close, 20)def get_return_lookahead(close, lookahead_prices):
lookahead_returns = np.log(lookahead_prices/close)

return lookahead_returns

price_return_5 = get_return_lookahead(close, lookahead_5)
price_return_10 = get_return_lookahead(close, lookahead_10)
price_return_20 = get_return_lookahead(close, lookahead_20)

Получение возврата сигнала

Путем умножения сигнала на доходность с прогнозированием вперед была рассчитана ожидаемая доходность для каждой акции, основанная на нашей стратегии. По сути, это означает, что вы заработаете (или потеряете), если будете следовать сигналам.def get_signal_return(signal, lookahead_returns):
signal_return = signal * lookahead_returns

return signal_return

signal_return_5 = get_signal_return(signal_5, price_return_5)
signal_return_10 = get_signal_return(signal_10, price_return_10)
signal_return_20 = get_signal_return(signal_20, price_return_20)

Анализ и визуализация сигналов

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

Гистограммы возврата сигналов

Гистограммы обеспечивают визуальное представление распределения набора данных. Построены гистограммы доходности за 5, 10 и 20 дней. Интересно, что все сигнальные сигналы возвращались с отклонениями от стандартного нормального распределения.from scipy.stats import shapiro, kstest
import matplotlib.pyplot as plt
import warnings

dataframes = [signal_return_5, signal_return_10, signal_return_20]
colors = [‘blue’, ‘green’, ‘red’]
labels = [‘signal_return_5’, ‘signal_return_10’, ‘signal_return_20’]plt.figure(figsize=(10, 6))

for df, color, label in zip(dataframes, colors, labels):
# Filter out NaN and zero values and flatten the data
filtered_data = df.values[~pd.isna(df.values)]
filtered_data = filtered_data[filtered_data != 0.0]

# Plot the histogram
plt.hist(filtered_data, bins=30, edgecolor=’black’, alpha=0.5, color=color, label=label)

plt.title(‘Histogram of Signal Returns’)
plt.xlabel(‘Value’)
plt.ylabel(‘Frequency’)
plt.legend()
plt.show()

Проверка на нормальность

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

Мы использовали тест Шапиро-Уилка, распространенный метод проверки нормальности. Гипотеза (H0) для этого теста состоит в том, что данные были взяты из нормального распределения. p-значение указывает на силу доказательств против этой гипотезы. Как правило, более низкое p-значение означает, что мы отклоняем H0, предполагая, что данные не распределены нормально.means = []

for df, label in zip(dataframes, labels):
# Filter out NaN and zero values and flatten the data
filtered_data = df.values[~pd.isna(df.values)]
filtered_data = filtered_data[filtered_data != 0.0]

# Calculate the mean
mean_val = filtered_data.mean()
means.append(mean_val)

# Shapiro-Wilk Test
stat, p = shapiro(filtered_data)
alpha = 0.05
if p > alpha:
print(f»{label}: Looks Gaussian (fail to reject H0). p-value = {p:.5f}»)
else:
print(f»{label}: Does not look Gaussian (reject H0). p-value = {p:.5f}»)

print(«\nMeans of the distributions:»)
for label, mean in zip(labels, means):
print(f»{label}: {mean:.5f}»)

Из нашего анализа:

  • signal_return_5 имеет p-значение, близкое к нулю, поэтому не следует гауссову распределению.
  • signal_return_10 демонстрирует аналогичную тенденцию с p-значением, равным примерно нулю.
  • signal_return_20 согласуется с двумя предыдущими с p-значением 0,00002 (очень низкое).

Учитывая эти ничтожные p-значения, мы с уверенностью отвергаем гипотезу о том, что наши доходы взяты из нормального распределения для всех трех длительностей сигнала.

Кроме того, средние значения распределений дают представление о средней доходности:

  • Средняя доходность signal_return_5 составляет 0,00195.
  • Средняя доходность для signal_return_10 составляет 0,00262.
  • Средняя доходность для signal_return_20 составляет 0,00315.

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

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

Несмотря на то, что данные, по-видимому, не следовали нормальному распределению, важность этого наблюдения была подчеркнута при сравнении сигнальных сигналов с фактическим нормальным распределением. Расхождения были очевидны.for df, color, label in zip(dataframes, colors, labels):
# Filter out NaN and zero values and flatten the data
filtered_data = df.values[~pd.isna(df.values)]
filtered_data = filtered_data[filtered_data != 0.0]

# Parameters for normal distribution
mean_val = filtered_data.mean()
std_dev = filtered_data.std()

# Generate random samples from a normal distribution with the same mean and std_dev
normal_samples = np.random.normal(mean_val, std_dev, size=len(filtered_data))

# Plot histograms
plt.figure(figsize=(6, 5))
plt.hist(filtered_data, bins=30, edgecolor=’black’, alpha=0.5, color=color, label=label, density=True)
plt.hist(normal_samples, bins=30, edgecolor=’black’, alpha=0.5, color=’grey’, label=’Normal Dist.’, density=True, histtype=’step’)
plt.title(f’Histogram of {label} vs. Normal Distribution’)
plt.xlabel(‘Value’)
plt.ylabel(‘Density’)
plt.legend()
plt.show()

Доброта посадки

В нашем анализе мы использовали критерий Колмогорова-Смирнова (KS) для оценки соответствия доходности различных акций с нормальным распределением. Этот подход был тщательно выбран для сравнения доходности отдельных акций с нормальным распределением, определяемым средним значением и стандартным отклонением агрегированной доходности акций.# Filter out returns that don’t have a long or short signal.
long_short_signal_returns_5 = signal_return_5[signal_5 != 0].stack()
long_short_signal_returns_10 = signal_return_10[signal_10 != 0].stack()
long_short_signal_returns_20 = signal_return_20[signal_20 != 0].stack()

# Get just ticker and signal return
long_short_signal_returns_5 = long_short_signal_returns_5.reset_index().iloc[:, [1,2]]
long_short_signal_returns_5.columns = [‘ticker’, ‘signal_return’]
long_short_signal_returns_10 = long_short_signal_returns_10.reset_index().iloc[:, [1,2]]
long_short_signal_returns_10.columns = [‘ticker’, ‘signal_return’]
long_short_signal_returns_20 = long_short_signal_returns_20.reset_index().iloc[:, [1,2]]
long_short_signal_returns_20.columns = [‘ticker’, ‘signal_return’]import pandas as pd
import warnings
from scipy.stats import kstest

warnings.simplefilter(action=’ignore’, category=FutureWarning)

def calculate_kstest(long_short_signal_returns):
ks_values = pd.Series(dtype=’float64′)
p_values = pd.Series(dtype=’float64′)

for ticker, signals in long_short_signal_returns.groupby(‘ticker’)[‘signal_return’]:
mean = signals.mean()
std = signals.std(ddof=0)
standardized_signals = (signals — mean) / std
ks_value, p_value = kstest(standardized_signals, ‘norm’)
ks_values[ticker] = ks_value
p_values[ticker] = p_value

return ks_values, p_values

# Calculate KS test values for all three dataframes
ks_values_5, p_values_5 = calculate_kstest(long_short_signal_returns_5)
ks_values_10, p_values_10 = calculate_kstest(long_short_signal_returns_10)
ks_values_20, p_values_20 = calculate_kstest(long_short_signal_returns_20)

# Compile results into a DataFrame
results = pd.DataFrame({
‘ticker’: ks_values_5.index,
‘ks_value_5’: ks_values_5.values,
‘p_value_5’: p_values_5.values,
‘ks_value_10’: ks_values_10.values,
‘p_value_10’: p_values_10.values,
‘ks_value_20’: ks_values_20.values,
‘p_value_20’: p_values_20.values,
})

# Use style.bar to display the bars for easier visual comparison
results.style.bar(subset=[‘ks_value_5’, ‘p_value_5’, ‘ks_value_10’, ‘p_value_10’, ‘ks_value_20’, ‘p_value_20′], align=’zero’, color=[‘#d65f5f’, ‘#000000’])

При более внимательном рассмотрении результатов выявилось несколько трендов на разных тикерах и таймфреймах:

  • AAPL: Не было существенных отклонений от нормального распределения на всех трех таймфреймах (5, 10 и 20 дней), при этом p-значения превышали стандартный порог 0,05.
  • ADBE: Для 10-дневного таймфрейма p-значение 0,157879 указывает на близкий отход от нормы, но 5- и 20-дневные доходности, похоже, более точно соответствуют нормальному распределению.
  • GOOGL: 20-дневный таймфрейм представляет собой заметное отклонение от нормального распределения с p-значением 0,157280. Однако при более коротких сроках (5 и 10 дней) доходность существенно не отклоняется от нормы.
  • NFLX: 10-дневный таймфрейм с p-значением 0,282107 граничит с границей типичного уровня значимости, что предполагает потенциальный отход от нормальности. Тем не менее, в течение 5 и 20 дней доходность более близка к нормальному распределению.

Другие акции, такие как AMZN, INTC, META, MSFT, NVDA и TSLA, постоянно показывали p-значения выше порога 0,05 для всех таймфреймов, что указывает на то, что их доходность существенно не отклонялась от нормального распределения.

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

Заключение

Наше углубленное исследование распределения доходности акций с помощью критерия Шапиро-Уилка показало, что доходность многих акций отклоняется от ожидаемого распределения Гаусса. Такие идеи носят не только академический характер; Они бросают практический вызов основополагающим принципам, лежащим в основе многих финансовых стратегий.

Дополнительная глубина была добавлена с помощью критерия Колмогорова-Смирнова. Этот тест, будучи более универсальным, выявил четкие закономерности распределения по различным акциям. Например, в то время как акции AAPL и AMZN в целом соответствовали нормальному распределению, GOOGL и ADBE показали потенциальные отклонения в определенные периоды.

Очень важно понимать разницу между этими двумя тестами. Тест Шапиро-Уилка высоко ценится за свою чувствительность и идеально подходит для определения того, происходит ли выборка из нормально распределенной популяции. В отличие от этого, тест KS может быть использован для любого распределения, что делает его универсальным инструментом.

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

Кодирование пробоя акций на Python

  • Часть 1. Введение
  • Часть 2. Кодирование бэктестера для прорывов акций на Python
  • Часть 3. Программирование скринера прорывов акций на Python

Часть 1. Введение

Можем ли мы использовать Python, чтобы стать лучшим трейдером? В этой статье давайте посмотрим, как это возможно, ответив на следующий вопрос: как при наличии акции (или любой торгуемой ценной бумаги) мы можем сразу определить пробойные свечи на ее графике?

Настройка прорыва

В трейдинге пробой является сигналом к покупке. Сильные движения цены и объема сопровождают пробойную свечу. Взгляните на следующий пример настройки пробоя для AAPL в ноябре 2021 года.

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

Кроме того, как вы, возможно, читали в книгах по трейдингу, таких как «Торгуйте как волшебник фондового рынка» Минервини или «Как заработать деньги на акциях» О’Нила, пробойной свече предшествует низкая волатильность цен (короткие свечи) в предыдущие 10-20 свечей.

Наша задача по программированию

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

Вот наши условия, найдите каждую свечу, которая:

  1. зеленый цвет (т.е. цена закрытия выше цены открытия);
  2. имеет тело, которое является самым длинным в 10 дней;
  3. имеет тело, которое как минимум на 100% длиннее, чем среднее значение предыдущих 20 свечей (включая текущую свечу); и
  4. имеет объем, который как минимум на 50% выше, чем среднее значение предыдущих 20 свечей (включая текущую свечу).

Библиотека Yahoo-Fin

Мы будем извлекать биржевые данные из Yahoo Finance, используя библиотеку yahoo-fin с открытым исходным кодом. Вы можете установить его, выполнив эту команду: pip install yahoo-fin. Чтобы узнать больше об этой библиотеке, прочтите ее документацию здесь.

Для нашей задачи мы будем использовать его метод .get_data из его модуля stock_info.get_data очищает исторические цены по данному тикерному символу.

Метод имеет 5 параметров: get_data(ticker, start_date=None, end_date=None, index_as_date=True, interval=‘1d’). Единственным обязательным параметром является тикер, который не чувствителен к регистру для биржевого символа.

  • ticker = обязательный, биржевой символ
  • start_date = необязательный, в формате ‘дд‘dd/mm/yyyy’‘yyyy-mm-dd’. Его значением по умолчанию являются первые данные по акциям.
  • end_date = необязательный, в формате ‘дд‘dd/mm/yyyy’.‘yyyy-mm-dd’ Его значением по умолчанию являются самые последние данные по акции.
  • index_as_date = необязательный, значение по умолчанию — True, что означает, что индекс строки очищенного кадра данных — это дата.
  • interval = опционально, это интервал каждой свечи. Его значение по умолчанию — ‘1d’, что означает дневные цены. Другие значения: ‘‘1wk’‘ и ‘‘1mo’.

Кодирование нашей задачи

ПРИМЕЧАНИЕ: Мои коды реализованы в Jupyter Notebook.

Давайте сначала импортируем необходимые модули.from yahoo_fin.stock_info import get_data

import pandas as pd
import numpy as np

Затем давайте наскребет исторические цены акций. Давайте воспользуемся компанией Apple Inc. (AAPL) для этой демонстрации.# Get historical prices from first candle to the most recent candle
hist = get_data(‘AAPL’, index_as_date=False)

# Show the first 5 rows of our dataframe
hist.head()

Выходные данные предыдущих кодов:

Обратите внимание, что мне нужна дата в виде столбца (а не индекса), поэтому я закодировал index_as_date=False выше. Давайте проверим, сколько у нас свечей:

Каждая строка в нашем датафрейме — это дневная свеча. Как видите, за всю историю AAPL мы наскребли 10 811 свечей, используя всего несколько строк кодов.

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

Если вы торгуете завтра, вы не будете тратить свое время на размышления о том, произойдет ли дробление акций в будущем. Вы просто собираетесь использовать метод открытия-максимума-минимума-закрытия (OHLC) для своего технического анализа. И именно поэтому у нас есть следующие коды:# Remove the adjusted close column and rename our dataframe as «prices»

prices = hist.drop([‘adjclose’], axis=1)
prices.head()

Теперь мы добавляем несколько столбцов в наш фрейм данных. Давайте сначала сосредоточимся на расчетах для цен. Обратите внимание на мои комментарии на Python.# Add difference between closing price and opening price
# NOTE: O-to-C is the length of the candle’s body

prices[‘O-to-C’] = prices[‘close’] — prices[‘open’]

# Add 20-Day moving average for Open-to-Close column

prices[‘OC-20D-Mean’] = prices[‘O-to-C’].rolling(20).mean()

# Calculate the % change of the current day’s O-to-C relative to the moving average

prices[‘OC-%-from-20D-Mean’] = 100*(prices[‘O-to-C’] — prices[‘OC-20D-Mean’])/prices[‘OC-20D-Mean’]

# Get the maximum OC compared to the recent 10 candles (including the current candle)

prices[‘MaxOC_Prev10’] = prices[‘O-to-C’].rolling(10).max()

ПРИМЕЧАНИЕ: Мы использовали метод ..rolling() из pandas для вычисления скользящей средней и максимума.

Теперь добавим вычисляемые столбцы, которые включают объем. Опять же, обратите внимание на мои комментарии на Python.# Add 20-Day moving average for volume

prices[‘Volume-20D-Mean’] = prices[‘volume’].rolling(20).mean()

# Calculate the % change of the current volume relative to the moving average

prices[‘Volume-%-from-20D-Mean’] = 100*(prices[‘volume’] — prices[‘Volume-20D-Mean’])/prices[‘Volume-20D-Mean

Прежде чем показывать измененный фрейм данных, давайте сначала изменим порядок столбцов.# Print the columns for easy copy-pasting
prices.columns

Отсюда мы можем получить измененный фрейм данных с переставленными столбцами:# Rearrange the columns for our dataframe

prices = prices[[‘ticker’, ‘date’, ‘open’, ‘high’, ‘low’, ‘close’,
‘O-to-C’, ‘OC-20D-Mean’, ‘volume’, ‘Volume-20D-Mean’,
‘MaxOC_Prev10’, ‘Volume-%-from-20D-Mean’, ‘OC-%-from-20D-Mean’,
]]

# Show the 10 most recent rows

prices.tail(10)

Выходные данные предыдущих кодов:

Хороший! Мы на шаг ближе к нахождению строк, которые могут указывать на прорывы. Но прежде чем вы начнете радоваться, если вы prices.info() вы поймете, что есть нулевые значения:

Значения null являются результатом столбцов, в которых мы использовали метод .rolling() И эти нули находятся только в первых строках нашего исторического фрейма данных. Взгляните на фрагмент нашего датафрейма ниже.

В MaxOC_Prev10 нет вычисления для первых 9 строк, так как наш код для этого столбца требует 10 строк столбца O-to-C, прежде чем отобразится максимальное значение. Аналогичным образом, первые 19 строк для четырех столбцов, зависящих от наших 20-дневных скользящих средних, содержат нулевые значения.

Давайте удалим эти 19 строк с нулевыми значениями, потому что они не помогут нам найти пробойные свечи, которые на самом деле зависят от 20-дневных скользящих средних.# Remove rows with null values

prices = prices.dropna()
prices.info()

Результат предыдущих кодов:

Теперь у нас есть 10 792 строки (или свечи), готовые к анализу!

Потенциальные пробойные свечи

Как упоминалось ранее, наши условия пробоя:

1. зеленый цвет (т.е. цена закрытия выше цены открытия);prices[‘O-to-C’] >= 0.0

2. имеет тело, которое дольше всего в 10 дней;prices[‘O-to-C’] == prices[‘MaxOC_Prev10’]

3. имеет тело, которое как минимум на 100% длиннее среднего показателя предыдущих 20 свечей (включая текущую свечу);prices[‘OC-%-from-20D-Mean’] >= 100.0

4. имеет объем, который как минимум на 50% выше, чем в среднем за предыдущие 20 свечей (включая текущую).prices[‘Volume-%-from-20D-Mean’] >= 50.0

Сложив все это вместе, мы можем выбрать только те ряды, которые могут сигнализировать о пробойных свечах.condition = (prices[‘O-to-C’] >= 0.0) & (prices[‘O-to-C’] == prices[‘MaxOC_Prev10’]) & (prices[‘OC-%-from-20D-Mean’] >= 100.0) & (prices[‘Volume-%-from-20D-Mean’] >= 50.0)

breakouts = prices[condition]

breakouts

Выходные данные предыдущих кодов:

ПРИМЕЧАНИЕ: Выделенная строка является примером прорыва, упомянутым во введении выше.

По состоянию на октябрь 2023 года существует всего 167 потенциальных пробойных рядов для AAPL с 1980 по 2023 год. Действительно, пробойные свечи редки, но прибыльны (особенно когда продажа тоже сделана правильно). Прорыв случается редко для одной акции. Но на самом деле у вас есть свобода торговать от 5 до 10 (или более) акциями. Следовательно, с точки зрения любого рынка (NYSE, S&P 500, филиппинский фондовый рынок и т.д.), по крайней мере, одна из его акций может показать пробойный сетап в месяц.

Возвращаясь к нашему фрейму данных, нам нужны значения дат 167, чтобы мы могли проверить их на биржевом графике из Yahoo Finance, Trading View или любой другой торговой платформы. Таким образом, мы можем увидеть, действительно ли акция прибыльна с помощью пробойных сетапов.# Save the values under the ‘date’ column to a list

breakouts[‘date’].tolist()

Размещение всех кодов в одной функции

def potential_breakouts(ticker):
»’A function that returns date and prices for potential breakouts of a stock using historical daily prices»’

# Import libraries
from yahoo_fin.stock_info import get_data
import pandas as pd
import numpy as np

# Get the historical weekly prices from the specified start date and end date (both YYYY-mm-dd)
hist = get_data(ticker, index_as_date=False)

# Drop the adjusted close column
prices = hist.drop([‘adjclose’], axis=1)

# Get the length of candle’s body (from open to close)
prices[‘O-to-C’] = prices[‘close’] — prices[‘open’]

# Get the rolling mean of the candles’ bodies for recent 20 candles
prices[‘OC-20D-Mean’] = prices[‘O-to-C’].rolling(20).mean()

# Get the % change of the current OC relative from the rolling mean
prices[‘OC-%-from-20D-Mean’] = 100*(prices[‘O-to-C’] — prices[‘OC-20D-Mean’])/prices[‘OC-20D-Mean’]

# Get the maximum OC compared to the recent 10 candles
prices[‘MaxOC_Prev10’] = prices[‘O-to-C’].rolling(10).max()

# Get the rolling mean of volume for the recent 20 candles
prices[‘Volume-20D-Mean’] = prices[‘volume’].rolling(20).mean()

# Get the % change of the current volume relative from the rolling mean
prices[‘Volume-%-from-20D-Mean’] = 100*(prices[‘volume’] — prices[‘Volume-20D-Mean’])/prices[‘Volume-20D-Mean’]

# Drop the null values for the first 19 rows, where no mean can be computed yet
prices = prices.dropna()

# Rearrange columns
prices = prices[[‘ticker’, ‘date’, ‘open’, ‘high’, ‘low’, ‘close’,
‘O-to-C’, ‘OC-20D-Mean’, ‘volume’, ‘Volume-20D-Mean’,
‘MaxOC_Prev10’, ‘OC-%-from-20D-Mean’, ‘Volume-%-from-20D-Mean’,
]]

# Select the subset of dataframe where breakout conditions apply
# Conditions: 1. green candle, 2. candle’s body is longest in 10 days,
# 3. breakout volume is 50% higher than the rolling 20-day average, and
# 4. breakout candle has body that is 100% higher than the rolling 20-day average

condition = (prices[‘O-to-C’] >= 0.0) & (prices[‘O-to-C’] == prices[‘MaxOC_Prev10’]) & (prices[‘OC-%-from-20D-Mean’] >= 100.0) & (prices[‘Volume-%-from-20D-Mean’] >= 50.0)

breakouts = prices[condition]

return breakouts

Напоминание

Целью наших кодов является тестирование стратегии пробоя на истории. Не все входы на прорыв прибыльны. Некоторые пробойные свечи являются «качающимися свечами», что может означать, что за пробойной свечой следует рост цены (от 3% до 5%) в течение следующих 3 дней, но продолжает снижаться в следующие 3 недели.

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

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

Мой следующий план

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

Часть 2. Кодирование бэктестера для прорывов акций на Python

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

Быстрый отзыв

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

В трейдинге пробой является сигналом к покупке. Сильные движения цены и объема сопровождают пробойную свечу. Взгляните на следующий пример настройки прорыва для AAPL (Apple Inc.) в ноябре 2021 года. Его пробивная свеча сопровождается объемом выше среднего, и свеча самая длинная за 10 дней, прямо перед тем, как ее цена вырастет.

Пример настройки пробоя для AAPL в ноябре 2021 года

Условия для пробойной свечи:
Я уже говорил об условиях в моей предыдущей статье, но когда я закончил свою функцию Python, я понял, что мне нужно добавить одну (т.е. третью ниже). Следовательно, свеча является пробивной, если она:

  1. зеленый цвет (т.е. цена закрытия выше цены открытия);
  2. имеет тело, которое является самым длинным в 10 дней;
  3. [ДОПОЛНЕНИЕ] имеет давление продажи (т.е. расстояние между максимумом свечи и ценой закрытия), которое составляет 40% или короче тела;
  4. имеет тело, которое как минимум на 100% длиннее, чем среднее значение предыдущих 20 свечей (включая текущую свечу); и
  5. имеет объем, который как минимум на 50% выше, чем среднее значение предыдущих 20 свечей (включая текущую свечу).
Части биржевой свечи. Изображение с сайта https://www.pristinebreakthroughs.com/blog/single.php?id=1#.

Библиотека Yahoo-Fin

Мы будем извлекать биржевые данные из Yahoo Finance, используя библиотеку yahoo-fin с открытым исходным кодом. Вы можете установить его, выполнив эту команду: pip install yahoo-fin. Чтобы узнать больше об этой библиотеке, прочтите ее документацию здесь.

Кодирование функции бэктестера

Шаг 1: Для данной акции получите датафрейм, который содержит только пробойные ряды (или свечи) за всю ее историю.

Моя предыдущая статья была посвящена этому шагу. Здесь я просто покажу вам фрейм данных для AAPL (Apple Inc.), который мы будем использовать в этом блоге.

Фрагмент кадра данных breakouts для AAPL. Колонка SellingPressure еще не была частью первой статьи.

Шаг 2: Получаем прибыль за каждую свечу.

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

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

  1. Период хранения = максимум 10 дней
  2. Цена покупки = цена закрытия пробойной свечи
  3. Идеальная цена продажи = самая высокая цена в течение 10 дней
  4. Stop Loss Price = цена открытия пробойной свечи
    ПРИМЕЧАНИЕ: Когда акция падает до уровня цены открытия пробоя, мы должны принудительно выйти (с убытком или отрицательной прибылью). Это часть управления рисками, которое является важнейшей концепцией для успешной торговли/инвестирования. Вы не ждете, пока рынок развернется в вашу пользу, он может и не повернуться.
Иллюстрация точек входа и выхода для AAPL

Рассуждая таким образом, теперь мы можем программно собирать прибыль на пробивную свечу. Обратите внимание на мои комментарии на Python в кодах ниже. (ПРИМЕЧАНИЕ: В приведенных ниже кодах предполагается, что у нас уже есть фрейм breakouts и дневной исторический фрейм данных, называемый prices из шага 1.)## GET THE PROFIT (OR LOSS) PER CANDLE

# Get the index (from the dataframe) of each breakout row, which is necessary for looping later
breakouts_indices = breakouts.index.tolist()

profits = []
for index in breakouts_indices:
# For a given breakout candle index, slice the historical prices dataframe 10 rows RIGHT AFTER the breakout row
ten_rows_after_a_breakout = prices.iloc[index+1:index+11]

# Compute the highest price within the next 10 days RIGHT AFTER the breakout candle
highest_price_within10days = ten_rows_after_a_breakout[‘high’].max()

# Compute the lowest price within the next 10 days RIGHT AFTER the breakout candle
lowest_price_within10days = ten_rows_after_a_breakout[‘low’].min()

# Get the row index corresponding for the highest_price_within10days
highest_price_index = ten_rows_after_a_breakout[ten_rows_after_a_breakout[‘high’] == highest_price_within10days].index[0]

# Get the row index corresponding for the lowest_price_within10days
lowest_price_index = ten_rows_after_a_breakout[ten_rows_after_a_breakout[‘low’] == lowest_price_within10days].index[0]

# Calculate our Buy Price, which is the breakout candle’s close
breakout_close = breakouts.loc[index, ‘close’]

# Calculate our Stop Loss Price, which is the breakout candle’s open
breakout_open = breakouts.loc[index, ‘open’]

## GET THE PROFITS:
# If lowest_price_index is lower (or earlier) than highest_price_index, then we sold at stop loss before reaching the highest_price_within10days
# This counts as negative profit (a loss)

# If highest_price_index is lower (or earlier) than the lowest_price_index, this should count as a win
# This means we were able to exit with a profit before the stock goes to the stop loss within 10 days

if lowest_price_within10days <= breakout_open:
if highest_price_index < lowest_price_index:
profit = round(100*(highest_price_within10days — breakout_close)/breakout_close, 2)
profits.append(profit)

elif lowest_price_index <= highest_price_index:
profit = round(100*(breakout_open — breakout_close)/breakout_close, 2)
profits.append(profit)

else:
profit = round(100*(highest_price_within10days — breakout_close)/breakout_close, 2)
profits.append(profit)

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

Шаг 3: Рассчитайте соответствующие торговые вероятности.

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

В следующих строках показано, как закодировать эти вероятности.## GET PROFIT PER TYPE TO CALCULATE SOME PROBABILITIES
wins = []
breakevens = []
losses = []
for profit in profits:
if profit > 0.0:
wins.append(profit)
elif profit == 0.0:
breakevens.append(profit)
elif profit < 0.0:
losses.append(profit)

# Calculate some trading probabilities
win_rate = round(100*len(wins)/len(profits), 2)
breakeven_rate = round(100*len(breakevens)/len(profits), 2)
loss_rate = round(100*len(losses)/len(profits), 2)

ave_positive_profit = round(sum(wins)/len(wins), 2)
ave_negative_profit = round(sum(losses)/len(losses), 2)

Шаг 4: Визуализируйте распределение прибыли (Выходные данные 1).

Наш первый вывод — это гистограмма с помощью sns.histplot() Мы также отобразим вероятности, которые мы рассчитали ранее, с помощью plt.text().sns.histplot(pd.Series(profits), bins=20)
plt.title(f»Distribution of Breakout Profits for {ticker.upper()}»)
plt.text(0.95, 0.95, f»Total Breakouts: {len(profits)} \n Ave. Positive Profit: {ave_positive_profit}% \n Ave. Negative Profit: {ave_negative_profit}% \n Win Rate: {win_rate}% \n Loss Rate: {loss_rate}% \n Breakeven Rate: {breakeven_rate}%»,
ha=’right’, va=’top’, transform=plt.gca().transAxes)
plt.ylabel(‘Number of Breakouts’)
plt.xlabel(‘Profit (%)’)
plt.show()

Шаг 5: Распечатайте дополнительную информацию (Выход 2).

Наш второй вывод — это f-строка, которая печатает даты для самых ранних и последних свечей пробоя.# Supply other information, in addition to the chart, for the output
# NOTE: breakout_dates are in timestamp, so we have to convert to date format
breakout_dates = pd.to_datetime(breakouts[‘date’])
earliest_breakout = breakout_dates.min().strftime(‘%Y-%m-%d’)
latest_breaktout = breakout_dates.max().strftime(‘%Y-%m-%d’)

supplementary_info = f»Additional Info: The first breakout for {ticker} was observed on {earliest_breakout} while the most recent breakout was on {latest_breaktout}. The holding period for each breakout trade is maximum of 10 days.»

Размещение всех кодов в одной функции

Теперь у нас есть все строки для кодирования нашей функции breakout_profitability(ticker) которой нужен только тикер для акции, которую вы хотите проанализировать.

https://python.plainenglish.io/media/105ccca342a4743853054f186dc5f36a

ПРИМЕЧАНИЕ: Последняя строка возвращает supplementary_info. Но функция также покажет гистограмму, так как мы вставили plt.show() в строку 133.

Примеры выходных данных

Воспользуемся нашей функцией для акций Apple и Google.

Прорывная прибыльность для Apple
Прорывная прибыльность для Google

Что теперь?

Используя мою функцию breakout_profitability(ticker) у вас теперь есть возможность проверить (бесплатно!), является ли наша стратегия пробоя прибыльной для данной акции. Просто запустите функцию для акции, которую вы хотите проанализировать. Я признаю, что моя функция ограничена акциями США от Yahoo Finance, где их символы представлены должным образом.

(Я планирую разработать аналогичную функцию для моих местных филиппинских акций. Символы филиппинских акций в Yahoo Finance не совпадают с их локальными символами. Не знаю почему.)

Математическое ожидание (EV) — отдельная тема для трейдинга, но для его расчета можно использовать вывод нашей функции. Используя ключевые числа, полученные из моей функции, вы можете получить EV = (Средняя положительная прибыль для X долларов)*(Процент выигрышей) — (Средняя отрицательная прибыль для X долларов)*(Коэффициент потерь).

Например, предположим, что наша инвестиция в AAPL составляет $1000 (таким образом, X = $1000). В среднем, если акция движется в нашу сторону после пробоя, мы получаем 0,1742*1000 = $174,2. Однако, если акция вынуждает нас выйти в убыток, в среднем мы теряем 0,0616*1000 = $61,6.

Таким образом, наше математическое ожидание для этой стратегии пробоя составляет EV = ($174,2)*(0,7445) — ($61,6)*(0,2555) = $113,95. Это средняя прибыль для AAPL, если вы снова и снова торгуете на его пробойных сетапах (исходя из наших условий).

Внимание: Это не инвестиционный совет! Если вы завтра торгуете AAPL и инвестируете $1000, вы не обязательно получите $113,95 в течение 10 дней. Ваша сделка может быть прибыльной, а может быть остановлена с убытком. Никто не знает.

Но, используя нашу функцию, вы можете сравнить тысячи акций и увидеть, какие из них имеют самые высокие EV, а затем выбрать несколько для отслеживания и, возможно, торговать (когда появятся их прорывы). Пожалуйста, помните, что моя функция зависит от условий покупки и продажи, которые я установил выше. Эти условия могут не совпадать с сигналами пробоя.

Проверить мой блокнот Colab

Вы можете проверить все коды в моей записной книжке Jupyter Notebook, которая загружена на мой GitHub здесь.

Часть 3. Программирование скринера прорывов акций на Python

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

В этой статье мы также рассмотрим еще один способ, которым Python может помочь нам стать более эффективными трейдерами. Здесь мы закодируем скринер для тех акций, у которых была хотя бы одна пробивная свеча за последние 5 дней.

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

Стратегия пробоя

В трейдинге пробой является сигналом к покупке. Сильные движения цены и объема сопровождают пробойную свечу. Взгляните на следующий пример настройки прорыва для AAPL (Apple Inc.) в ноябре 2021 года. Его пробивная свеча сопровождается объемом выше среднего, и свеча самая длинная за 10 дней, прямо перед тем, как ее цена вырастет.

Пример настройки пробоя для AAPL в ноябре 2021 года

Условия для пробойной свечи:
Как уже говорилось в предыдущей статье, мы определяем свечу как пробивную, если она:

  1. зеленый цвет (т.е. цена закрытия выше цены открытия);
  2. имеет тело, которое является самым длинным в 10 дней;
  3. имеет давление на продажу (т.е. расстояние между максимумом свечи и ценой закрытия) на 40% или короче тела;
  4. имеет тело, которое как минимум на 100% длиннее, чем среднее значение предыдущих 20 свечей (включая текущую свечу); и
  5. имеет объем, который как минимум на 50% выше, чем среднее значение предыдущих 20 свечей (включая текущую свечу).
Части биржевой свечи. Изображение с сайта https://www.pristinebreakthroughs.com/blog/single.php?id=1#.

Наша задача по программированию

Наша цель — создать функцию, которая выводит список акций, которые:

  • соответствовать минимальному среднему объему (т.е. количеству торгуемых акций), который мы указываем; и
  • За последние 5 дней у вас была какая-либо свеча пробоя.

Библиотека Yahoo-Fin

Мы будем извлекать биржевые данные из Yahoo Finance, используя библиотеку yahoo-fin с открытым исходным кодом. Вы можете установить его, выполнив эту команду: pip install yahoo-fin. Чтобы узнать больше об этой библиотеке, прочтите ее документацию здесь.

Для нашей задачи мы будем использовать его метод .get_data из его модуля stock_info.get_data очищает исторические цены по данному тикерному символу.

Метод имеет 5 параметров: get_data(ticker, start_date=None, end_date=None, index_as_date=True, interval=‘1d’). Единственным обязательным параметром является тикер, который не чувствителен к регистру для биржевого символа.

  • ticker = обязательный, биржевой символ
  • start_date = необязательный, в формате ‘дд‘dd/mm/yyyy’‘yyyy-mm-dd’. Его значением по умолчанию являются первые данные по акциям.
  • end_date = необязательный, в формате ‘дд‘dd/mm/yyyy’.‘yyyy-mm-dd’ Его значением по умолчанию являются самые последние данные по акции.
  • index_as_date = необязательный, значение по умолчанию — True, что означает, что индекс строки очищенного кадра данных — это дата.
  • interval = опционально, это интервал каждой свечи. Его значение по умолчанию — ‘1d’, что означает дневные цены. Другие значения: ‘‘1wk’‘ и ‘‘1mo’.

Кодирование нашей задачи

Давайте попробуем разработать наши коды только для одного тикера. Давайте воспользуемся Google (тикер: GOOGL) для этой демонстрации. Моя начальная фаза состоит в том, чтобы создать фрейм данных для последних 5 свечей со столбцами, которые могут помочь нам выбрать строки, указывающие на прорыв (если таковые имеются). После этого этапа мы сможем расширить исходные коды и создать финальную функцию скринера.

ПРИМЕЧАНИЕ: Мои коды реализуются с помощью Google Colab, который представляет собой записную книжку на базе Jupyter.

Импорт библиотекfrom yahoo_fin.stock_info import get_data
import pandas as pd
import numpy as np

Соскребите последние 25 свечей

Используя метод get_data() мы можем очистить исторические цены GOOGL. Но нам не нужны все его свечи за всю его историю. Для этого мы будем использовать метод ..tail() из Pandas, чтобы получить последние 25 свечей.latest_25_candles = get_data(‘GOOGL’, index_as_date=False).tail(25)
latest_25_candles

Выходные данные предыдущих кодов:

Последние 25 свечей GOOGL (на момент написания статьи)

Вы, наверное, задаетесь вопросом: зачем мы наскребли 25 свечей, если наш скринер хочет посмотреть только на последние 5 дней?

Ответ на этот вопрос кроется в условиях пробойных свечей, которые мы определили ранее. Помните, что два из этих 5 условий основаны на 20-свечных скользящих средних. В частности, свеча является пробойной, если ее тело как минимум на 100% длиннее, чем среднее значение предыдущих 20 свечей, и объем, который как минимум на 50% выше, чем средний показатель предыдущих 20 свечей .

Каждая строка в нашем датафрейме — это свеча. Каждая из последних 5 строк (или дней) должна обращаться к предыдущим 20 строкам для вычисления 20-свечного скользящего среднего. Вы увидите это через минуту.

Но сначала давайте удалим скорректированный столбец закрытия.# Drop the adjusted close column
latest_25_candles = latest_25_candles.drop([‘adjclose’], axis=1)

Нам нужны только столбцы Open-High-Low-Close (OHLC), как мы обычно делаем для наших торговых решений. Если столбец adjclose отличается от столбца close, это означает, что дробление акций уже произошло. Если вы торгуете завтра, вы не будете тратить время на размышления о том, будет ли дробление акций в будущем. Вы просто будете использовать OHLC в качестве основы для своего технического анализа.

Добавьте необходимые столбцы

Теперь это та часть, где мы добавляем столбцы в наш фрейм данных. Обратите внимание на мои комментарии к Python ниже.# Get the selling pressure (i.e. distance between candle’s high and close)
latest_25_candles[‘SellingPressure’] = latest_25_candles[‘high’] — latest_25_candles[‘close’]

# Get the length of candle’s body (from open to close)

latest_25_candles[‘O-to-C’] = latest_25_candles[‘close’] — latest_25_candles[‘open’]

# Get the rolling mean of the candles’ bodies for recent 20 candles

latest_25_candles[‘OC-20D-Mean’] = latest_25_candles[‘O-to-C’].rolling(20).mean()

# Get the % change of the current OC relative from the rolling mean

latest_25_candles[‘OC-%-from-20D-Mean’] = 100*(latest_25_candles[‘O-to-C’] — latest_25_candles[‘OC-20D-Mean’])/latest_25_candles[‘OC-20D-Mean’]

# Get the maximum OC compared to the recent 10 candles

latest_25_candles[‘MaxOC_Prev10’] = latest_25_candles[‘O-to-C’].rolling(10).max()

# Get the rolling mean of volume for the recent 20 candles

latest_25_candles[‘Volume-20D-Mean’] = latest_25_candles[‘volume’].rolling(20).mean()

# Get the % change of the current volume relative from the rolling mean

latest_25_candles[‘Volume-%-from-20D-Mean’] = 100*(latest_25_candles[‘volume’] — latest_25_candles[‘Volume-20D-Mean’])/latest_25_candles[‘Volume-20D-Mean’]

Если мы latest_25_candles в следующей ячейке, то получим следующий результат:

Изменен latest_25_candles кадр данных для GOOGL

Теперь последние (нижние) 5 строк содержат информацию для скользящих средних, таких как OC-20D-Mean и Volume-20D-Mean. Они нужны для того, чтобы увидеть, удовлетворяет ли какая-либо строка нашим условиям прорыва.

Получаем только последние 5 строк (или свечей) из нашего датафреймаlatest_5_candles = latest_25_candles.tail(5)
latest_5_candles

Результатом предыдущих кодов является наш датафрейм для анализа:

Latest_5_candles датафрейм для GOOGL

Выберите только те строки, которые удовлетворяют нашим условиям прорываcondition = (latest_5_candles[‘O-to-C’] >= 0.0) & (latest_5_candles[‘O-to-C’] == latest_5_candles[‘MaxOC_Prev10’]) & (latest_5_candles[‘SellingPressure’]/latest_5_candles[‘O-to-C’] <= 0.40) & (latest_5_candles[‘OC-%-from-20D-Mean’] >= 100.0) & (latest_5_candles[‘Volume-%-from-20D-Mean’] >= 50.0)

breakouts = latest_5_candles[condition].reset_index(drop=True)

breakouts

Напомним: Пять условий для пробойной свечи были оговорены ранее.

Результатом предыдущих кодов является пустой кадр данных. Он был пуст, потому что на момент написания статьи не было прорыва для последних пяти свечей GOOGL.

Если он пустой, то он не должен быть просеян нашим скринером. Если длина датафрейма (количество строк) больше нуля, то тикер должен быть отсеян. Эта условная логика поможет нам создать нашу финальную функцию скринера, которую вы увидите ниже.

Теперь мы знаем, как обработать данный тикер и напечатать его пробивные строки (максимум 5 строк), если таковые имеются. Теперь мы готовы расширить эту идею, создав функцию, которая выводит не датафрейм, а акции (из заданного списка), которые недавно пробились.

Наша функция скрининга

https://marvinrubia.medium.com/media/f8ce0ac3bcb987da79c20197aaf98ec5

Наша функция breakouts_screener() принимает два stocks, который представляет собой список биржевых символов, и min_ave_volume, который является минимальным средним объемом (за последние 5 дней), который мы хотим отфильтровать.

В приведенных выше кодах строки с 10 по 50 должны быть вам знакомы. Они показывают коды, которые мы разработали ранее для GOOGL. Однако для этой функции мы ожидаем список биржевых символов. Следовательно, мы перебираем цикл для каждого тикера и получаем каждый датафреймbreakouts (за последние 5 дней).

Теперь строки с 56 по 58 предписывают нашей функции сохранять тикер в контейнере screened_stocks только в том случае, если фрейм данных breakouts для акции содержит хотя бы одну строку. Этот процесс продолжается до тех пор, пока цикл не достигнет последнего запаса в предоставленном списке.

Возможно, что не было проверенных акций, а это означает, что ни одна акция из предоставленного списка не имела прорыва за последние 5 дней. Таким образом, строки с 63 по 70 возвращают желаемый вывод на основе этой логики.

Использование нашей функции

Давайте сначала попробуем составить список из 3 акций.my_list = [‘AAPL’, ‘MSFT’, ‘GOOGL’]

breakouts_screener(stocks=my_list, min_ave_volume=100000)

Вывод: No stocks from the provided list with recent minimum average volume of 100000 shares showed any breakout candle in the past 5 days.

Затем давайте попробуем это сделать с акциями S&P 500. Однако, прежде чем мы сможем это сделать, нам понадобится список символов для компаний S&P 500:# Access wiki page for S&P 500 stocks using pd.read_html() method
# The result of the following codes is lists of dataframes

url = ‘https://en.wikipedia.org/wiki/List_of_S%26P_500_companies’
sp500_wiki = pd.read_html(url)

# Get the our desired dataframe for S&P 500

sp500_df = sp500_wiki[0]

# Get only the stock symbols and save them in a list object

sp500_symbols_list = sp500_df[‘Symbol’].tolist()
print(‘Number of stock symbols: ‘, len(sp500_symbols_list))

# Print the first 20 symbols in our list
sp500_symbols_list[:20]

Используя метод .read_html() от Pandas, мы можем получить биржевые символы компаний из индекса S&P 500. Оттуда мы получаем нужный нам список, который мы назвали sp500_symbols_list.

Теперь мы можем передать этот список в нашу функцию:breakouts_screener(stocks=sp500_symbols_list, min_ave_volume=100000)

Выходные данные: The stock/s with at least one breakout candle in the past 5 days is/are [‘AMAT’, ‘BLK’, ‘CPB’, ‘GIS’, ‘HWM’, ‘INTC’, ‘TGT’, ‘ULTA’].

Я запустил все это 19 ноября 2023 года. К тому времени, когда вы прочитаете эту статью и запустите мою функцию, ваш результат может быть другим.

Что теперь?

Предыдущие 8 отобранных акций удовлетворяют нашим условиям пробоя. Это не значит, что вы должны торговать всеми из них. Единственное, что вы должны проверить графики этих проверенных акций, чтобы решить, какими из них стоит торговать (если таковые имеются). Основная цель нашей функции breakouts_screener() — значительно сократить время, затрачиваемое на поиск торговых возможностей. После запуска функции для компаний из индекса S&P 500 моему Google Colab потребовалось всего 5 минут, чтобы вывести 8 акций, указанных выше.

Фундаментальный анализ акций с использованием Python

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

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

Прежде чем перейти к коду Python, давайте сначала коснемся основ фундаментального анализа и веб-парсинга.

ЧТО ТАКОЕ ФУНДАМЕНТАЛЬНЫЙ АНАЛИЗ?

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

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

ОЧИСТКА ВЕБ-СТРАНИЦ

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

В этой статье мы будем использовать «Красивый суп», который представляет собой пакет Python для извлечения данных из ФАЙЛОВ HTML и XML. Он создает дерево синтаксического анализа сложного HTML-документа, которое затем используется для чтения данных.

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

Этапы, связанные с веб-парсингом

ШАГ 1: Определение URL-адреса для извлечения данных

ШАГ 2: Проверьте страницу, чтобы понять ее HTML-структуру

ШАГ 3: Напишите код для извлечения необходимых данных из HTML

Парсинг данных с других веб-сайтов

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

Чтобы контролировать трафик, некоторые веб-сайты определяют роботов.txt файл. Это дает указание на URL-адреса, которые не должны использоваться для веб-парсинга с этого конкретного веб-сайта. Для получения более подробной информации о роботах.txt, обратитесь сюда.

ДАВАЙТЕ ПОЦАРАПАЕМ ЕГО!!

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

В этой статье мы рассмотрим акции из NSE и используем веб-страницу из screener.in для извлечения фундаментальных деталей конкретной акции. Та же логика может быть применена и к любой другой веб-странице. Код может потребовать незначительных изменений для согласования с HTML-структурой этой веб-страницы.

Цель этой статьи состоит в том, чтобы предоставить начальный код для кого-то, чтобы быстро получить свою собственную стратегию анализа. Следовательно, область действия ограничена получением некоторой базовой информации и применением простой стратегии BUY/WAIT.

ИЗВЛЕЧЕНИЕ ФУНДАМЕНТАЛЬНЫХ ДАННЫХ

Для этого анализа мы рассмотрим следующие АКЦИИ ИТ с Национальной фондовой биржи (NSE): INFY, HCLTECH, LTI, TCS, LTTS, WIPRO

Infosys — Получение рыночной капитализации с помощью веб-парсинга

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

ШАГ 1: Определите URL-адрес для извлечения данных

URL: https://www.screener.in/company/INFY

ШАГ 2: Проверьте страницу, чтобы понять ее HTML-структуру

С помощью браузера проверьте страницу (щелкните правой кнопкой мыши на странице и выберите опцию проверки). HTML-структуру страницы можно изучить здесь.

В этом примере HTML-страница выглядит следующим образом.

Как видно из структуры HTML, рыночная капитализация присутствует в рамках класса div «company-ratios». Внутри этого класса он находится внутри ul-элемента (Unordered List Element) с идентификатором «top-ratios». Внутри этого рыночная капитализация доступна в виде простого элемента диапазона.

ШАГ 3: Напишите код для извлечения необходимых данных из HTML

Используя красивый пакет супа, содержимое данного URL-адреса может быть считано в объект дерева. Затем его можно пройти, пока мы не достигнем требуемых данных.

Используя приведенный ниже код, рыночная капитализация акций Infy может быть извлечена с веб-страницы скринера.https://medium.com/media/49af295489f30764746778773c2fc520

Выпуск:

Получение других фундаментальных деталей акций

Другие параметры акции также могут быть получены с аналогичным подходом. Это может быть повторено для любого количества акций. Здесь мы извлекли данные для 6 различных ИТ-акций.

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

Ниже приведена таблица с кратким изложением фундаментальных данных, извлеченных для 6 ИТ-акций.

Сводка извлеченных данных о запасах

Вот краткое руководство по извлеченным данным:

  • Market_Cap — общая рыночная стоимость размещенных акций компании
  • Цена — текущая рыночная цена акции
  • Максимум — 52-недельный максимум доли
  • Минимум — 52-недельный минимум акции
  • PE — Отношение цены к прибыли
  • ROE — Рентабельность собственного капитала
  • ROCE — Рентабельность задействованного капитала
  • Дивиденды — Дивидендная доходность компании

АНАЛИЗ ЗАПАСОВ

Как только данные получены, пришло время изучить их.

В качестве примера сравнивается рыночная капитализация различных ИТ-акций.

Рыночная капитализация Infy составляет почти половину TCS. L&T Technology Services (LTTS) имеет самую низкую рыночную капитализацию среди 6 рассматриваемых акций.

СРАВНЕНИЕ ЗАПАСОВ ОДНОГО И ТОГО ЖЕ СЕКТОРА

Фундаментальный анализ обычно используется для сравнения акций одного и того же сектора. Коэффициент PE, ROE и ROCE являются одними из общих параметров, используемых для сравнения.

Сравнение параметров PE-ROE-ROCE (Изображение по автору)

PE — Соотношение цены и прибыли: Он измеряет текущую цену акций относительно прибыли на акцию.

ROE — Рентабельность собственного капитала: Это отношение чистой прибыли к акционерному капиталу.

ROCE — Рентабельность занятости капитала: ROCE — это отношение операционной прибыли компании к ее общему используемому капиталу.

PE + ROE + ROCE:

  • Коэффициент ПЭ обычно не используется изолированно. Он либо сравнивается с другими компаниями из той же отрасли (например: PE INFY сравнивается с PE HCL, LTI, TCS и т. Д.), Либо сравнивается с отраслевым PE (PE Infy сравнивается с PE ИТ-сектора)
  • Более высокий PE указывает на то, что акции оцениваются высоко. Более низкий PE указывает на хорошую инвестиционную возможность (при условии, что другие основы компании хороши)
  • Высокий ROE указывает на то, что компания хорошо конвертирует свою прибыль в прибыль.
  • Более высокий ROCE указывает на то, что компания генерирует более высокую доходность для держателей долгов, чем для держателей акций. Чем выше значение коэффициента ROCE, тем выше шансы на прибыль.
  • ROE рассматривает доходность только с точки зрения акционеров, тогда как ROCE также учитывает долг и другие обязательства. Это обеспечивает лучшее представление о финансовых показателях для компаний со значительным долгом.
  • Если значение ROCE выше, чем значение ROE, это означает, что компания эффективно использует свои долги для снижения стоимости капитала.
  • Г-н Уоррен Баффет, один из самых успешных инвесторов 20-го века, предпочитает компании, где значения ROE и ROCE почти близки друг к другу, и оба выше 20%.

СОЗДАНИЕ ИНДИВИДУАЛЬНЫХ СТРАТЕГИЙ

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

Стратегия прибыли-убытка

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

КУПИТЬ, если:

  • Чистая прибыль компании стабильно растет в последние несколько лет
  • Текущая рыночная цена по крайней мере на 10% ниже 52-недельного максимума (акции не торгуются вокруг своего исторического максимума)

Применение этой стратегии с учетом чистой прибыли за последние 3 года дает следующие рекомендации для 6 рассматриваемых ИТ-акций.

СТРАТЕГИЯ ПРИБЫЛИ-УБЫТКА — ИТ-АКЦИИ
ВИЗУАЛИЗАЦИЯ СТРАТЕГИИ ПРИБЫЛИ-ПОТЕРЬ — ИТ-АКЦИИ

Ниже приведены подробности чистой прибыли за последние 3 года, а также текущая цена акций и 52-недельный максимум для INFY и HCLTECH

Подробная информация о стратегии, применяемой на акциях Infy и HCL

Применяя эту стратегию, существует рекомендация покупать акции Infy, LTI и Wipro, что подразумевает, что они показали последовательный рост своей чистой прибыли за последние 3 года.

СОБИРАЯ ВСЁ ВМЕСТЕ

Обратитесь к следующему репозиторию github, чтобы получить доступ к полному коду, используемому для очистки веб-страниц и выполнения фундаментального анализа, : Fundamental Analyzer

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

Заключение

В этой статье мы рассмотрели только одну стратегию для выполнения рекомендации BUY/WAIT. Аналогично могут быть реализованы другие стратегии (фундаментальные или технические) для конкретной акции. Консолидированные результаты по всем стратегиям могут быть использованы для принятия окончательного решения BUY/WAIT/SELL.

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

Фундаментальный анализ акций с использованием API Python

В этой статье мы рассмотрим 10 фундаментальных индикаторов для выбора акций.

Отказ от ответственности: Информация, представленная здесь, предназначена только для информационных целей и не предназначена для личных финансовых, инвестиционных или других советов.

Ключевые показатели:

  1. EPS (Earnings Per Share) — часть прибыли компании, которая присваивается каждой акции
  2. P/E (Price to Earnings) — соотношение между ценой акций компании и ее прибылью на акцию. Это помогает инвесторам определить, является ли акция недооцененной или переоцененной по сравнению с другими акциями в том же секторе.
  3. PEG (прогнозируемый рост прибыли) — рассчитывается путем деления P/E акции на прогнозируемый 12-месячный форвардный темп роста выручки. В целом, PEG ниже 1 является хорошим знаком, а PEG выше 2 указывает на то, что акция может быть переоценена
  4. FCFY (Free Cash Flow Yield) — коэффициент финансовой состоятельности, который сравнивает свободный денежный поток на акцию, который, как ожидается, заработает компания, с ее рыночной стоимостью на акцию. Более низкий коэффициент указывает на менее привлекательную инвестиционную возможность.
  5. PB (Price to Book) — коэффициент 1 указывает на то, что акции компании торгуются в соответствии с ее балансовой стоимостью. P/B выше 1 означает, что компания торгуется с премией к балансовой стоимости, а P/B ниже 1 указывает на акции, которые могут быть недооценены по отношению к активам компании.
  6. ROE (Return on Equity) — позволяет инвесторам оценить, насколько эффективно компания использует свой капитал для получения прибыли. Более высокая рентабельность капитала свидетельствует о более эффективном использовании акционерного капитала, что может привести к увеличению спроса на акции и повышению цены акций, а также увеличению прибыли компании в будущем.
  7. P/S (Price to Sales) — определяет справедливую стоимость акций на основе рыночной капитализации и выручки компании. Он показывает, насколько рынок оценивает продажи компании, что может быть эффективным при оценке акций роста, которые еще не принесли прибыли или не работают так, как ожидалось, из-за временной неудачи.
  8. DPR (Dividend Payment Ratio) — отношение общей суммы дивидендов, выплаченных акционерам, к чистой прибыли компании.
  9. DY (Dividend Yield Ratio) — коэффициент показывает сумму, выплачиваемую компанией в виде дивидендов каждый год, по отношению к цене ее акций. Это оценка доходности инвестиций в акции только за счет дивидендов.
  10. CR (Current Ratio) — измеряет способность компании погашать свои текущие обязательства (подлежащие погашению в течение одного года) за счет своих оборотных активов, таких как денежные средства, дебиторская задолженность и запасы. Чем выше коэффициент, тем лучше ликвидность компании.
  11. Бета — это мера волатильности акции по отношению к рынку в целом. Акции, которые со временем колеблются больше, чем рынок, имеют коэффициент бета выше 1,0. Если акция движется меньше, чем рынок, коэффициент бета акции меньше 1,0.

Доступ к данным

Мы будем использовать yfinance API для получения данных из Yahoo Finance. Мы сосредоточимся на информационном компоненте тикера, который является одним из многих компонентов (например, отчет о прибылях и убыткахденежный поток и т. д.), предоставляемых API.

Заметка:

Однако по состоянию на октябрь/ноябрь 2023 г. доступ к компоненту info приводит к ошибке 404. Чтобы обойти эту проблему, я использую обходной путь, предложенный на Github. Однако используйте это решение только в том случае, если вы получаете ошибку 404; Другие указывают на то, что проблема носит региональный характер. Кроме того, я буду использовать файлы, полученные с помощью отдельной программы, чтобы уменьшить количество вызовов API к YF, что может ограничить интенсивное использование. Это программное обеспечение, помимо обходного пути, можно найти на Github.

Подход

  1. Загрузка данных по каждому биржевому символу в каталог
  2. Использование записной книжки Jupyter для анализа данных

Здесь обсуждается только Часть 2 (анализ), так как Часть 1 представляет собой простую программу для загрузки и сохранения данных в формате JSON в каталог.

Служебная программа для загрузки данных, модуль в качестве обходного пути и записная книжка Jupyter доступны на GitHub.

Библиотеки Python

Необходимые библиотеки Python:

  • pandas — использовать фрейм данных
  • numpy — для доступа к np.nan
  • JSON — для работы с данными JSON

Импорт библиотек

import json

# For DataFrame
import pandas as pd
import numpy as np

Конфигурация

# List of stock symbols we need to run fundamental analysis on — any symbol added here must have the json file
# containing stock info from YF
SYMBOLS = [‘INTU’,’CDNS’,’WDAY’,’ROP’,’TEAM’,’ADSK’,’DDOG’,’ANSS’,’ZM’,’PTC’,\
‘BSY’,’GRAB’,’SSNC’,’APP’,’AZPN’,’MANH’,’ZI’,’NICE’]

# Path to read stock data from YF
DATA_PATH = ‘path to accesss json files’

# Dictionary to collect data to create a DF later
data = {
‘Symbol’: [],
‘Name’: [],
‘Industry’: [],
‘EPS (fwd)’: [],
‘P/E (fwd)’: [],
‘PEG’: [],
‘FCFY’ : [],
‘PB’: [],
‘ROE’ : [],
‘P/S (trail)’: [],
‘DPR’ : [],
‘DY’ : [],
‘CR’ : [],
‘Beta’: [],
‘Price’: [],
’52w Low’: [],
’52w High’: []
}

Загружает данные

Это служебный метод для извлечения индикаторов из заданных данных данных JSON и заполнения словаря данныхdef load_data(json_data):
data[‘Symbol’].append(json_data[‘symbol’])
data[‘Name’].append(json_data[‘longName’])
data[‘Industry’].append(json_data[‘industry’])
data[‘Price’].append(json_data[‘currentPrice’])

# Could be that some indicators are not available; use NaN if this is the case

if ‘forwardEps’ in json_data:
data[‘EPS (fwd)’].append(json_data[‘forwardEps’])
else:
data[‘EPS (fwd)’].append(np.nan)

if ‘forwardPE’ in json_data:
data[‘P/E (fwd)’].append(json_data[‘forwardPE’])
else:
data[‘P/E (fwd)’].append(np.nan)

if ‘pegRatio’ in json_data:
data[‘PEG’].append(json_data[‘pegRatio’])
else:
data[‘PEG’].append(np.nan)

if (‘freeCashflow’ in json_data) and (‘marketCap’ in json_data):
fcfy = (json_data[‘freeCashflow’]/json_data[‘marketCap’]) * 100
data[‘FCFY’].append(round(fcfy, 2))
else:
data[‘FCFY’].append(np.nan)

if ‘priceToBook’ in json_data:
data[‘PB’].append(json_data[‘priceToBook’])
else:
data[‘PB’].append(np.nan)

if ‘returnOnEquity’ in json_data:
data[‘ROE’].append(json_data[‘returnOnEquity’])
else:
data[‘ROE’].append(np.nan)

if ‘priceToSalesTrailing12Months’ in json_data:
data[‘P/S (trail)’].append(json_data[‘priceToSalesTrailing12Months’])
else:
data[‘P/S (trail)’].append(np.nan)

data[‘DPR’].append(json_data[‘payoutRatio’] * 100)

if ‘dividendYield’ in json_data:
data[‘DY’].append(json_data[‘dividendYield’])
else:
data[‘DY’].append(0.0)

if ‘beta’ in json_data:
data[‘Beta’].append(json_data[‘beta’])
else:
data[‘Beta’].append(np.nan)

if ‘currentRatio’ in json_data:
data[‘CR’].append(json_data[‘currentRatio’])
else:
data[‘CR’].append(np.nan)

if ‘fiftyTwoWeekLow’ in json_data:
data[’52w Low’].append(json_data[‘fiftyTwoWeekLow’])
else:
data[’52w Low’].append(np.nan)

if ‘fiftyTwoWeekHigh’ in json_data:
data[’52w High’].append(json_data[‘fiftyTwoWeekHigh’])
else:
data[’52w High’].append(np.nan)

Примечания:

  1. Никаких дополнительных вычислений не требуется, за исключением Cash Flow Yield
  2. Numpy NaN вставляется для любого отсутствующего индикатора; Позже мы удалим эти записи из анализа

Загружает биржевые данные из json-файлов

for symbol in SYMBOLS:
# Specify the full path to load JSON data
file_name = f'{DATA_PATH}/{symbol}.json’
try:
# Open the file in read mode
with open(file_name, ‘r’) as file:
# Use json.load() to parse the JSON data from the file
load_data(json.load(file))
except FileNotFoundError:
print(f»File ‘{file_name}’ not found.»)
except json.JSONDecodeError as e:
print(f»Error decoding JSON data: {e}»)
except Exception as e:
print(f»An error occurred: {e}»)

Перебирает SYMBOLS, объявленные в разделе Configuration, и вызывает метод load_data для заполнения словаря данных

Создание фрейма данных

Любая акция со значением NaN для индикатора перемещается в отдельный фрейм данных. Мы также добавим новый столбец, чтобы обеспечить последующую визуализацию с помощью стилей, чтобы показать, какие акции находятся вблизи своего 52-недельного минимума, а какие — около 52-недельного максимума. Например, показатель в 90% указывает на то, что нынешняя цена очень близка к своему 52-недельному максимуму.# Create a DF using the dictionary
df = pd.DataFrame(data)

# Save any stocks with NaN values
df_exceptions = df[df.isna().any(axis=1)]

# Remove any stocks with NaN values
df=df.dropna()

# Reset index after dropping rows with NaN values
df.reset_index(drop=True, inplace=True)

# Add 52 week price range
df[’52w Range’] = ((df[‘Price’] — df[’52w Low’])/(df[’52w High’] — df[’52w Low’]))*100

df_exceptions

Ниже приведен частичный вывод после создания фрейма данных.

Результат загрузки данных из JSON-файлов

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

Исключения — индикаторы со значениями NaN

Добавляет стили

Служебный метод для добавления стилей к ранее созданному фрейму данныхdef make_pretty(styler):
# Column formatting
styler.format({‘EPS (fwd)’: ‘${:.2f}’, ‘P/E (fwd)’: ‘{:.2f}’, ‘PEG’: ‘{:.2f}’,
‘FCFY’: ‘{:.2f}%’, ‘PB’ : ‘{:.2f}’, ‘ROE’ : ‘{:.2f}’, ‘P/S (trail)’: ‘{:.2f}’,
‘DPR’: ‘{:.2f}%’, ‘DY’: ‘{:.2f}%’, ‘CR’ : ‘{:.2f}’, ‘Beta’: ‘{:.2f}’, ’52w Low’: ‘${:.2f}’,
‘Price’: ‘${:.2f}’, ’52w High’: ‘${:.2f}’, ’52w Range’: ‘{:.2f}%’
})
# Set the bar visualization
styler.bar(subset = [’52w Range’], align = «mid», color = [«salmon», «cornflowerblue»])

# Grid
styler.set_properties(**{‘border’: ‘0.1px solid black’})

# Set background gradients
styler.background_gradient(subset=[‘EPS (fwd)’], cmap=’Greens’)
styler.background_gradient(subset=[‘P/E (fwd)’], cmap=’Greens’)
styler.background_gradient(subset=[‘PEG’], cmap=’Greens’)
styler.background_gradient(subset=[‘FCFY’], cmap=’Greens’)
styler.background_gradient(subset=[‘PB’], cmap=’Greens’)
styler.background_gradient(subset=[‘ROE’], cmap=’Greens’)
styler.background_gradient(subset=[‘P/S (trail)’], cmap=’Greens’)
styler.background_gradient(subset=[‘DPR’], cmap=’Greens’)
styler.background_gradient(subset=[‘DY’], cmap=’Greens’)
styler.background_gradient(subset=[‘CR’], cmap=’Greens’)

# No index
styler.hide(axis=’index’)

# Tooltips
styler.set_tooltips(
ttips, css_class=’tt-add’,
props=[
(‘visibility’, ‘hidden’),
(‘position’, ‘absolute’),
(‘background-color’, ‘salmon’),
(‘color’, ‘black’),
(‘z-index’, 1),
(‘padding’, ‘3px 3px’),
(‘margin’, ‘2px’)
]
)
# Table column headers are aligned
styler.set_table_styles([dict(selector=’th’, props=[(‘text-align’, ‘center’)])])

# Left text alignment for some columns
styler.set_properties(subset=[‘Symbol’, ‘Name’, ‘Industry’], **{‘text-align’: ‘left’})
return styler

Добавляет всплывающие подсказки

Служебный метод для добавления всплывающих подсказок к ранее созданному фрейму данныхdef populate_tt(df, tt_data, col_name):
stats = df[col_name].describe()

per25 = round(stats.loc[‘25%’], 2)
per50 = round(stats.loc[‘50%’], 2)
per75 = round(stats.loc[‘75%’], 2)

# Get position based on the column name
pos = df.columns.to_list().index(col_name)

for index, row in df.iterrows():
pe = row[col_name]
if pe == stats.loc[‘min’]:
tt_data[index][pos] = ‘Lowest’
elif pe == stats.loc[‘max’]:
tt_data[index][pos] = ‘Hightest’
elif pe <= per25:
tt_data[index][pos] = ‘25% of companies under {}’.format(per25)
elif pe <= per50:
tt_data[index][pos] = ‘50% of companies under {}’.format(per50)
elif pe <= per75:
tt_data[index][pos] = ‘75% of companies under {}’.format(per75)
else:
tt_data[index][pos] = ‘25% of companies over {}’.format(per75)

Применение стилей и подсказок

# Initialize tool tip data — each column is set to » for each row
tt_data = [[» for x in range(len(df.columns))] for y in range(len(df))]

# Gather tool tip data for indicators
populate_tt(df, tt_data, ‘EPS (fwd)’)
populate_tt(df, tt_data, ‘P/E (fwd)’)
populate_tt(df, tt_data, ‘PEG’)
populate_tt(df, tt_data, ‘FCFY’)
populate_tt(df, tt_data, ‘PB’)
populate_tt(df, tt_data, ‘ROE’)
populate_tt(df, tt_data, ‘P/S (trail)’)
populate_tt(df, tt_data, ‘DPR’)
populate_tt(df, tt_data, ‘DY’)
populate_tt(df, tt_data, ‘CR’)

# Create a tool tip DF
ttips = pd.DataFrame(data=tt_data, columns=df.columns, index=df.index)

# Add table caption and styles to DF
df.style.pipe(make_pretty).set_caption(‘Fundamental Indicators’).set_table_styles(
[dict(selector=’caption’,props=[(‘font-size’, ‘150%’), (‘font-weight’, ‘bold’)])]

Ниже приведен пример выходных данных после применения стилей и подсказок:

Результаты после применения стилей и подсказок

Заключение

В этой статье описывается альтернативный подход к визуализации фундаментальных индикаторов с помощью Pandas Data Frame.

Для целей анализа очень важно выбрать группу связанных акций. Например, высокий или низкий PE имеет значение только в рамках группы похожих акций. Кроме того, файлы данных необходимо поддерживать в актуальном состоянии, так как записная книжка Jupyter полагается на них.

Панель мониторинга анализа акций на Python

За последние пару недель я разработал панель анализа цен на акции с использованием библиотеки Python Dash. Моим основным вдохновением для макета был веб-сайт Trading View. Панель должна была отображать свечной график с набором используемых индикаторов и некоторыми другими дополнительными деталями о выбранной пользователем акции. Все эти фрагменты информации будут извлечены известным pandas.data_reader. Объект DataReader.

Визуальный элемент, который мы собираемся построить, показывает производительность некоторых компаний, которые являются частью бразильского фондового рынка BOVESPA с 1 января 2015 года по 31 декабря 2021 года.

Код проекта можно разделить на четыре основных раздела, которые расположены по порядку:

1. Сбор и очистка данных

2. Импорт данных и диаграммы по умолчанию

3. Макет приложения

4. Интерактивность

Загрузка необходимых данных

В первый момент необходимо получить названия акций, чтобы DataReader мог собирать свои данные в Интернете. Чтобы выполнить эту работу, я нашел интересную веб-страницу, которая отображает таблицу с идентификатором BOVESPA компаний вместе с экономическим сектором, к которому они относятся.

Код с этого этапа включает в себя часть раздела «Сбор и очистка данных» проекта.

# These are the libaries to be used.
import dash
from dash import html, dcc, dash_table, callback_context
import plotly.graph_objects as go
import dash_trich_components as dtc
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import json
import pandas as pd
from pandas_ta import bbands
import pandas_datareader as web
import numpy as np
import datetime
from scipy.stats import pearsonr

# 1. Data Collection & Cleaning 
stocks = pd.read_html(
    'https://blog.toroinvestimentos.com.br/empresas-listadas-b3-bovespa', decimal=',')
stocks_sectores = stocks[1]
stocks_sectores.columns = stocks_sectores.iloc[0]
stocks_sectores.drop(index=0, inplace=True)
stocks_sectores_dict = stocks_sectores[[
    'Ticker', 'Setor']].to_dict(orient='list')

# Implementing the economic sector names as the dictionay key and their stocks as values.
d = {}
for stock, sector in zip(stocks_sectores_dict['Ticker'], stocks_sectores_dict['Setor']):
    if sector not in d.keys():
        d[sector] = [stock]
    else:
        d[sector].append(stock)

# Correcting a tiny issue with the resulting dictionary: sometimes, two of its keys 
# were concerning the very same economic sector! Let's merge them into a single one.
d['Bens Industriais'].extend(d['Bens industriais'])
d.pop('Bens industriais')
d['Bens Industriais']
d['Energia Elétrica'].extend(d['Energia elétrica'])
d.pop('Energia elétrica')
d['Energia Elétrica']
d['Aluguel de Veículos'].extend(d['Locação de veículos'])
d.pop('Locação de veículos')
d['Construção civil'].extend(d['Construção'])
d.pop('Construção')
d['Shopping Centers'].extend(d['Exploração de imóveis'])
d.pop('Exploração de imóveis')

# Now, we'll translate the economic sectors names to English for better understanding.
translate = {'Alimentos': 'Food & Beverages', 'Varejo': 'Retail', 
             'Companhia aérea': 'Aerospace', 'Seguros': 'Insurace', 
             'Shopping Centers': 'Shopping Malls', 'Financeiro': 'Finance',
             'Mineração': 'Mining', 'Químicos': 'Chemical', 'Educação': 'Education',
             'Energia Elétrica': 'Electrical Energy', 'Viagens e lazer': 'Traveling',
             'Construção civil': 'Real Estate', 'Saúde': 'Health', 
             'Siderurgia e Metalurgia': 'Steel & Metallurgy',
             'Madeira e papel': 'Wood & Paper', 'Aluguel de Veículos': 'Vehicle Rental',
             'Petróleo e Gás': 'Oil & Gas', 'Saneamento': 'Sanitation', 
             'Telecomunicações': 'Telecommunication', 'Tecnologia': 'Technology', 
             'Comércio': 'Commerce', 'Bens Industriais': 'Industrial Goods'}
for key, value in translate.items():
    d[value] = d.pop(key)


# Unfortunately, some of the stocks listed in the dictionary cannot be accessed with yahoo's API. 
# Thus, we'll need to exclude them.
for sector in list(d.keys()):
    for stock in d[sector]:
        # Trying to read the stock's data and removing it if it's not possible.
        try:
            web.DataReader(f'{stock}.SA', 'yahoo', '01-01-2015', '31-12-2021')
        except:
            d[sector].remove(stock)
    # After the removing process is completed, some economic sectors may not have 
    # any stocks at all, so they won't be of use for the project.
    if d[sector] == []:
        d.pop(sector)

# Converting it into a json file
with open("sector_stocks.json", "w") as outfile:
    json.dump(d, outfile)

Конечным результатом этой операции является JSON-файл под названием «sector_stocks.json», имеющий свои ключи и значения названий экономических секторов и их соответствующих акций.

Установка диаграмм по умолчанию

Как вы поняли в GIF-файле визуального элемента, панель мониторинга содержит в общей сложности три диаграммы. Основным из них является свечной график, отображающий ценовую информацию OHLC. Два других показывают некоторое представление о производительности компании за последние 52 недели. Линейный график представляет среднюю недельную цену акции, а спидометр показывает, насколько текущая цена находится от минимальных и максимальных значений, достигнутых в течение периода.

Dash требует, чтобы мы установили версии по умолчанию для этих визуальных элементов. Они будут мгновенно отображаться после загрузки приборной панели. Принимая это во внимание, я решил, что графики будут охватывать данные о запасах гиганта индустрии напитков AMBEV (ABEV3) по стандарту. Обратите внимание, что следующий код представляет собой часть проекта «Импорт данных и диаграммы по умолчанию».

# 2. Data Importing & Default Charts

# The standard stock displayed when the dashboard is initialized will be ABEV3.
ambev = web.DataReader('ABEV3.SA', 'yahoo',
                       start='01-01-2015', end='31-12-2021')

# 'ambev_variation' is a complementary information stored inside the card that 
# shows the stock's current price (to be built futurely). It presents, as its 
# variable name already suggests, the stock's price variation when compared to 
# its previous value.
ambev_variation = 1 - (ambev['Close'].iloc[-1] / ambev['Close'].iloc[-2])

# 'fig' exposes the candlestick chart with the prices of the stock since 2015.
fig = go.Figure()

# Observe that we are promtly filling the charts with AMBEV's data.
fig.add_trace(go.Candlestick(x=ambev.index,
                             open=ambev['Open'],
                             close=ambev['Close'],
                             high=ambev['High'],
                             low=ambev['Low'],
                             name='Stock Price'))
fig.update_layout(
    paper_bgcolor='black',
    font_color='grey',
    height=500,
    width=1000,
    margin=dict(l=10, r=10, b=5, t=5),
    autosize=False,
    showlegend=False
)
# Setting the graph to display the 2021 prices in a first moment. 
# Nonetheless,the user can also manually ajust the zoom size either by selecting a 
# section of the chart or using one of the time span buttons available.

# These two variables are going to be of use for the time span buttons.
min_date = '2021-01-01'
max_date = '2021-12-31'
fig.update_xaxes(range=[min_date, max_date])
fig.update_yaxes(tickprefix='R$')

# The output from this small resample operation feeds the weekly average price chart.
ambev_mean_52 = ambev.resample('W')[
    'Close'].mean().iloc[-52:]
fig2 = go.Figure()
fig2.add_trace(go.Scatter(x=ambev_mean_52.index, y=ambev_mean_52.values))
fig2.update_layout(
    title={'text': 'Weekly Average Price', 'y': 0.9},
    font={'size': 8},
    plot_bgcolor='black',
    paper_bgcolor='black',
    font_color='grey',
    height=220,
    width=310,
    margin=dict(l=10, r=10, b=5, t=5),
    autosize=False,
    showlegend=False
)
fig2.update_xaxes(showticklabels=False, showgrid=False)
fig2.update_yaxes(range=[ambev_mean_52.min()-1, ambev_mean_52.max()+1.5],
                  showticklabels=False, gridcolor='darkgrey', showgrid=False)

# Making a speedometer chart which indicates the stock' minimum and maximum closing prices
# reached during the last 52 weeks along its current price.
df_52_weeks_min = ambev.resample('W')['Close'].min()[-52:].min()
df_52_weeks_max = ambev.resample('W')['Close'].max()[-52:].max()
current_price = ambev.iloc[-1]['Close']
fig3 = go.Figure()
fig3.add_trace(go.Indicator(mode='gauge+number', value=current_price,
                            domain={'x': [0, 1], 'y': [0, 1]},
                            gauge={
                                'axis': {'range': [df_52_weeks_min, df_52_weeks_max]},
                                'bar': {'color': '#606bf3'}}))
fig3.update_layout(
    title={'text': 'Min-Max Prices', 'y': 0.9},
    font={'size': 8},
    paper_bgcolor='black',
    font_color='grey',
    height=220,
    width=280,
    margin=dict(l=35, r=0, b=5, t=5),
    autosize=False,
    showlegend=False
)

Выполнив эту задачу, мы можем, наконец, начать создание нашей приборной панели!

Настройка макета

Большинство компонентов панели мониторинга содержатся в двух столбцах Bootstrap. Левая секция покрывает 75% доступной ширины, в то время как правая занимает оставшееся пространство.

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

Карусель стоковых вариаций

Создание этого компонента подводит нас к одному из технических ограничений проекта. Поскольку карусель должна показывать изменение цены от группы акций, ей придется сделать несколько запросов к API Yahoo Finance. Это может повредить скорости системы быстрого отображения приборной панели.

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

При создании документа «sector_stocks.json» также создается второй JSON-файл. Он будет содержать названия акций в качестве ключей и их изменение цены в качестве значений. Поскольку временное покрытие приборной панели зафиксировано до 1 декабря 2021 года, это не создаст никаких проблем для проекта.

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

# 1. Data Collection & Cleaning (Continuance)

# A function that is going to measure the stocks' price variation.
def variation(name):
    df = web.DataReader(f'{name}.SA', 'yahoo',
                        start='29-12-2021', end='30-12-2021')
    return 1-(df['Close'].iloc[-1] / df['Close'].iloc[-2])


# Listing the companies to be shown in the Carousel.
carousel_stocks = ['ITUB4', 'BBDC4', 'VALE3', 'PETR4', 'PETR3',
                   'ABEV3', 'BBAS3', 'B3SA3', 'ITSA4', 'CRFB3', 'CIEL3',
                   'EMBR3', 'JBSS3', 'MGLU3', 'PCAR3', 'SANB11', 'SULA11']

# This dictionary will later be converted into a json file.
carousel_prices = {}
for stock in carousel_stocks:
    # Applying the 'variation' function for each of the list elements.
    carousel_prices[stock] = variation(stock)

# Turning 'carousel_prices' into a json file.
with open('carousel_prices.json', 'w') as outfile:
    json.dump(carousel_prices, outfile)

HTML Spans представляют название акции и изменение ее цены.

# 2. Data Importing & Default Charts (Ending)

# Loading the dashboard's 'sector_stock' and 'carousel_prices'  json files.
sector_stocks = json.load(open('sector_stocks.json', 'r'))
carousel_prices = json.load(open('carousel_prices.json', 'r'))

# 3. Application's Layout
app = dash.Dash(external_stylesheets=[dbc.themes.CYBORG])

# Beginning the layout. The whole dashboard is contained inside a Div and a Bootstrap
# Row.
app.layout = html.Div([
    dbc.Row([
        dtc.Carousel([

            html.Div([
                # This span shows the name of the stock.
                html.Span(stock, style={
                     'margin-right': '10px'}),

                # This other one shows its variation.
                html.Span('{}{:.2%}'.format('+' if carousel_prices[stock] > 0 else '', 
                     carousel_prices[stock]), 
                     style={'color': 'green' if carousel_prices[stock] > 0 else 'red'})
            ]) for stock in sorted(carousel_prices.keys())
        ], id='main-carousel', autoplay=True, slides_to_show=4),
      
      
    ])
])

Левая часть панели мониторинга

Как уже было сказано, большая часть содержимого приложения размещается внутри двух столбцов Bootstrap. Левый составляет 75% доступной ширины, а другой занимает остальное пространство. Глядя на GIF этой статьи, мы видим, что первыми функциями левой части панели мониторинга являются два раскрывающихся списка.

Раскрывающиеся списки

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

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

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

# 3. Application's Layout (Continuance)

# Place the following code under the top carousel structure. 
# The column below will occupy 75% of the width available in the dashboard.
        dbc.Col([

            # This row holds the dropdowns responsible for the selection of the stock
            # which informations are going to be displayed.
            dbc.Row([
                
                # Both dropdowns are placed inside two Bootstrap columns with equal length.
                dbc.Col([
                  # A small title guiding the user on how to use the component.
                    html.Label('Select the desired sector',
                               style={'margin-left': '40px'}),
                  
                  # The economic sectors dropdown. It mentions all the ones that are 
                  # available in the 'sector_stocks' dictionary.
                    dcc.Dropdown(options=[{'label': sector, 'value': sector}
                                          for sector in sorted(list(sector_stocks.keys()))],
                                 
                                 # The dashboard's default stock is ABEV3, which pertains 
                                 # to the 'Food & Beverages' sector. So make sure 
                                 # these informations are automatically displayed 
                                 # when the dashboard is loaded. 
                                 value='Food & Beverages',
                                 
                                 # It is essential to create an identification for the 
                                 # components which will be used in interactivity operations.
                                 id='sectors-dropdown', 
                                 style={'margin-left': '20px', 'width': '400px'}, 
                                 searchable=False)
                ], width=6),
              
              # The column holding the stock names dropdown.
                dbc.Col([
                    
                    # Nothing new here. Just using the same commands as above.
                    html.Label('Select the stock to be displayed'),
                    dcc.Dropdown(
                        id='stocks-dropdown',
                        value='ABEV3',
                        style={'margin-right': '20px'},
                        searchable=False
                    )
                ], width=6)
            ]),
                
       # The left major column closing bracket
       ], width=9),
            
  # These are the dashboard's major dbc.Row and html.Div closing brackets.
        ])
  ])
                
# 4. Interactivity

# Allowing the stock dropdown to display the companies that are pertained to the 
# economic sector chosen.
@ app.callback(
    
    # Observe how important the components id are to reference from where the function's
    # input comes from and in which element its output is used.
@ app.callback(
    Output('stocks-dropdown', 'options'),
    Input('sectors-dropdown', 'value')
)
def modify_stocks_dropdown(sector):
    
    # The 'stocks-dropdown' dropdown values are the stocks that are part of the
    # selected economic domain.
    stocks = sector_stocks[sector]
    return stocks

Приведенного выше кода достаточно для генерации обоих элементов и активации их внутренних отношений.

Свечной график, кнопки TimeSpan и индикаторы

В этом разделе будут рассмотрены три компонента, так как будет единый метод управления отношениями между ними.

Свечной график является наиболее актуальной особенностью. Он показывает цены OHLC, полученные объектом DataReader pandas_datareader. Как и в Trading View, его длину по оси X можно редактировать с помощью кнопок временного интервала. Индикаторы технического анализа перечислены в качестве элементов контрольного списка; когда кто-то выбирает предпочтительный, его соответствующие линии должны быть нанесены на визуальный элемент.

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

# 3. Application's Layout (Continuance)

# Insert the following portion of the code under the Dropdowns' dbc.Row.

# All the three components are placed inside this dbc.Row.
          dbc.Row([
            
            # Firstly, the candlestick chart is invoked. It is contained in a dcc.Loading
            # object, which presents a loading animation while the data is retrieved.
                    dcc.Loading(
                        [dcc.Graph(id='price-chart', figure=fig)], 
                        id='loading-price-chart', type='dot', color='#1F51FF'),
             
            # Next, this row will store the time span buttons as well 
            # as the indicators checklist
                dbc.Row([
                  
                  # The buttons occupy 1/3 of the available width.
                    dbc.Col([

                        # This Div contains the time span buttons for adjusting
                        # of the x-axis' length.
                        html.Div([
                            html.Button('1W', id='1W-button',
                                        n_clicks=0, className='btn-secondary'),
                            html.Button('1M', id='1M-button',
                                        n_clicks=0, className='btn-secondary'),
                            html.Button('3M', id='3M-button',
                                        n_clicks=0, className='btn-secondary'),
                            html.Button('6M', id='6M-button',
                                        n_clicks=0, className='btn-secondary'),
                            html.Button('1Y', id='1Y-button',
                                        n_clicks=0, className='btn-secondary'),
                            html.Button('3Y', id='3Y-button',
                                        n_clicks=0, className='btn-secondary'),

                        ], style={'padding': '15px', 'margin-left': '35px'})
                    ], width=4),

                    # The indicators have the remaining two thirds of the space.
                    dbc.Col([
                        dcc.Checklist(
                            ['Rolling Mean',
                             'Exponential Rolling Mean',
                             'Bollinger Bands'],
                            inputStyle={'margin-left': '15px',
                                        'margin-right': '5px'},
                            id='complements-checklist',
                            style={'margin-top': '20px'})
                    ], width=8)
                ]),
            ]),
        # The left major column's closing bracket.
        ], width=9),
      
      # The dashboard's major dbc.Row and html.Div closing brackets.
      ])
  ])
  
# 4. Interactivity (Continuance)
# Place this function below the dropdowns' interactivity method. It manages the 
# relationship among the three elements just created.
@ app.callback(
    Output('price-chart', 'figure'),
    Input('stocks-dropdown', 'value'),
    Input('complements-checklist', 'value'),
    Input('1W-button', 'n_clicks'),
    Input('1M-button', 'n_clicks'),
    Input('3M-button', 'n_clicks'),
    Input('6M-button', 'n_clicks'),
    Input('1Y-button', 'n_clicks'),
    Input('3Y-button', 'n_clicks'),
)
def change_price_chart(stock, checklist_values, button_1w, button_1m, button_3m, 
                              button_6m, button_1y, button_3y):
    # Retrieving the stock's data.
    df = web.DataReader(f'{stock}.SA', 'yahoo',
                        start='01-01-2015', end='31-12-2021')

    # Applying some indicators to its closing prices. Below we are measuring 
    # Bollinger Bands.
    df_bbands = bbands(df['Close'], length=20, std=2)

    # Measuring the Rolling Mean and Exponential Rolling means
    df['Rolling Mean'] = df['Close'].rolling(window=9).mean()
    df['Exponential Rolling Mean'] = df['Close'].ewm(
        span=9, adjust=False).mean()

    # Each metric will have its own color in the chart.
    colors = {'Rolling Mean': '#6fa8dc',
              'Exponential Rolling Mean': '#03396c', 'Bollinger Bands Low': 'darkorange',
              'Bollinger Bands AVG': 'brown',
              'Bollinger Bands High': 'darkorange'}

    fig = go.Figure()
    fig.add_trace(go.Candlestick(x=df.index, open=df['Open'], close=df['Close'],
                                 high=df['High'], low=df['Low'], name='Stock Price'))

    # If the user has selected any of the indicators in the checklist, we'll represent it in the chart.
    if checklist_values != None:
        for metric in checklist_values:

            # Adding the Bollinger Bands' typical three lines.
            if metric == 'Bollinger Bands':
                fig.add_trace(go.Scatter(
                    x=df.index, y=df_bbands.iloc[:, 0],
                    mode='lines', name=metric, 
                          line={'color': colors['Bollinger Bands Low'], 'width': 1}))

                fig.add_trace(go.Scatter(
                    x=df.index, y=df_bbands.iloc[:, 1],
                    mode='lines', name=metric, 
                          line={'color': colors['Bollinger Bands AVG'], 'width': 1}))

                fig.add_trace(go.Scatter(
                    x=df.index, y=df_bbands.iloc[:, 2],
                    mode='lines', name=metric, 
                          line={'color': colors['Bollinger Bands High'], 'width': 1}))

            # Plotting any of the other metrics remained, if they are chosen.
            else:
                fig.add_trace(go.Scatter(
                    x=df.index, y=df[metric], mode='lines', name=metric, 
                          line={'color': colors[metric], 'width': 1}))
                
    fig.update_layout(
        paper_bgcolor='black',
        font_color='grey',
        height=500,
        width=1000,
        margin=dict(l=10, r=10, b=5, t=5),
        autosize=False,
        showlegend=False
    )
    # Defining the chart's x-axis length according to the button clicked.
    # To do this, we'll alter the 'min_date' and 'max_date' global variables that were 
    # defined in the beginning of the script.
    global min_date, max_date
    changed_id = [p['prop_id'] for p in callback_context.triggered][0]
    if '1W-button' in changed_id:
        min_date = df.iloc[-1].name - datetime.timedelta(7)
        max_date = df.iloc[-1].name
    elif '1M-button' in changed_id:
        min_date = df.iloc[-1].name - datetime.timedelta(30)
        max_date = df.iloc[-1].name
    elif '3M-button' in changed_id:
        min_date = df.iloc[-1].name - datetime.timedelta(90)
        max_date = df.iloc[-1].name
    elif '6M-button' in changed_id:
        min_date = df.iloc[-1].name - datetime.timedelta(180)
        max_date = df.iloc[-1].name
    elif '1Y-button' in changed_id:
        min_date = df.iloc[-1].name - datetime.timedelta(365)
        max_date = df.iloc[-1].name
    elif '3Y-button' in changed_id:
        min_date = df.iloc[-1].name - datetime.timedelta(1095)
        max_date = df.iloc[-1].name
    else:
        min_date = min_date
        max_date = max_date
        fig.update_xaxes(range=[min_date, max_date])
        fig.update_yaxes(tickprefix='R$')
        return fig
      
    # Updating the x-axis range.
    fig.update_xaxes(range=[min_date, max_date])
    fig.update_yaxes(tickprefix='R$')
    return fig

Закончив эту часть кода, мы закончили левую сторону панели.

Правый раздел панели мониторинга

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

Таблица цен экономического сектора

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

# 3. Application's Layout (Continuance)

# Beginning the Dashboard's right section. It should be under the panel's left-side code.
# This other column occupies 25% of the dashboard's width.
        dbc.Col([

            dbc.Row([

                # The DataTable stores the prices from the companies that pertain 
                # to the same economic sector as the one chosen in the dropdowns.
                dcc.Loading([

                    dash_table.DataTable(
                        id='stocks-table', style_cell={'font_size': '12px',  
                                                       'textAlign': 'center'},
                        style_header={'backgroundColor': 'black',
                                      'padding-right': '62px', 'border': 'none'},
                        style_data={'height': '12px', 'backgroundColor': 'black', 
                                    'border': 'none'}, style_table={
                            'height': '90px', 'overflowY': 'auto'})
                ], id='loading-table', type='circle', color='#1F51FF')

            ], style={'margin-top': '28px'}),
        
        # The right major column closing bracket.
        ], width=3)
        
        # The dashboard's major dbc.Row and html.Div closing brackets.
    ])
])

# 4. Interactivity (Continuance)

# The following function goes after the candlestick interactivity method.
@ app.callback(
    Output('stocks-table', 'data'),
    Output('stocks-table', 'columns'),
    Input('sectors-dropdown', 'value')
)
# Updating the panel's DataTable
def update_stocks_table(sector):
    global sector_stocks
    # This DataFrame will be the base for the table.
    df = pd.DataFrame({'Stock': [stock for stock in sector_stocks[sector]], 
                       'Close': [np.nan for i in range(len(sector_stocks[sector]))]},
                      index=[stock for stock in sector_stocks[sector]])
    # Each one of the stock names and their respective prices are going to be stored 
    # in the 'df'  DataFrame.
    for stock in sector_stocks[sector]:
        stock_value = web.DataReader(
            f'{stock}.SA', 'yahoo', start='30-12-2021', end='31-12-2021')['Close'].iloc[-1]
        df.loc[stock, 'Close'] = f'R$ {stock_value :.2f}'
        
    # Finally, the DataFrame cell values are stored in a dictionary and its column
    # names in a list of dictionaries.
    return df.to_dict('records'), [{'name': i, 'id': i} for i in df.columns]

Update_stocks_table может показаться запутанным, но в основном он должен возвращать две вещи: информацию, которая заполняет ячейки таблицы в словаре и названия ее столбцов в списке словарей. Для получения дополнительной информации о том, как работает DataTable dash, взгляните на документацию библиотеки.

Карта текущей цены

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

# 3. Application's Layout (Continuance)

# Place this section beneath the DataTable code
# This Div will hold a card displaying the selected stock's current price.
            html.Div([

                dbc.Card([

                     # The card below presents the selected stock's current price.
                     dbc.CardBody([
                         # Recall that the name shown as default needs to be 'ABEV3', 
                         # since it is the panel's standard stock.
                         html.H1('ABEV3', id='stock-name',
                                 style={'font-size': '13px', 'text-align': 'center'}),
                         dbc.Row([
                             dbc.Col([
                                 # Placing the current price.
                                 html.P('R$ {:.2f}'.format(ambev['Close'].iloc[-1]), 
                                      id='stock-price', style={
                                     'font-size': '40px', 'margin-left': '5px'})
                             ], width=8),
                             dbc.Col([

                                 # This another paragraph shows the price variation.
                                 html.P(
                                    '{}{:.2%}'.format(
                                     '+' if ambev_variation > 0 else '-', ambev_variation),
                                     id='stock-variation', 
                                     style={'font-size': '14px', 
                                      'margin-top': '25px', 
                                      'color': 'green' if ambev_variation > 0 else 'red'})
                             ], width=4)

                         ])

                     ])
                     ], id='stock-data', style={'height': '105px'}, color='black'),
            ])
      # The right major column closing bracket.
      ], width=3)
      
    # The dashboard's major dbc.Row and html.Div closing brackets.
    ])
    ])
    
# 4. Interactivity (Continuance)
# The following function will edit the values being displayed in the price card
# and it should follow the method that handles the DataTable.
@app.callback(
    Output('stock-name', 'children'),
    Output('stock-price', 'children'),
    Output('stock-variation', 'children'),
    Output('stock-variation', 'style'),
    Input('stocks-dropdown', 'value')
)
def update_stock_data_card(stock):

    # Retrieving data from the Yahoo Finance API.
    df = web.DataReader(f'{stock}.SA', 'yahoo',
                        start='2021-12-29', end='2021-12-31')['Close']
    # Getting the chosen stock's current price and variation in comparison to 
    # its previous value.
    stock_current_price = df.iloc[-1]
    stock_variation = 1 - (df.iloc[-1] / df.iloc[-2])

    # Note that as in the Carousel, the varitation value will be painted in red or 
    # green depending if it is a negative or positive number.
    return stock, 'R$ {:.2f}'.format(stock_current_price), 
      '{}{:.2%}'.format('+' if stock_variation > 0 else'-', stock_variation), \
        {'font-size': '14px', 'margin-top': '25px',
            'color': 'green' if stock_variation > 0 else 'red'}

52-недельная карусель данных

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

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

# 3. Application's Layout (Continuance)

# This part of the script goes right under the price card construct.
# In the section below, some informations about the stock's performance in the last 
# 52 weeks are going to be exposed.
                html.Hr(),
                html.H1(
                    '52-Week Data', style={'font-size': '25px', 'text-align': 'center', 
                                           'color': 'grey', 'margin-top': '5px',
                                           'margin-bottom': '0px'}),
                # Creating a Carousel showing the stock's weekly average price and a 
                # Speedoemeter displaying how far its current price is from 
                # the minimum and maximum values achieved.
                dtc.Carousel([
                    
                  # By standard, the charts 'fig2' and 'fig3' made in the beginning
                  # of the script are displayed.
                    dcc.Graph(id='52-avg-week-price', figure=fig2),
                    dcc.Graph(id='52-week-min-max', figure=fig3)
                ], slides_to_show=1, autoplay=True, speed=2000, 
                  style={'height': '220px', 'width': '310px', 'margin-bottom': '0px'}),
          
          # The right major column closing bracket.
           ], width=3)
            
    # The dashboard's major dbc.Row and html.Div closing brackets.
    ])

])

# 4. Interactivity (Continuance)
# The function here is placed under the one dealing with the price card and
# is responsible for updating the weekly average price chart.

@app.callback(
    Output('52-avg-week-price', 'figure'),
    Input('stocks-dropdown', 'value')
)
def update_average_weekly_price(stock):
    # Receiving the stock's prices and measuring its average weekly price 
    # in the last 52 weeks.
    df = web.DataReader(f'{stock}.SA', 'yahoo',
                        start='2021-01-01', end='2021-12-31')['Close']
    df_avg_52 = df.resample('W').mean().iloc[-52:]

    # Plotting the data in a line chart.
    fig2 = go.Figure()
    fig2.add_trace(go.Scatter(x=df_avg_52.index, y=df_avg_52.values))
    fig2.update_layout(
        title={'text': 'Weekly Average Price', 'y': 0.9},
        font={'size': 8},
        plot_bgcolor='black',
        paper_bgcolor='black',
        font_color='grey',
        height=220,
        width=310,
        margin=dict(l=10, r=10, b=5, t=5),
        autosize=False,
        showlegend=False
    )
    fig2.update_xaxes(tickformat='%m-%y', showticklabels=False,
                      gridcolor='darkgrey', showgrid=False)
    fig2.update_yaxes(range=[df_avg_52.min()-1, df_avg_52.max()+1.5],
                      showticklabels=False, gridcolor='darkgrey', showgrid=False)

    return fig2

# This function will update the speedometer chart.
@app.callback(
    Output('52-week-min-max', 'figure'),
    Input('stocks-dropdown', 'value')
)
def update_min_max(stock):
    # The same logic as 'update_average_weekly_price', but instead we are getting
    # the minimum and maximum prices reached in the last 52 weeks and comparing 
    # them with the stock's current price.
    df = web.DataReader(f'{stock}.SA', 'yahoo',
                        start='2021-01-01', end='2021-12-31')['Close']
    df_avg_52 = df.resample('W').mean().iloc[-52:]
    df_52_weeks_min = df_avg_52.resample('W').min()[-52:].min()
    df_52_weeks_max = df_avg_52.resample('W').max()[-52:].max()
    current_price = df.iloc[-1]
    fig3 = go.Figure()
    fig3.add_trace(go.Indicator(mode='gauge+number', value=current_price,
                                domain={'x': [0, 1], 'y': [0, 1]},
                                gauge={
                                    'axis': {'range': [df_52_weeks_min, df_52_weeks_max]},
                                    'bar': {'color': '#606bf3'}}))
    fig3.update_layout(
        title={'text': 'Min-Max Prices', 'y': 0.9},
        font={'size': 8},
        paper_bgcolor='black',
        font_color='grey',
        height=220,
        width=280,
        margin=dict(l=35, r=0, b=5, t=5),
        autosize=False,
        showlegend=False
    )

    return fig3

Мы собираемся закончить структуру приборной панели. Все, что нам нужно вставить, это корреляции акций с индексом BOVESPA (IBOVESPA) и его средней ценой в экономической области.

Корреляции акций

Как уже отмечалось, эти последние цифры направлены на то, чтобы дать некоторое представление о производительности предприятия по сравнению с IBOVESPA и его коллегами из экономического сектора. Статистика, которая будет использоваться, — это r Пирсона.

# 3. Application's Layout (Ending!)

# This row is beneath the carousel just made and places the stock's correlations with 
# the BOVESPA index (IBOVESPA) and its sector's daily average price.
                dbc.Row([
                    # A small title for the section.
                    html.H2('Correlations', style={
                        'font-size': '12px', 'color': 'grey', 'text-align': 'center'}),
                    dbc.Col([

                        # IBOVESPA correlation.
                        html.Div([
                            html.H1('IBOVESPA',
                                    style={'font-size': '10px'}),
                            html.P(id='ibovespa-correlation',
                                   style={'font-size': '20px', 'margin-top': '5px'})
                        ], style={'text-align': 'center'})


                    ], width=6),

                    dbc.Col([

                        # Sector correlation.
                        html.Div([
                            html.H1('Sector',
                                    style={'font-size': '10px'}),
                            html.P(id='sector-correlation',
                                   style={'font-size': '20px', 'margin-top': '5px'})
                        ], style={'text-align': 'center'})

                    ], width=6)
                ], style={'margin-top': '2px'})
            ], style={'backgroundColor': 'black', 'margin-top': '20px', 'padding': '5px'})

        # The right major column closing bracket.
           ], width=3)
            
    # The dashboard's major dbc.Row and html.Div closing brackets.
    ])

])

# 4. Interactivity (Ending!)
# This function will measure the correlation coefficient between the stock's closing 
# values and the BOVESPA index.
@ app.callback(
    Output('ibovespa-correlation', 'children'),
    Input('stocks-dropdown', 'value')
)
def ibovespa_correlation(stock):
    start = datetime.datetime(2021, 12, 31).date() - \
        datetime.timedelta(days=7 * 52)
    end = datetime.datetime(2021, 12, 31).date()

    # Retrieving the IBOVESPA values in the last 52 weeks.
    ibovespa = web.DataReader('^BVSP', 'yahoo', start=start, end=end)['Close']

    # Now, doing the same with the chosen stock's prices.
    stock_close = web.DataReader(
        f'{stock}.SA', 'yahoo', start=start, end=end)['Close']

    # Returning the correlation coefficient.
    return f'{pearsonr(ibovespa, stock_close)[0] :.2%}'

# Now, this other function will measure the same stat, but now between the stock's value
# and the daily average closing price from its respective sector in the last 52 weeks.
@ app.callback(
    Output('sector-correlation', 'children'),
    Input('sectors-dropdown', 'value'),
    Input('stocks-dropdown', 'value')
)
def sector_correlation(sector, stock):
    start = datetime.datetime(2021, 12, 31).date() - \
        datetime.timedelta(days=7 * 52)
    end = datetime.datetime(2021, 12, 31).date()

    # Retrieving the daily closing prices from the selected stocks in the prior 52 weeks.
    stock_close = web.DataReader(
        f'{stock}.SA', 'yahoo', start=start, end=end)['Close']

    # Creating a DataFrame that will store the prices in the past 52 weeks 
    # from all the stocks that pertain to the economic domain selected.
    sector_df = pd.DataFrame()

    # Retrieving the price for each of the stocks included in 'sector_stocks'
    global sector_stocks
    stocks_from_sector = [stock_ for stock_ in sector_stocks[sector]]
    for stock_ in stocks_from_sector:
        sector_df[stock_] = web.DataReader(
            f'{stock_}.SA', 'yahoo', start=start, end=end)['Close']

    # With all the prices obtained, let's measure the sector's daily average value.
    sector_daily_average = sector_df.mean(axis=1)

    # Now, returning the correlation coefficient.
    return f'{pearsonr(sector_daily_average, stock_close)[0] :.2%}'


# Finally, running the Dash app.
if __name__ == '__main__':
    app.run_server(debug=True)

И все! Написание этой последней части скрипта гарантирует, что у вас будет полная панель анализа данных акций!

Источник

Источник

Источник

Источник

Источник