Python & LLM для анализа рынка

Часть I. Технические индикаторы

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

В этом посте мы рассмотрим, как рассчитать основные технические индикаторы с помощью Python и Pandas, используя библиотеки TA (Technical Analysis). Несмотря на то, что существует множество индикаторов на выбор, я сосредоточусь на нескольких из моих личных фаворитов:

  1. RSI
  2. Экспоненциальная скользящая средняя (EMA)
  3. Каналы Кельтнера

Для начала вам понадобятся исторические данные. Хотя я предпочитаю использовать API Zerodha Kite для торговли в реальном времени и исторических данных, за него взимается абонентская плата. Кроме того, вы можете получить доступ к бесплатным данным через функцию GOOGLEFINANCE в Google Таблицах или использовать библиотеку jugaad_data на Python.

Вот основные данные, которые вам потребуются:

  • DATE
  • Open
  • High
  • Low
  • Close

В этом руководстве мы продемонстрируем использование библиотеки jugaad_data. Обратите внимание, что, несмотря на то, что эта библиотека предоставляет бесплатные данные, она может не поддерживаться активно или не быть полностью надежной. Для более надежных источников данных рассмотрите Google Finance или Zerodha Kite API.

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

  1. Установка библиотеки

pip install jugaad-data

2. Импорт необходимых библиотекfrom datetime import datetime, timedelta
import pandas as pd
from jugaad_data.nse import stock_df

3. Получение данных в Pandas DataFrame@staticmethod
def extract_jugaad_data(tick,start,end):
df1 = stock_df(symbol=tick, from_date=start,
to_date=end, series=»EQ»)
return df1

Описанный выше метод требует, чтобы мы отправили название акции, а также даты начала и окончания. Этот метод всегда будет возвращать исторические данные за день. для большего количества вариантов, таких как 5 м, 10 м, 1 ч, 1 Вт и т. Д., Используйте API воздушного змея Zerodha.

4. Допустим, мы хотим извлечь исторические данные за 5 лет. Мы должны итеративно извлекать данные за каждый год. Запуск большого диапазона дат может привести к проблемам с производительностью и задержкам, а также к сбоям. Следовательно, нам нужен этот итеративный подход.class HistoricalData:
@staticmethod
def calculate_timelines(years):
curr = datetime.now().date()
ans = []
while years>0:
start = curr- timedelta(days=365)
ans.insert(0,(start,curr))
curr=start
years-=1
return ans

Приведенный выше метод вернет список дат начала и окончания.

5. Теперь, чтобы получить исторические данные, давайте сделаем это ниже def get_history_df(tick):
df = pd.DataFrame({})
for start,end in HistoricalData.calculate_timelines(5):
df = pd.concat([df,HistoricalData.extract_jugaad_data(tick,start,end)],ignore_index=True)
df = df[[«DATE», «OPEN», «HIGH»,»LOW»,»CLOSE»,»VOLUME»,»SYMBOL»]]
df[«DATE»] = pd.to_datetime(df[«DATE»], format=’%Y-%m-%d’)
df.set_index(‘DATE’,inplace=True)
df = df[~df.index.duplicated(keep=’first’)]
df = df.sort_values(by=’DATE’)

Здесь мы перебираем исторические данные, начальную и конечную даты и связываем данные с фреймом данных. затем используйте выбранные столбцы и преобразуйте столбец DATE в поле даты и времени pandas. Выполните очистку данных, удалив дубликаты и отсортировав данные по полю ДАТА. Убедитесь, что индекс установлен как дата, так как в каждой дате должна быть только одна строка.

6. Давайте коснемся самой важной части этого поста, расчета технических индикаторов. Здесь мы используем 2 библиотекиpip install ta
pip install TA-Lib

7. Обе библиотеки поддерживают широкий спектр технических индикаторов. Давайте попробуем некоторые из них. Давайте создадим файл tech_analysis.pyimport pandas as pd
import numpy as np
from talib import RSI, EMA, stream
from ta.volatility import KeltnerChannel
class TechAnalysis:
def __init__(self):
self.ema_period = 13
self.rsi_period = 7
self.stochastic_period = 7
self.rsi_divergence_period = 14

def calculate_technicals(self, df):
try:
df[‘EMA’] = EMA(df[‘CLOSE’],timeperiod=self.ema_period)
df[‘RSI7’] = RSI(df[‘EMA’], timeperiod=self.rsi_period)
df[‘RSI14’] = RSI(df[‘CLOSE’], timeperiod=self.rsi_divergence_period)
indicator_kc = KeltnerChannel(high=df[«HIGH»],low=df[«LOW»],close=df[«CLOSE»], window=50, window_atr=50,fillna=False,multiplier=5, original_version=False)
df[«KC_UPPER»] = indicator_kc.keltner_channel_hband()
df[«KC_MIDDLE»] = indicator_kc.keltner_channel_mband()
df[«KC_LOWER»] = indicator_kc.keltner_channel_lband()
return df
except Exception as e:
print(‘error while calculating technicals’)
print(f’exception occured {e}’)

TA = TechAnalysis()

Обратите внимание, что мы используем EMA для RSI7, но CLOSE для RSI14. Существуют различные способы расчета индикаторов RSI, и каждый из них имеет свои плюсы и минусы. EMA(),RSI() являются частью talib, а KeltnerChannel() — частью ta.volatility.

Канал Кельтнера имеет 3 полосы, верхнюю, среднюю и нижнюю. Чтобы его вычислить, инициируйте канал Кельтнераindicator_kc = KeltnerChannel(high=df[«HIGH»],low=df[«LOW»],
close=df[«CLOSE»], window=50, window_atr=50,fillna=False,multiplier=5,
original_version=False)

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

Затем используйте indicator_kc для вычисления полос. Например, для вычисления верхней полосы indicator_kc.keltner_channel_hband(). Я рекомендую выставить другие методы этих 2 библиотек.

Вот ссылка на TA-Lib и ссылка на .

Возвращает кадр данных, чтобы столбцы были добавлены во фрейм данных.

8. Теперь обновим наш get_history_df (тик) из нашего шага 5 для расчета технических индикаторов. Добавьте импорт вверху и сохраните данные в CSV.from tech_analysis import TAdef get_history_df(tick):
df = pd.DataFrame({})
for start,end in HistoricalData.calculate_timelines(5):
df = pd.concat([df,HistoricalData.extract_jugaad_data(tick,start,end)],ignore_index=True)
df = df[[«DATE», «OPEN», «HIGH»,»LOW»,»CLOSE»,»VOLUME»,»SYMBOL»]]
df[«DATE»] = pd.to_datetime(df[«DATE»], format=’%Y-%m-%d’)
df.set_index(‘DATE’,inplace=True)
df = df[~df.index.duplicated(keep=’first’)]
df = df.sort_values(by=’DATE’)
df = TA.calculate_technicals(df)
df.to_csv(f’./{tick}.csv’)

Приведенный выше код теперь будет хранить CSV-файл в той же папке, где находится код.

9. В целом, то, что мы видели до сих пор, можно свести воедино, как показано ниже. Запустите technical_data.py и обратите внимание, что csv-файл сгенерирован со значениями технических индикаторов

technicals_data.pyfrom datetime import datetime, timedelta
import pandas as pd
from jugaad_data.nse import stock_df
from tech_analysis import TA
class HistoricalData:
@staticmethod
def calculate_timelines(years):
curr = datetime.now().date()
ans = []
while years>0:
start = curr- timedelta(days=365)
ans.insert(0,(start,curr))
curr=start
years-=1
return ans

@staticmethod
def extract_jugaad_data(tick,start,end):
df1 = stock_df(symbol=tick, from_date=start,
to_date=end, series=»EQ»)
return df1

@staticmethod
def get_history_df(tick):
df = pd.DataFrame({})
for start,end in HistoricalData.calculate_timelines(5):
df = pd.concat([df,HistoricalData.extract_jugaad_data(tick,start,end)],ignore_index=True)
df = df[[«DATE», «OPEN», «HIGH»,»LOW»,»CLOSE»,»VOLUME»,»SYMBOL»]]
df[«DATE»] = pd.to_datetime(df[«DATE»], format=’%Y-%m-%d’)
df.set_index(‘DATE’,inplace=True)
df = df[~df.index.duplicated(keep=’first’)]
df = df.sort_values(by=’DATE’)
df = TA.calculate_technicals(df)
df.to_csv(f’./{tick}.csv’)

HistoricalData.get_history_df(«INFY»)

tech_analysis.pyimport pandas as pd
import numpy as np
from talib import RSI, EMA, stream
from ta.volatility import KeltnerChannel
class TechAnalysis:
def __init__(self):
self.ema_period = 13
self.rsi_period = 7
self.stochastic_period = 7
self.rsi_divergence_period = 14

def calculate_technicals(self, df):
try:
df[‘EMA’] = EMA(df[‘CLOSE’],timeperiod=self.ema_period)
df[‘RSI’] = RSI(df[‘EMA’], timeperiod=self.rsi_period)
df[‘RSI14’] = RSI(df[‘CLOSE’], timeperiod=self.rsi_divergence_period)
indicator_kc = KeltnerChannel(high=df[«HIGH»],low=df[«LOW»],close=df[«CLOSE»], window=50, window_atr=50,fillna=False,multiplier=5, original_version=False)
df[«KC_UPPER»] = indicator_kc.keltner_channel_hband()
df[«KC_MIDDLE»] = indicator_kc.keltner_channel_mband()
df[«KC_LOWER»] = indicator_kc.keltner_channel_lband()
return df
except Exception as e:
print(‘error while calculating technicals’)
print(f’exception occured {e}’)

TA = TechAnalysis()

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

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

Часть II. Стратегии тестирования на истории

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

В прошлой статье мы рассмотрели основы получения исторических данных и добавления технических индикаторов, таких как EMA, RSI и стратегия Кельтнера. В этом посте мы углубимся в тестирование нескольких стратегий, основанных на этих индикаторах.

Почему важно тестировать на истории перед применением стратегии вживую?

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

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

  1. Стратегия 1: покупаем на уровне RSI 20, продаем на уровне RSI 80
  2. Стратегия 2: Покупайте, когда текущая цена равна или ниже минимума Кельтнера, продавайте, когда текущая цена равна или выше максимума Кельтнера.
  3. Стратегия 3:
  • Золотой крест, когда 50 DMA пересекает 200 DMA снизу вверх, подтверждая бычий сигнал, Совершите покупку
  • Death Cross, когда 50 DMA пересекает DMA сверху, подтверждая медвежий сигнал, совершаем продажу

Заметка:

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

Стратегия 1 — Уровни RSI

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

  • Чтение CSV-файла и его сохранение в Pandas DataFrame
  • Итерация по строкам DataFrame
  • Выполнение фиктивной покупки (когда последуют стратегии покупки и продажи)
  • Выполнение фиктивных продаж и отслеживание прибылей и убытков
  • Создание комплексного отчета по всем покупкам и продажам и хранение его в CSV

Запустим класс Strategy и метод buy для выполнения простой фиктивной покупки в прошлом. (Когда покупать, а когда продавать, чуть позже).

strategy.pyclass Strategy:
def __init__(self):
self.default_quantity = 100
self.buy_limit = 100000

def buy_stock(self,row,current_state,order_df,default_quantity,tick):
if current_state[‘quantity’]==0:
current_state[‘start_date’] = row[‘TIMESTAMP’]
row_df = pd.DataFrame({‘tick’:[tick],’TIMESTAMP’: [row[‘TIMESTAMP’]],’action’:[‘BUY’],’price’:[row[‘CLOSE’]],’quantity’:[default_quantity],’pnl’:[0],’percent’:[0],
‘days_held’:[0]})
order_df = pd.concat([order_df,row_df],ignore_index=True)
new_q = current_state[‘quantity’] + default_quantity
current_state[‘avg_price’] = ((current_state[‘quantity’]*current_state[‘avg_price’]) + (default_quantity*row[‘CLOSE’]))/new_q
current_state[‘quantity’] = new_q
current_investment = default_quantity*row[‘CLOSE’]
return order_df,current_state

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

Теперь давайте углубимся в процесс продажи и проведем сравнение с действиями на покупку.def sell_stock(self,row,current_state,order_df,tick,total_pnl):
current_pnl = (current_state[‘quantity’]*(row[‘CLOSE’]-current_state[‘avg_price’]))
percentage = (current_pnl/(current_state[‘quantity’] * current_state[‘avg_price’]))*100
days_held = (row[‘TIMESTAMP’] — current_state[‘start_date’]).days
row_df = pd.DataFrame({‘tick’:[tick],’TIMESTAMP’: [row[‘TIMESTAMP’]],’action’:[‘SELL’],’price’:[row[‘CLOSE’]],’quantity’:[current_state[‘quantity’]],’pnl’:[current_pnl], ‘percent’:[percentage],
‘days_held’:[days_held]})
order_df = pd.concat([order_df,row_df],ignore_index=True)
new_fund = (current_state[‘quantity’]*row[‘CLOSE’])
percentage = (current_pnl/(current_state[‘quantity’] * current_state[‘avg_price’]))*100
total_pnl+=current_pnl
current_state = {‘quantity’:0, ‘avg_price’:0}
return order_df, current_state,total_pnl

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

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

Далее мы рассмотрим конкретные критерии для принятия решения о том, когда покупать, а когда продавать, с отдельными классами, посвященными каждой стратегии.class RSIStrategy(Strategy):
def __init__(self):
self.default_quantity = 100
self.buy_limit = 100000

def execute(self, row, order_df, current_state):
tick = row[‘SYMBOL’]
total_pnl = 0
already_bought = current_state[‘quantity’] * current_state[‘avg_price’]
if row[‘RSI14’]>=80:
if current_state[‘quantity’]>0 and row[‘CLOSE’] > current_state[‘avg_price’]:
order_df, current_state,total_pnl = super().sell_stock(row,current_state,order_df,tick,total_pnl)

elif row[‘RSI14’]<=20:
order_df,current_state = super().buy_stock(row,current_state,order_df,self.default_quantity,tick)
return order_df,current_state

Класс RSIStrategy является подклассом Strategy. Это наследование позволяет нам повторно использовать методы ‘buy’ и ‘sell’ из родительского класса во всех подклассах, которые мы создаем.

Обратите внимание, что значения ’20’ и ’80’ в настоящее время жестко запрограммированы в стратегии. В следующей статье мы параметризуем эти значения, что позволит нам регулировать их динамически, возможно, с помощью пользовательского интерфейса. Наш следующий шаг — внедрить фреймворк для тестирования на истории в пользовательский интерфейс Streamlit, что позволит нам эффективно визуализировать результаты.

Кроме того, поскольку мы планируем переключаться между несколькими торговыми стратегиями, мы создадим фабричный класс. Этот класс будет возвращать экземпляры на основе определенных параметров, таких как имя стратегии.class StrategyFactory:
@staticmethod
def create_strategy(strategy):
if strategy==»Keltner»:
return KeltnerStrategy()
elif strategy==»MA»:
return MovingAverageStrategy()
elif strategy==»RSI»:
return RSIStrategy()
else:
raise ValueError(«invalid.»)

Примечание: сейчас мы разработали только RSIStrategy(). Остальные 2 стратегии мы рассмотрим ближе к концу. Теперь, когда мы завершили нашу важную часть, следующим шагом станет то, как мы будем использовать Стратегию.

Расход стратегии

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

backtest.pyimport pandas as pd
import os
from csvwriter import write_to_excel
from strategy import StrategyFactory
class BackTest:
def __init__(self):
self.folder = ‘./historical/analysis/’
self.files = os.listdir(self.folder)
self.sheets = [«day»]
self.indicator = «RSI»
self.strategy = StrategyFactory.create_strategy(self.indicator)
self.output_file_path = f’./back_test/{self.indicator}_back_tested.csv’

В нашем фреймворке self.folder служит местом хранения исторических данных в формате CSV. Мы используем стратегию RSI, и результирующий CSV-файл, содержащий запись о действиях на покупку и продажу, хранится в self.output_file_path.

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

  1. Начнем с чтения CSV-файла с историческими данными.
  2. Инициализируем current_state.
  3. Затем мы перебираем строки и вызываем self.strategy.execute, где в игру вступает фактический метод execute каждого объекта Strategy.
  4. После выполнения этих действий мы рассчитываем нереализованную прибыль и убыток (P&L). Стоит отметить, что бывают ситуации, когда происходит покупка, но соответствующей продажи не следует. В таких случаях мы рассчитываем и отслеживаем активы.

def back_test_a_stock(self, filename):
tick = filename
unrealized=0
unrealized_percent = 0
df = pd.read_excel(self.folder+filename, sheet_name=self.sheet)

df[‘TIMESTAMP’] = pd.to_datetime(df[‘TIMESTAMP’])
current_state = {‘quantity’:0, ‘avg_price’:0, ‘start_date’:None}

order_df = pd.DataFrame()
for index, row in df.iloc[21:].iterrows():
order_df,current_state = self.strategy.execute(row,order_df,current_state)

if current_state[‘quantity’]>0:
q = current_state[‘quantity’]
a = current_state[‘avg_price’]
last_price = df.iloc[-1][«CLOSE»]
unrealized = (last_price-a)*q
unrealized_percent = (last_price-a)/a*100
return order_df,unrealized, unrealized_percent

Мы успешно реализовали процесс тестирования на истории для одного (стокового) CSV-файла. Однако реальные сценарии часто включают в себя несколько акций, каждая из которых имеет свои исторические данные. Чтобы решить эту проблему, мы разработаем еще один метод в том же классе. def strategy_test(self):
#part1: Iterate through all the files
back_tested_result = pd.DataFrame()
unrealized_total=0
unrealized_percent = []
for filename in self.files:
if filename.endswith(‘.xlsx’) and not filename.startswith(‘~$’):
result,unrealized,u_percent = self.back_test_a_stock(filename)
if u_percent!=0:
unrealized_percent.append(u_percent)

unrealized_total+=unrealized
back_tested_result = pd.concat([back_tested_result, result], ignore_index=True)
#part 2: calculate profit percentage and total days held
back_tested_result.to_csv(self.output_file_path, index=False)
avg_profit_percentage = back_tested_result[back_tested_result[‘percent’] != 0][‘percent’].mean()
avg_days_held = back_tested_result[back_tested_result[‘days_held’] != 0][‘days_held’].mean()
realized_total = back_tested_result[‘pnl’].sum()
#part3: print the results
print(f’Average realized pnl % is {avg_profit_percentage}%’)
print(f’Average holding days is {avg_days_held}’)
if len(unrealized_percent) > 0:
avg_profit_percentage_u = sum(unrealized_percent)/len(unrealized_percent)
print(f’Total unrealized % is {avg_profit_percentage_u}%’)

Хотя код может показаться обширным и раздражающим (так оно и было, когда я читал!), по сути он состоит из трех фундаментальных операций:

  1. Инициализация ключевых параметров.
  2. Итерация по каждому файлу исторических данных с применением метода тестирования на истории.
  3. Расчет ключевых метрик для оценки эффективности стратегии, в том числе:
  • Средняя реализованная прибыль и убыток (P&L).
  • Общая нереализованная прибыль и убытки (еще не реализованы).
  • Количество дней удержания.

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

Наконец, инициализируем наш класс BackTestbt = BackTest()
bt.strategy_test()

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

На сегодняшний день мы разработали

  • backtest.py
  • strategy.py
  • tech_analysis.py (предыдущая запись)
  • tech_data.py(предыдущая запись)

Убедитесь, что вы создали 2 папки: /historical/analyis (здесь хранятся все исторические данные CSV) и /back_test (где хранится каждый экземпляр результата тестирования). Это полная настройка, необходимая для полного запуска одной стратегии. Давайте создадим еще 2 стратегии.

Стратегия 2 — Каналы Кельтнера

Отличное введение об этой стратегии здесь.class KeltnerStrategy(Strategy):
def __init__(self):
self.default_quantity = 100
self.buy_limit = 100000

def execute(self, row, order_df, current_state):
tick = row[‘SYMBOL’]
total_pnl = 0
already_bought = current_state[‘quantity’] * current_state[‘avg_price’]
buy_support_level = current_state[‘avg_price’]-(current_state[‘avg_price’]/100)
good_buy_range = (row[‘KC_LOWER’]+(row[‘KC_LOWER’]/400))

if row[‘CLOSE’]>=row[‘KC_UPPER’]:
if current_state[‘quantity’]>0 and row[‘CLOSE’] > current_state[‘avg_price’]:
order_df, current_state,total_pnl = super().sell_stock(row,current_state,order_df,tick,total_pnl)


elif row[‘CLOSE’]<=row[‘KC_LOWER’]:
eq = 2*self.default_quantity
order_df,current_state = super().buy_stock(row,current_state,order_df,eq,tick)

elif row[‘CLOSE’]<=buy_support_level:
order_df,current_state = super().buy_stock(row,current_state,order_df,self.default_quantity,tick)

elif (current_state[‘quantity’]==0 and row[‘CLOSE’]<=good_buy_range):
order_df,current_state = super().buy_stock(row,current_state,order_df,self.default_quantity//2,tick)
else:
pass
return order_df,current_state

Здесь мы добавляем немного больше сложности в нашу стратегию.

  • Покупайте, когда цена ближе к KC Lower
  • Покупайте 2X, когда цена ниже KC Lower
  • Средняя, если цена ниже 1% от нашего среднего уровня владения
  • Продавать, когда цена выше KC Upper

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

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

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

Часть III . Позвольте вашей торговой системе реагировать на ежедневные новости

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

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

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

  1. Извлечение списка последних новостных статей из API агрегатора новостей.
  2. Свяжитесь с каждым новостным источником и соберите полные статьи.
  3. Резюмируйте статью с помощью LLM.
  4. Анализ настроений с использованием моделей Finance LLM (Bloomberg, PaLM Finance model)
  5. Объедините их с Техническими индикаторами и постройте рекомендательную систему.

Обо всем по порядку!

Являются ли языковые модели (LLM) настоящими финансовыми экспертами, если им предоставлены правильные данные? Продолжающиеся исследования в этой области говорят о том, что их пока нет. Несмотря на то, что есть несколько многообещающих моделей, их надежность не является абсолютной. Заслуживающая внимания исследовательская статья, проливающая свет на этот вопрос, доступна на сайте https://arxiv.org/pdf/2310.12664.pdf.

Теперь возникает вопрос: почему мы должны рассмотреть возможность использования LLM для финансов? На мой взгляд, потенциал кроется в будущем. Со временем мы ожидаем разработки очень сложных моделей, которые могут предложить экспертную информацию в области финансов. В рамках этой серии статей мы подробнее рассмотрим эту концепцию. В заключение мы приступим к тонкой настройке нашей собственной модели Finance LLM, используя набор данных, состоящий из 50 000 статей о бизнесе. Этот практический подход продемонстрирует, как тонко настроенная модель может превзойти универсальные модели.

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

Так что же мы строим?

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

API новостного агрегатора

Во-первых, нам нужен способ собрать все ежедневные новости в одном месте. Вот тут-то и пригодится newsapi.org. Нас особенно интересуют их деловые новости. К счастью, newsapi имеет как REST API, так и библиотеку python, которую мы можем установить и использовать. Давайте установим эту библиотеку с помощьюpip3 install newsapi-python

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

Давайте создадим простой python-класс, который извлекает новостные статьи.#news_api.py
class NewsApi:
def __init__(self):
self.token = os.getenv(«NEWS_API_TOKEN»)
self.country = ‘in’
self.category=’business’
self.csv_location = ‘./news/’
self.filename = f’news_{date.today()}.csv’
self.newsapi = NewsApiClient(api_key=self.token) def extract_news(self):
page = 1
total_news_count = 0
all_articles = []
try:
while True:
top_headlines = self.newsapi.get_top_headlines(
country=self.country,
language=’en’,
category=self.category,
page=page
)
if top_headlines.get(‘status’)==’ok’:
for row in top_headlines.get(«articles»,None):
article = [row[‘publishedAt’],row[‘title’],row[‘description’],row[‘url’]]
all_articles.append(article)
page+=1

if len(all_articles)>=top_headlines[‘totalResults’]:
break
self.all_articles = pd.DataFrame(all_articles,columns=[‘Date’,’Title’,’Description’,’URL’])
self.all_articles.to_csv(f'{self.csv_location}{self.filename}’)
except Exception as e:
print(e)

Короче говоря, мы начинаем со страницы 1, извлекаем все новости на странице и переходим на следующую страницу. Мы собираем новости и храним их в all_articles переменной list. Наконец, мы сохраняем его в csv-файле. В то время как результаты API имеют несколько других столбцов, нас интересуют только Дата, Заголовок, Описание и URL.

Извлечение полных новостных статей

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

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

Можно ли копировать новостные статьи с сайта?

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

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

  1. https://dorianlazar.medium.com/scraping-medium-with-python-beautiful-soup-3314f898bbf5
  2. https://realpython.com/beautiful-soup-web-scraper-python/
  3. https://medium.com/@poojan.s/stock-market-news-scraper-using-beautifulsoup-bc7db5c75f99

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

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

Суммирование и анализ тональности с LLM

В то время как я лично использую PaLM2 для обобщения и BloombergGPT от Huggingface Hub, который я настроил с помощью приличного набора данных бизнес-новостей для получения новостных настроений, в этой статье я буду использовать PaLM2 от Google для обеих целей. В связи с недавними разговорами о Gemini, самом известном мультимодальном самолете, созданном Google, PaLM2 определенно стоит попробовать!

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

Давайте начнем с создания простого интерфейса PaLM2, который принимает пользовательский запрос и извлекает результаты из LLM через Rest API. Далее мы создадим простую подсказку, которая поможет нам извлечь данные нужным нам способом.

  • Для начала нам понадобится ключ API. Перейдите по этой ссылке и получите его. https://developers.generativeai.google/products/palm
  • Убедитесь, что вы сохранили ключ API в файле .env как PALM2_API_TOKEN

Установить клиент palm2 довольно простоpip3 install google-generativeai#palm_interface.py
class PalmInterface:
def prompt(self,query):
try:
payload = self.build_input(query)
defaults = { ‘model’: ‘models/text-bison-001’ }
response = palm.generate_text(**defaults, prompt=payload)
json_response = json.loads(response.result)
return json_response
except Exception as e:
print(e)
return None

Пришло время для быстрого проектирования…def summarize(self,full_news):
template3 = f»You excel in succinctly summarizing business and finance-related news articles.\
Upon receiving a news article, your objective is to craft a concise and accurate summary while \
retaining the name of the company mentioned in the original article. The essence of the article \
should be preserved in your summary. A job well done in summarizing may earn you a generous tip.\
Please proceed with the provided full news article. {full_news}»
try:
defaults = { ‘model’: ‘models/text-bison-001’ }
response = palm.generate_text(**defaults, prompt=template3)
return response.result
except Exception as e:
print(e)
return None

Оперативное проектирование… Если этот термин звучит для вас впервые, думайте о Побуждении как о Йоде проявления. Чем яснее ваши просьбы к Вселенной в сочетании с непоколебимой верой, тем больше вероятность того, что ваши желания окажутся у вас на коленях. 🙂 Но дело не только в этом — давайте сделаем передышку, переварим эту пижаму, а затем быстро забудем о ней.

На этот раз несколько выстрелов…def build_input(self,article):
article_1 = «Tech giant Apple Inc. reports record-breaking quarterly earnings, surpassing market expectations and driving stock prices to new highs. Investors express optimism for the company’s future prospects.»
article_2 = «Alphabet Inc., the parent company of Google, faces a setback as regulatory concerns lead to a sharp decline in share prices. The market reacts negatively to uncertainties surrounding the company’s antitrust issues.»
prompt = f»»»
Few-shot prompt:
Task: Analyze the impact of the news on stock prices.
Instructions: As a seasoned finance expert specializing in the Indian stock market, you possess a keen understanding \
of how news articles can influence market dynamics. In this task, you will be provided with a news article \
or analysis. Upon thoroughly reading the article, if it contains specific information about a company’s \
stock, please provide the associated Stock Symbol (NSE or BSE Symbol), the Name of the stock, and the \
anticipated Impact of the news.The Impact value should range between -1.0 and 1.0, with -1.0 signifying \
highly negative news likely to cause a significant decline in the stock price in the coming days/weeks, \
and +1.0 representing highly positive news likely to lead to a surge in share price in the next few days/weeks.\
Your response must be strictly in the JSON format.Consider the following factors while determining the impact: \
The magnitude of the news, The sentiment of the news,Market conditions at the date of the news, Liquidity \
of the stock, The sector in which the company operates, The JSON response should include the keys: symbol, \
name, and impact. Do not consider indices such as NIFTY. If the news is not related to the stock market or any \
specific company, leave the values blank. Do not invent values; maintain accuracy and integrity in your response.\
Examples:
1. Article: «{article_1}»
Response: {{«symbol»: «AAPL», «name»: «Apple Inc.», «impact»: 0.9}}

2. Article: «{article_2}»
Response: {{«symbol»: «GOOGL», «name»: «Alphabet Inc.», «impact»: -0.5}}

3. Article: «{article}»
Response:
«»»
return prompt

Что ж, определенно существуют лучшие методы подсказок, чем та, что описана выше.

Полезно почитать о методах подсказок — ознакомьтесь с ними и измените их по мере необходимости.

https://developers.generativeai.google/guide/prompt_best_practices

Несколько примеров, на которые можно вдохновиться https://developers.generativeai.google/prompt-gallery

Разработать торговую стратегию

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

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

Strong Buy — Если новость имеет позитивный настрой и текущий RSI < 35 (зона перепроданности)

Покупать — если новость имеет позитивный настрой

Продавать — если новость имеет негативные настроения

Strong Sell — Если новость имеет негативные настроения и текущий RSI > 75 (зона перекупленности)

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

Давайте воплотим эту стратегию в жизнь. Простой класс с 4 категориями.#news_tech_trader.py
from news_api import news
import pandas as pd
from palm_interface import palm_interface
from yahoo_finance import yfi
import os
from datetime import date
from news_scrapper import factory
class NewsTrader:
def __init__(self):
self.strong_buy = []
self.buy = []
self.strong_sell = []
self.sell = []

Импорт может вас немного смутить. Не переживай. Взгляните на GitHub, и все будет выглядеть нормально.

Вспомогательный метод для получения RSI для данного символа. def get_rsi(self, symbol):
row = yfi.download_data(symbol,’./data/’)
print(row)
if row is None:
return 50.0
else:
return row[‘RSI14’] if row[‘RSI14’] else 50.0

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

Теперь соединяем кусочки вместе…def run(self):

# PART — I — Check below explanation
results_list = []
extracted_news_file = f’./data/news/{date.today()}.csv’
df = pd.DataFrame()
if os.path.exists(extracted_news_file):
df = pd.read_csv(extracted_news_file)
else:
extracted_news_file = news.extract_news()
df = pd.read_csv(extracted_news_file)

# PART — II — Check below explanation
if ‘symbol’ not in df:
for index,row in df.iterrows():
text = factory.create_and_scrape(row[‘URL’])
if text is None or len(text)<10:
print(‘scrape not successful!’)
text = str(row[‘Title’]) + ‘ ‘ + str(row[‘Description’])
text = palm_interface.summarize(text)
data = palm_interface.prompt(text)
results_list.append({
‘symbol’: data[‘symbol’] if data else «»,
‘name’: data[‘name’] if data else «»,
‘impact’: data[‘impact’] if data else 0.0
})
df2 = pd.DataFrame(results_list)
df = pd.concat([df, df2], axis=1)
df.to_csv(extracted_news_file, index=False)

# PART — III, IV — Check below explanation
for index, row in df.iterrows():
if not pd.isna(row[‘symbol’]) and len(row[‘symbol’])>0:
rsi = self.get_rsi(row[‘symbol’])
if row[‘impact’] > 0.4:
if rsi <=35:
self.strong_buy.append(row[‘symbol’])
else:
self.buy.append(row[‘symbol’])
elif row[‘impact’] < -0.4:
if rsi >=75:
self.strong_sell.append(row[‘symbol’])
else:
self.sell.append(row[‘symbol’])

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

  1. Прочтите CSV-файл новости, если он существует. Если нет, сначала загрузите новостные статьи в формате CSV.
  2. Выполните итерацию по каждой статье, очистите всю статью по мере необходимости и сделайте вывод API Palm. Свяжите результаты вывода с самим новостным DF. Сохраните его обратно в тот же файл
  3. Повторите каждую статью еще раз. получить значение RSI для каждой акции, вызвав get_rsi.
  4. В зависимости от категории, в которую он попадает, храните в списке. Для некоторых статей Символ может быть недоступен или неверен. Пока все в порядке, в одной из следующих статей мы рассмотрим, как избавиться от таких проблем с помощью ElasticSearch и убедиться, что результаты извлечения из LLM правильные.

Полный код доступен на Github. Пожалуйста, пометьте проект звездочкой или ответвлением, если хотите.

Заключение

Одна из моих любимых цитат звучит так:

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

Уоррен Баффет

А что может быть лучше для понимания текущих обстоятельств компании, чем ее последние новости?

Реализация нашей стратегии здесь освежающе проста, но настоящее волнение возникает, когда мы смешиваем силу новостей и технического анализа с фундаментальными идеями. В следующих статьях мы будем использовать Screener API, чтобы добавить уровень сложности в режиме реального времени. Например, подумайте о долгосрочном влиянии новостей, таких как положительная выручка или увеличение книги заказов — оно часто выходит за рамки одного дня на рынке. И наоборот, негативные новости могут влиять на динамику акций в течение нескольких недель. Так зачем же ограничивать себя только одним днем новостей? В увлекательном упражнении для наших читателей мы призываем изучить новости за несколько дней для каждой компании, усреднить настроения, объединить объем и поэкспериментировать, чтобы найти золотую середину. Поверьте, это увлекательное путешествие.

В следующих статьях мы углубимся в тонкую настройку языковых моделей (LLM) с историческими новостными данными, сопоставив их с фактическими движениями цен и рассмотрев макроэкономические условия. Мы рассмотрим процесс публикации и тестирования модели, чтобы оценить ее производительность. Кроме того, мы займемся парсингом лент Twitter для зарегистрированных на бирже компаний, интегрируя важные фундаментальные данные, такие как соотношение долга к собственному капиталу (D/E), рентабельность собственного капитала (ROE), рентабельность задействованного капитала (ROCE), отношение цены к прибыли (P/E) и многое другое. Чтобы еще больше усовершенствовать рекомендательную систему, мы включим анализ движения объемов. Приготовьтесь к увлекательному путешествию, поскольку вместе мы создадим более ценную и всеобъемлющую систему рекомендаций.

Часть IV. ElasticSearch для точности биржевого символа/тикера

Этот пост является продолжением нашего предыдущего поста и будет прочитан после завершения предыдущего.

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

В нашем прошлом посте мы агрегировали новостные статьи, использовали LLM для извлечения символа и настроения новости, а затем объединили его с техническими индикаторами. Несмотря на то, что НЛП и LLM обладают большим потенциалом для выполнения всех этих задач, крайне важно привязать каждую новостную статью к правильной организации/символу. Это гарантирует, что мы можем комбинировать новости с их фундаментальными и техническими данными из других источников.

Является ли получение точного символа проблемой ИИ?

Несмотря на то, что LLM/NLP обычно используется для идентификации символа по названию акции,

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

Что ж, тогда, если я отдам полную базу данных символов организации в LLM и попрошу поискать ошибки, разве это не решит проблему для меня, не выдумывая?

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

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

Что такое ElasticSearch?

ElasticSearch — это полнотекстовая поисковая библиотека с открытым исходным кодом, построенная на основе Apache Lucene. Преимуществ много, так как он предлагает текстовый поиск (даже если у нас нет точно соответствующего текста для поиска). Он называется Fuzzy Search. Нечеткий поиск позволяет ElasticSearch находить документы, соответствующие заданному поисковому запросу, даже если в терминах есть орфографические ошибки или небольшие различия. Это особенно полезно, когда модели LLM/NLP не предоставляют нам точное название организации или когда сама исходная статья имеет только частичное название или название с орфографическими ошибками. Есть и другие преимущества. В конце концов, мы посмотрим на это по мере реализации.

Недавно Elasticsearch представил нечто под названием Elasticsearch Query Language (ES|К.Л.). Он преобразует и упрощает исследование данных. Система ES|Механизм QL предоставляет расширенные возможности поиска, повышая эффективность и ускоряя разрешение проблем с помощью поиска и оптимизированных рабочих процессов.

  1. Первое, что нам нужно, это источник данных для списка акций. мы можем получить его с официального сайта NSE и BSE.

https://www.nseindia.com/market-data/securities-available-for-trading

2. Скачайте файл и сохраните его в формате csv — EQUITY_L.csv

2. Далее нам понадобится экземпляр ElasticSearch. Обычно я запускаю такие экземпляры как Docker-контейнер. Перейдите по ссылке ниже, чтобы установить Elastic на Docker.

https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html

Примечание: Достаточно одного узла, если только это не производственная среда.

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

В определении класса мы зададим некоторые основные детали эластичных конфигураций и имя индекса.#elastic_interface.py
import pandas as pd
from elasticsearch import Elasticsearch
from elasticsearch.helpers import bulk
import json

class Elastic:
def __init__(self):
# Elasticsearch connection settings
self.es_user = ‘elastic’
self.es_password = ‘mfb6RIIrWNrg7ors2LhA’

# Create Elasticsearch connection with authentication
self.es = Elasticsearch(
«https://localhost:9200»,
http_auth=[self.es_user,self.es_password],
verify_certs=False
)
# Specify your index (without doc_type for recent Elasticsearch versions)
self.index_name = ‘stocks’

def load_data(self):
# Read CSV file into a DataFrame
csv_file_path = ‘EQUITY_L.csv’
df = pd.read_csv(csv_file_path)

# Convert DataFrame to JSON with orient=’records’
json_data = df.to_json(orient=’records’)

# Convert JSON data to a list of dictionaries
documents = json.loads(json_data)

# Use the bulk API to index the data
actions = [
{«_op_type»: «index», «_index»: self.index_name, «_source»: doc}
for doc in documents
]

success, failed = bulk(self.es, actions)
print(f»Successfully indexed {success} documents. Failed to index {failed} documents.»)

Наконец, выполните метод, инициализировав объектelastic = Elastic()
#elastic.load_data()

Раскомментируйте строку elastic.load_data() и запустите файл с помощью python3 elastic_interface.py и закомментируйте его обратно. Нам не нужно загружать данные более одного раза (если только контейнер не нужно запускать заново)

Примечание: verify_certs=False следует рассматривать только в том случае, если вы запускаете elasticsearch локально. При работе на сервере, особенно в производственной среде, рассмотрите возможность использования сертификатов ЦС.

Краткое пояснение к коду:

  1. Загрузите CSV-файл и преобразуйте его в JSON.
  2. Выполните цикл JSON и подготовьте данные для индексации следующим образом
  • _op_type: тип операции, который может быть «index», «create», «update» или «delete». В нашем случае мы используем «index», чтобы указать, что мы хотим проиндексировать (вставить или обновить) документ.
  • _index: имя индекса, в котором должен храниться документ.
  • _source: фактические данные документа.

мы используем массовый API библиотеки elasticsearch для индексации данных, поэтому мы делаем шаг 2.

3. Сохраните успешные и неудачные результаты в 2 параметрах, которые массовый API возвращает по умолчанию.

Приведенный выше сценарий выдаст результат, подобный приведенному нижеSuccessfully indexed 1941 documents. Failed to index [] documents.

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

Но перед этим важно отметить, что сам Yahoo Finance предоставляет API для извлечения символов из имен. Тем не менее, этот API может ожидать общее название организации и может не учитывать орфографические ошибки или галлюцинации, которые могут возникнуть у LLM. Кроме того, похоже, что в этом API все еще есть некоторые ошибки. Например, при прохождении мимо Индийского банка возвращается символ South Indian Bank. Indian Bank Это 2 разных банка! Таким образом, полагаться исключительно на поисковый API Yahoo Finance может быть не очень надежно, но он может послужить хорошим решением, которое мы можем улучшить с помощью нашего собственного Elasticsearch.

Мы обновим файл yahoo_finance.py, который мы создали в предыдущей статье. Давайте сначала добавим несколько зависимостей#yahoo_finance.py
from elastic_interface import elastic
import requests

Мы создадим новый get_symbol_from_name, который будет использовать как yahoo finance api, так и эластичный поиск, чтобы убедиться, что правильный символ идентифицирован.#yahoo_finance.py

def get_symbol_from_name(self, name, symbol):
try:
yfinance = «https://query2.finance.yahoo.com/v1/finance/search»
user_agent = ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36’
params = {«q»: name, «quotes_count»: 1, «country»: «India»}
res = requests.get(url=yfinance, params=params, headers={‘User-Agent’: user_agent})
data = res.json()
symbol = next((row[‘symbol’] for row in data[‘quotes’] if row[‘exchange’] == ‘NSI’ or row[‘exchange’] == ‘BSE’),None)
symbol = next(row[‘symbol’] for row in data[‘quotes’]) if not symbol else symbol
except:
symbol = elastic.perform_search(name) if data else «»
symbol = str(symbol) + (‘.NS’ if symbol and not(symbol.endswith(‘.NS’)) and not(symbol.endswith(‘.BO’)) else »)
return symbol

мы делаем API-запросы к url https://query2.finance.yahoo.com/v1/finance/search

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

Давайте подумаем о нескольких сценариях.

  1. Когда статья о компании, которая не котируется на рынке.
  2. Когда статья о компании, которая не котируется на индийском рынке.
  3. Когда LLM предоставил название компании, но оно не совпадает с базой данных Yahoo Finance, или есть несколько компаний с похожими названиями с очень редкими способами однозначной идентификации.
  4. Мы загрузили в Elasticsearch только индийский список акций, и он не может работать с компаниями из других стран.

Учитывая все вышеперечисленные факторы, после того, как мы выполним вызов API для поиска, мы выполним следующие действияsymbol = next((row[‘symbol’] for row in data[‘quotes’] if row[‘exchange’] == ‘NSI’ or row[‘exchange’] == ‘BSE’),None)
symbol = next(row[‘symbol’] for row in data[‘quotes’]) if not symbol else symbol

Что мы здесь делаем на самом деле?

Строка 1: Если компания котируется на индийском фондовом рынке (проверка BSE или NSI), берем символ; в противном случае мы устанавливаем его в None.

Строка 2: Если компания не котируется на индийском фондовом рынке, но котируется в какой-либо другой части мира, мы все равно берем символ (мы скоро выясним, почему). Если нет, мы выбираем путь исключения.

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

Ближе к концу мы намеренно добавляем .NS к символу, даже если это не индийская компания. Это связано с тем, что Yahoo Finance будет считать такую компанию несуществующей и не будет возвращать никаких данных. Мы не хотим, чтобы Yahoo возвращала технические данные, когда акции не являются частью BSE или NSE, поскольку наше внимание здесь сосредоточено только на акциях NSE и BSE, поэтому мы следуем этому подходу. Как читатель, если вы хотите использовать это для других бирж, вам, возможно, придется немного изменить это здесь.

Обратите внимание, что мы также импортировали эластичный объект и использовали elastic.perform_search(name) в приведенном выше коде.

Напишем определение perform_search#elastic_interface.py

def perform_search(self,search_term):
if not search_term:
return
print(search_term)
q = {
«query»: {
«match» : {
«NAME OF COMPANY»: {
«query»: search_term,
«fuzziness»: «AUTO»
}
}
}
}
search_results = self.es.search(index=self.index_name, body=q)

for hit in search_results[‘hits’][‘hits’]:
return hit[‘_source’][‘Symbol’]

Это способ запроса Elasticsearch, похожий на SQL-запрос. Как вы можете заметить, мы использовали нечеткий поиск со значением, совпадающим с полем NAME OF COMPANY. Нечеткий поиск оставляет место для ошибок и заблуждений. В случае, если INFOSYS неправильно написано как INSOFYS нечеткий поиск использует так называемое расстояние Левенштейна, допуская такие минимальные ошибки, и мы все равно получим правильный символ от elasticsearch в качестве ответа.

Это особенно важно при работе с LLM, потому что на один и тот же запрос LLM может отвечать по-разному в разное время. Один раз может быть написано RELIANCE RELIANCE LTD, а в другой раз RELIANCE INDUSTRIES LIMITED, и мы не можем полагаться на точное совпадение текста. Эта функция похожа на то, что помогает нам найти нужный фильм, даже если мы вводим частичный или неправильный термин в поиске Netflix.

Несмотря на то, что у этого подхода Elasticsearch есть свои плюсы, есть и минусы. Например, новостная статья о Boeing Ltd. идентифицирована Elasticsearch как Bombay Dyeing!

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

Последнее изменение заключается в том, чтобы включить изменения для поиска Yahoo Finance и Elasticsearch в наш основной файл, который создает рекомендации на основе новостей.

Добавьте следующую строку после того, как мы подведем итог и извлечем результат из LLM (см. предыдущую статью для ясности).#news_tech_trader.py
symbol = yfi.get_symbol_from_name(data[‘name’],data[‘symbol’]) if data else «»

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

Теперь, когда мы закончили писать код, давайте посмотрим на него в действии. Давайте снова запустим код, который мы создали в предыдущей статьеnews_tech_trader.py news_tech_trader.py выполнив python3 news_tech_trader.py в терминале.

Результат выглядит примерно так: ниже

Неплохо? Качество результатов значительно улучшилось по сравнению с нашей предыдущей статьей, так как мы можем увидеть, что прогнозы символов и технические данные имеют меньше ошибок, чем те, которые были у нас при первой реализации. До сих пор мы объединили новостные статьи и некоторые технические индикаторы, а также некоторые прогнозы настроений с LLM. Вот полный код на github.

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

Часть V — Streamlit и CSS для портала агрегации новостей

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

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

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

Сначала я экспериментировал с WTForms и шаблонами Jinja, но поиски динамических графиков и привлекательных визуальных эффектов привели меня к дальнейшим исследованиям. Именно тогда я наткнулся на Streamlit — абсолютное откровение. Легкость Streamlit позволила мне придерживаться удобного для меня стиля программирования на Python, что сделало разработку проектов удивительно простой.

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

История окончена….

Примечание: Пожалуйста, прочтите предыдущие статьи Часть-III и Часть-IV, прежде чем читать дальше, для плавной преемственности.

Что это за портал пользовательского интерфейса?

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

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

  • Home.py: Файл Streamlit, предназначенный для фронтенда.
  • styles.css: простой CSS-файл для стилизации фронтенда и улучшения макетов карточек.

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

#Home.py
import streamlit as st
from datetime import datetime, date
import pandas as pd
import os
from news_tech_trader import nt
import time

st.set_page_config(layout=»wide»)
bgs = [
«#8B0000″,»#B22222″,»#DC143C»,»#FF4500″, «#FF6347″,»#FF7F50″,»#FFA07A»,»#FFD700″, «#ADFF2F»,»#008000″
]

Home.py — это серверный файл Streamlit, предназначенный для обслуживания содержимого пользовательского интерфейса. В этом файле рекомендательный класс из предыдущей статьи (находится в news_tech_trader.py) импортируется с помощью строки from news_tech_trader import nt. Макет страницы настроен таким образом, чтобы он был широким, что обеспечивает полностраничное взаимодействие с приложением.

Кроме того, палитра из 10 цветов, от зеленого до янтарного и красного, была выбрана и сохранена в списке (bgs). Эти цвета будут использоваться для визуального представления настроений ИИ на отображаемых карточках.#Home.py — continues
def local_css(file_name):
with open(file_name) as f:
st.markdown(f'<style>{f.read()}</style>’, unsafe_allow_html=True)

def get_data_frame(file_name):
df = pd.read_csv(file_name)
if ‘ImageURL’ not in df.columns:
df[‘ImageURL’] = None

if ‘recommendation’ not in df.columns:
df[‘recommendation’] = None

if ‘PRICE_AT_TIME’ not in df.columns:
df[‘PRICE_AT_TIME’] = None
else:
df = df[df[‘PRICE_AT_TIME’].notna()]
df = df.sort_values(by=’impact’, ascending = False)
return df

В этом сегменте Home.py определены два метода. Функция local_css загружает CSS-файл с помощью st.markdown, что позволяет использовать пользовательские стили. Функция get_data_frame отвечает за получение необходимого кадра данных из CSV-файла. Метод включает в себя такие этапы очистки данных, как проверка на наличие нулевых значений, проверка существования столбца и сортировка данных по столбцу «влияние» в порядке убывания для лучшего представления.

Далее идет самая большая часть этой реализации. Так что готовьтесь..#Home.py — continues
local_css(‘./styles.css’)
def display_news_cards(selected_date):
col1, col2, col3 = st.columns(3)
cols = [col1, col2, col3]
file_name = f’./data/news/{selected_date}.csv’
if os.path.exists(file_name):
df = get_data_frame(file_name)
chunk_size = 3
for i in range(0, len(df), chunk_size):
chunk = df.iloc[i:i+chunk_size].reset_index(drop=True)
for idx, row in chunk.iterrows():
with cols[idx]:
new_value = int((row[‘impact’] + 1) * 50)
disp = new_value — (new_value%10)
bg = bgs[disp//10]
container = st.container()
container.markdown(
f»»»
<div class=»card»>
<img src={row[‘ImageURL’]} alt=»News Image»>
<div class=»content»>
<h2><a href=»{row[‘URL’]}» target=»_blank»>{row[‘Title’]}</a></h2>
<p class=»organization»>{row[‘name’]} — {row[‘symbol’]}</p>
<p class=»sentiment»>AI Sentiment(ranges from -1 to 1): {row[‘impact’]}</p>
<p class=»sentiment»>AI Recommendation: {row[‘recommendation’]}</p>
</div>
<div class=»sentiment-container»>
<!— Sentiment Fill —>
<div class=»sentiment-slider» style=»width: {disp}%; background-color:{bg};»></div>
</div>
</div>»»», unsafe_allow_html=True)

else:
st.text_area(label=»Empty data display»,value=»No Data Available.»,label_visibility=»collapsed»)

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

Вот объяснение кода

  • Этот сегмент кода начинается с вызова метода, ответственного за загрузку CSS-файла. В функции display_news_cards начальный шаг включает в себя попытку прочитать файл на основе даты, введенной пользователем. Как напоминаем из нашей предыдущей статьи, мы приняли соглашение об именовании CSV-файлов — <date>.csv — где дата соответствует дате публикации новостных статей, тональности ИИ и техническим данным.
  • Если файл существует, его содержимое отображается в пользовательском интерфейсе в виде карточек. При отсутствии файла отображается текстовое поле с сообщением «Данные недоступны». Чтобы приспособиться к макету, мы используем функцию st.columns в Streamlit для определения трех столбцов.
  • Если файл существует, кадр данных группируется в несколько наборов, каждый из которых содержит три элемента. Такая группировка гарантирует, что каждый элемент отображается в определенном столбце. Механизм фрагментации, облегченный установкой chunk_size равным 3, способствует организованному представлению новостных статей в пользовательском интерфейсе.

Обратите внимание на строку chunk = df.iloc[i:i+chunk_size].reset_index(drop=True) очень внимательно. Мы сбрасываем индекс так, чтобы каждый чанк всегда имел индекс 0,1,2. Это помогает нам сгруппировать статью (строку в df) в соответствующий столбец в нашем макете из 3 столбцов.

  • Мы реализуем механизм округления прогнозируемых значений тональности, первоначально находящихся в диапазоне от -1,0 до +1,0, чтобы они помещались в диапазоне от 0 до 100 и округлялись до ближайшего 10. Это округленное значение становится решающим фактором при определении цветового представления тональности.

new_value = int((row[‘impact’] + 1) * 50)
disp = new_value — (new_value%10)
bg = bgs[disp//10]

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

<div class=»card»>
<img src={row[‘ImageURL’]} alt=»News Image»>
<div class=»content»>
<h2><a href=»{row[‘URL’]}» target=»_blank»>{row[‘Title’]}</a></h2>
<p class=»organization»>{row[‘name’]} — {row[‘symbol’]}</p>
<p class=»sentiment»>AI Sentiment(ranges from -1 to 1): {row[‘impact’]}</p>
<p class=»sentiment»>AI Recommendation: {row[‘recommendation’]}</p>
</div>
<div class=»sentiment-container»>
<!— Sentiment Fill —>
<div class=»sentiment-slider» style=»width: {disp}%; background-color:{bg};»></div>
</div>
</div>

  • Макет определяется с помощью <div> с card класса. Он включает в себя раздел изображений в верхней части, заполненный сохраненной ссылкой на изображение. Далее следует еще один <div> с классом content, где отображается текстовая информация, такая как заголовок новости, название компании, настроение ИИ и рекомендация.
  • В нижней части карточки находится класс sentiment-container . В этом разделе ширина и цвет фона (bg) динамически задаются на основе вычисленной переменной dispbg выбирает один из цветов, определенных в переменной bgs, обеспечивая визуальное представление тональности. Эта гибкая комбинация Python и HTML/CSS в Streamlit облегчает создание эстетически привлекательных и интерактивных пользовательских интерфейсов.

Очень долго я писал CSS. Поэтому я использовал немного ChatGPT, чтобы закодировать его для себя. Давайте углубимся в наш CSS макета карточек..card {
display: flex;
flex-direction: column;
position: relative;
width: 100%;
height: 500px;
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
margin: 10px;
}

/* Image Styling */
.card img {
width: 100%;
height: 50%;
object-fit: cover;
}

/* Content Styling */
.card .content {
flex-grow: 1;
padding: 15px;
}

.card h2 {
font-size: 1.2em;
margin-bottom: 10px;
}

.card p {
font-size: 0.9em;
color: #21bdf5;
}

.card .organization {
font-weight: bold;
}

/* Sentiment Styling */
.card .sentiment-container {
display: flex;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 25px;
background-color: #f2f2f2;
}

.card .sentiment-slider {
flex-grow: 1;
background-color: #4CAF50;
}

.card .sentiment-label {
font-size: 25px;
color: #44e6f8;
}

/* Link Styling */
.card a {
text-decoration: none;
color: #007BFF;
}

/* Hover Effect */
.card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
transition: box-shadow 0.3s, transform 0.3s;
}

Давайте попросим владельца кода дать нам небольшое объяснение. Я считаю, что ChatGPT мог бы предоставить лучшее объяснение приведенного выше кода, чем то, что я когда-либо мог сделать. Итак, вот ответ https://chat.openai.com/share/77b72f06-dd10-4abc-b5f5-bf5f2d02af66

Далее, давайте создадим индикатор выполнения, который отображает ход выполнения, в то время как загрузка, извлечение и сбор настроений ИИ происходят в серверной части#Home.py
def progress_bar():
progress_text = «Operation in progress. Please wait.»
my_bar = st.progress(0, text=progress_text)

for percent_complete,progress_text in nt.run(date.today()):
time.sleep(0.01)
my_bar.progress(percent_complete, text=progress_text)
time.sleep(1)
my_bar.empty()

Как мы могли видеть, my_bar.progress(percent_complete, text=progress_text) настроен на отображение прогресса. Следовательно, нам нужно будет обновить некоторую часть нашего кода из предыдущей статьи, чтобы регулярно возвращать статус.

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

Что такое yield в Python?

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

Используя yield, функция становится генератором. При вызове функции-генератора она выполняется не сразу; Вместо этого он возвращает объект-генератор. Функция начинает выполняться только при вызове метода next() для объекта-генератора. При каждом столкновении с оператором yield во время выполнения функция создает значение, приостанавливается и сохраняет свое состояние. Последующие вызовы next() возобновляют функцию с того места, где она остановилась, облегчая пошаговую генерацию результатов.

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

Подробнее о генераторах здесь — https://www.geeksforgeeks.org/python-yield-keyword/

В файле news_tech_trader.py, который мы разработали в предыдущей статье, давайте сосредоточимся на использовании ключевого слова yield в методе run.#news_tech_trader.py (developed in last article)
def run(self, day):
results_list = []
day = date.today() if not day else day
extracted_news_file = f’./data/news/{day}.csv’
df = pd.DataFrame()
if not os.path.exists(extracted_news_file):
extracted_news_file = news.extract_news()
df = pd.read_csv(extracted_news_file)
yield 5,»Download Completed..»
df_len = len(df)
i = 0
if ‘symbol’ not in df:
for index,row in df.iterrows():
text = factory.create_and_scrape(row[‘URL’])
if text is None or len(text)<10:
text = str(row[‘Title’]) + ‘ ‘ + str(row[‘Description’])
text = palm_interface.summarize(text)
data = palm_interface.prompt(text)
symbol = yfi.get_symbol_from_name(data[‘name’],data[‘symbol’]) if data else «»
results_list.append({
‘symbol’: symbol if symbol else «»,
‘name’: data[‘name’] if data else «»,
‘impact’: data[‘impact’] if data else 0.0
})
i+=1
percentage = i/df_len*100
yield int(percentage), «Scrapping and AI Analyzing in progress…»
df2 = pd.DataFrame(results_list)
df = pd.concat([df, df2], axis=1)

yield 100, «Generating Recommendations»

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

Хорошо.. Наконец, возвращаясь к нашему Home.py, где мы собрали все это в действии.button_click = st.button(f’Download Latest {date.today()}’)
if button_click:
d = None
progress_bar()
d = date.today()
button_click = False

d = st.date_input(label=»:blue[Select a date]»,format=»YYYY-MM-DD»)
if d:
with st.expander(«News»):
display_news_cards(d)

  • В этом разделе Home.py мы добавили поле кнопки с меткой «Загрузить последнюю» с текущей датой. Нажатие кнопки запускает функцию progress_bar() инициируя фактическое выполнение загрузки новостей, парсинга и анализа тональности ИИ.
  • После этого предоставляется поле выбора даты с помощью st.date_input. При выборе даты отображается раздел экспандера, раскрывающий карточки новостей на выбранную дату с помощью функции display_news_cards, которую мы разработали ранее.
  • Эта интерактивная настройка обеспечивает бесперебойную работу пользователей, позволяя пользователям загружать последние новости, отслеживать ход выполнения с помощью динамического индикатора выполнения и просматривать карточки новостей на основе предпочтительной даты.

Мы закончили писать весь код… Уфф…

Полный код доступен на github — https://github.com/vishyarjun/news_based_stock_analyzer

Результат реализации

  1. Карточки и представление тональности ИИ

2. Загрузка и обновление прогресса в реальном времени

Несколько заключительных мыслей…

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

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

Часть VI — LLM Vs Алгоритмический скринер акций

Как мы знаем, большие языковые модели превосходно раскрывают аналитические сведения при запросе. Однако их истинный потенциал проявляется, когда для анализа предоставляются реальные данные. Самое большое преимущество заключается в их способности генерировать результаты, основанные на рациональном обосновании, а не на чувствах (хотя предубеждения все еще могут сохраняться!).

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

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

Итак, какие данные мы собираемся предоставить магистрам права?

  • Мультипликатор РЕ
  • Долг/Собственный капитал
  • Возврат за 1 неделю, 1 месяц, 3 месяца, 6 месяцев, 1 год
  • Рыночная капитализация
  • Рост прибыли
  • Рост выручки

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

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

Для всех своих оценок LLM я полагаюсь на mlflow и Ragas Эти инструменты оптимизируют процесс оценки и предоставляют бесценную информацию о производительности модели. Кроме того, в будущем я планирую углубиться в отдельную серию, посвященную оценке LLM.

Какую систему оценки вы предпочитаете? Делитесь своими мыслями в комментариях!

Итак, почему Cohere?

Cohere выделяется как корпоративная платформа искусственного интеллекта, предлагающая ряд больших языковых моделей (LLM), включая Command, Rerank и Embed. Что отличает Cohere, так это его приверженность удобным API-интерфейсам, разработанным для упрощения потребления. Этот акцент на доступности согласуется с главной целью платформы — сделать ИИ более доступным.

Лично изучив различные варианты, я обнаружил, что в экосистеме Cohere удивительно легко ориентироваться, что является освежающим отходом от сложностей, часто связанных с такими платформами, как AWS, GCP и Azure. Более того, бесшовная поддержка Cohere для тонкой настройки и разработки генеративных моделей с расширением извлечений (RAG) еще больше укрепила мое восхищение.

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

Ладно, пора погружаться в реализацию!

Часть I — исторические данные

  • Если вы следили за этой серией статей, вы, вероятно, знакомы с различными источниками, такими как Yahoo Finance, Kite API и другими, используемыми для получения исторических данных. Эта тема подробно рассмотрена в многочисленных статьях.
  • Для этого эксперимента я собрал ежедневные данные за два года по всем акциям Nifty500 и Nasdaq.
  • Я предполагаю, что вы, читатель, можете загрузить исторические данные по акциям, которые вы хотите проанализировать, и аккуратно организовать их в одной папке. Этот основополагающий шаг имеет решающее значение для оптимизации процесса скрининга запасов.

Часть II — Генерация фрейма данных для LLM

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

  1. Создайте класс с некоторыми переменными. self.df_all — это место, где хранится окончательный кадр данных перед передачей в LLM для скрининга
#screener.py
class Screener:
def __init__(self, folder):
self.index = folder
self.folder = f'./data/historical/analysis/{folder}/'
self.files = os.listdir(self.folder)
self.df_all = pd.DataFrame()

2. Создадим метод run для генерации DF

#screener.py
def run(self):
for index, filename in enumerate(self.files):
if (filename.endswith('xlsx') or filename.endswith('csv')) and not filename.startswith('~$'):
df = pd.read_csv(self.folder+filename)

df['TIMESTAMP'] = pd.to_datetime(df['TIMESTAMP'],utc=True)
df['TIMESTAMP'] = df['TIMESTAMP'].dt.date
current_date = df.iloc[-1]['TIMESTAMP']

symbol = df.iloc[-1]['SYMBOL']

info = yf.Ticker(symbol)
earnings_date = info.info.get('mostRecentQuarter',None)
earnings_date = datetime.utcfromtimestamp(earnings_date) if earnings_date else None
earnings_growth = round(info.info.get('earningsQuarterlyGrowth',0)*100,2)
close = df.iloc[-1]['CLOSE']
pe = info.info.get('trailingPE',None)

revenue_growth = round(info.info.get('revenueGrowth',0)*100,2)
debtToEquity = round(info.info.get('debtToEquity',0)/100.0,2)
market_cap = str(round(info.info.get('marketCap',0)/10000000.0,2)) + 'cr'
one_week_ago = self.find_nearest_available_date(current_date - pd.DateOffset(days=7),df)
close_one_week_ago = df.loc[df['TIMESTAMP'] == one_week_ago,'CLOSE'].iloc[0]

# sometimes a data before a year may not be there, we simply fall back to the available value
year_ago = self.find_nearest_available_date(current_date - pd.DateOffset(years=1),df)
matching_rows = df.loc[df['TIMESTAMP'] == year_ago]
if not matching_rows.empty:
close_year_ago = matching_rows.iloc[0]['CLOSE']
else:
close_year_ago = close_six_month_ago

Не волнуйтесь! Все просто.

  • Выполните итерацию по всем файлам, прочтите файл Excel и сделайте столбец timestamp столбцом datetime
  • Извлечение из листа самой поздней даты
  • yf.Ticker(symbol) для извлечения некоторых фундаментальных данных и получения некоторой информации, такой как мультипликатор PE, задолженность по прибыли к собственному капиталу, рыночная капитализация и т. д.
  • Посмотрите, как мы использовали find_nearest_available_date когда мы пытаемся извлечь цену закрытия неделю или месяц назад, она может закончиться праздником, поэтому здесь мы попробуем найти ближайшую дату, когда рынок открыт, и вытащить цену закрытия. Ниже приведена фактическая логика
#screener.py
def find_nearest_available_date(self, date,df):
ctr=0
date = date.date()
while not (df['TIMESTAMP']==date).any():
date -= timedelta(days=1)
ctr+=1
if ctr>5:
break

return date

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

#screener.py
def get_percent(self, current,old):
change = current-old
percent = change/old*100
return round(percent,2)

4. Наконец, сгенерируйте фрейм данных.

#screener.py - under run() method under the for loop (check above for reference)
data = {'date': [current_date],
'symbol': [symbol],
'pe' : [pe],
'debtToEquity':[debtToEquity],
'market_cap':[market_cap],
'close': [close],
'price_change_one_week': [self.get_percent(close,close_one_week_ago)],
'price_change_one_month': [self.get_percent(close,close_one_month_ago)],
'price_change_three_month':[self.get_percent(close,close_three_month_ago)],
'price_change_six_month':[self.get_percent(close,close_six_month_ago)],
'price_change_one_year':[self.get_percent(close,close_year_ago)],
'earnings_date':earnings_date,
'earnings_growth':earnings_growth,
'revenue_growth':revenue_growth
}
df_change = pd.DataFrame(data)
self.df_all = pd.concat([self.df_all,df_change], axis=0)

5. нам нужно хранить данные в CSV

#screener.py
def store_in_csv(self):
folder = f'./data/historical/screener/'
filename = f'{self.index}_{str(date.today())}.csv'
path = os.path.join(folder,filename)
self.df_all.to_csv(path)

6. Теперь у нас есть все данные, необходимые для основного шага. Переходим к самому захватывающему — Скринингу

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

Алгоритмический скринер

  • Наше правило для алгоритмического скринера простое
  • Компании с недавними результатами (мы зафиксировали результат квартала в нашем DF), низким долгом, низким P/E, хорошей выручкой и прибылью, но менее 3-месячной прибыли за недооценку. Здесь можно использовать любые критерии.
def algorithmic_screening(self):
df = self.df_all
# Calculate today's date
today = datetime.now()

# Calculate the date three months from today
three_months_ago = today - timedelta(days=90)

# Apply filter conditions
df = df[(df['pe'] < 25) &
(df['debtToEquity'] < 0.5) &
(df['earnings_growth'] > 0) &
(df['revenue_growth'] > 0) &
(df['earnings_date'] > three_months_ago)]
# Sort filtered DataFrame by 'one_month' ascending
df = df.sort_values(by='one_month', ascending=True)
print(df.head(5))
return

Мы получаем вышеуказанные результаты, запустив код. Как это подтвердить? Возьмем, к примеру, BAJAJHLDNG, у нее 100% рост прибыли и 1058% роста выручки, но в прошлом месяце она снизилась на -3,68%. Давайте проверим мой любимый сайт screener.in, чтобы проверить это.

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

Интересно, что такой персонализированный подход отражает функциональность пользовательских интерфейсов (UI) многих веб-сайтов скрининга

Скринер LLM

Установите клиент cohere python pip install cohere.

Хорошо, давайте сначала создадим интерфейс cohere, как cohere_interface.py. Я просто обожаю их документацию — https://docs.cohere.com/reference/chat

#cohere_interface.py
import cohere
from dotenv import load_dotenv
import os
import json
import re
load_dotenv()

class Cohere:
def __init__(self):
self.co = cohere.Client(os.getenv("CO"))

def chat_cohere(self,message,preamble):
response = self.co.chat(
message=message,
preamble=preamble
)
pattern = r'\{[^{}]*\}'
# Find all JSON matches in the input string
json_matches = re.findall(pattern, response.text)
json_out = [json.loads(match) for match in json_matches]
return json_out

co = Cohere()
  • Зарегистрируйтесь в cohere, сгенерируйте ключ API и сохраните его в .env
  • Импортируйте все библиотеки. Создайте клиента под __init__
  • Реализуйте простой метод chat_cohere, который принимает 2 входных сообщения и преамбулу.
  • Преамбула — это, по сути, контекст, который мы хотим установить LLM для конкретной задачи, а message — это фактические входные данные для LLM.
  • Они также предоставляют атрибуты, такие как документы, куда мы можем передать контекст RAG и т. д., но здесь они нам не нужны.
  • Просто вызовите их метод чата и проанализируйте текстовый ответ.
  • Я использую функции регулярных выражений, чтобы убедиться, что я получаю ответ Json. OpenAI, Gemini уже имеют встроенный ответ JSON в свои модели. но я не смог найти эту опцию в Cohere, поэтому я создал простой синтаксический анализ регулярных выражений
  • Нам также нужно убедиться, что наше приглашение позволяет Cohere генерировать вывод JSON.
  • Далее мы будем использовать вышесказанное в наших screener.py, которые мы построили выше.
  • Обязательно импортируйте объект CO из cohere_interface.py в screener.py и вызовите метод, который мы разработали в предыдущем chat_cohere.
  • ai_screening метод, в котором мы сгенерируем приглашение. Обратите внимание на то, как мы передаем параметр Data Frame in message и как мы устанавливаем контекст для LLM в preamble.
#screener.py
def ai_screening(self):
message = f"""You are given the data of some stocks, Analyse the data and Screen the stock list and select \
2 stocks that is undervalued and is likely to generate better returns in future. Below is the data {self.df_all}"""
preamble = """You are an excellent stock market analyst who can understand the provided data well and take \
careful decisions. Output should strictly be in the form of json with follwing structure
[
{
"stock": "Name of the stock"
"justification": "Reason why the stock is screened or selected"
},
{
"stock": "Name of the stock"
"justification": "Reason why the stock is screened or selected"
}
]

There should not be additional output other than json
"""
result = co.chat_cohere(message=message,preamble=preamble)
print(result)

Давайте посмотрим на JSON-результаты LLM из терминала.

Как обычно, полный код будет доступен на моем github. Не стесняйтесь тянуть его.

Вот и все!! это так просто использовать Cohere LLM через их API!

Если вы решили использовать LLM на собственном хостинге, я рекомендую ознакомиться с этой статьей, чтобы узнать, как разместить LLM в AWS — https://medium.com/@viswanathan.arjun/deploy-and-host-a-financial-bert-model-as-api-in-aws-sagemaker-7b381dfbcd6a

Нет!! Использовать LLM для принятия решений рискованно!

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

Итак, является ли это доказательством дурака?

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

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

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

Источник