Это моя первая программа на Python, которую я написал на самом деле, и у меня практически нет опыта в этом языке. Я просто решил, что могу учиться и заниматься чем-то, что меня интересует одновременно.
Я не изучаю книги. Я не могу просто сесть и прочитать кучу технических документов и тому подобное, поэтому я уверен, что большая часть этого кода в лучшем случае нетрадиционна, а в худшем — просто плохая. Я лучше всего учусь на практике, поэтому теперь, когда у меня заработала эта программа, я хочу узнать, как сделать ее лучше.
Цель программы — загрузить страницу параметров финансирования Yahoo для каждого тикера в созданном мною файле. Затем извлеките все данные опционов вызова для каждой даты истечения срока действия и загрузите все эти данные в базу данных SQL (для запроса позже).
Я добавил мультиобработку, чтобы попытаться сделать это быстрее, но, похоже, это просто замедлило его. Я должен понять эту часть.
import logging
import pyodbc
import config
import yahoo_fin as yfin
import asyncio
import multiprocessing
import time
from yahoo_fin import options
from datetime import datetime, date
from selenium import webdriver
def main():
read_ticker_file()
def init_selenium():
driver = webdriver.Chrome(config.CHROME_DRIVER)
return driver
def yfin_options(symbol):
logging.basicConfig(filename="yfin.log", level=logging.INFO)
logging.basicConfig(filename="no_options.log", level=logging.ERROR)
try:
# get all options dates (in epoch) from dropdown on yahoo finance options page
dates = get_exp_dates(symbol)
# iterate each date to get all calls and insert into sql db
for date in dates:
arr = yfin.options.get_calls(symbol, date)
arr_length = len(arr.values)
i = 0
for x in range(0, arr_length):
strike: str = str(arr.values[i][2])
volume = str(arr.values[i][8])
open_interest = str(arr.values[i][9])
convert_epoch = datetime.fromtimestamp(int(date))
try:
sql_insert(symbol, strike, volume, open_interest, convert_epoch)
i += 1
except Exception as insert_fail:
print("I failed at sqlinsert {0}".format(insert_fail))
file_name_dir = "C:\temp\rh\options{0}{1}.xlsx".format(symbol, date)
logging.info(arr.to_excel(file_name_dir))
except Exception as e:
bad_tickers_file_dir = config.BAD_TICKERS
f = open(bad_tickers_file_dir, "a")
f.write(symbol)
f.write('n')
def sql_insert(symbol, strike, volume, open_interest, exp_date):
conn_string = ('Driver={SQL Server};'
'Server=DESKTOP-7ONNV8L;'
'Database=optionsdb;'
'Trusted_Connection=yes;')
conn = pyodbc.connect(conn_string)
cursor = conn.cursor()
insert_string = """INSERT INTO dbo.options (Ticker, Strike, Volume, OpenInterest, expDate)
VALUES
(?, ?, ?, ?, ?)"""
cursor.execute(insert_string, symbol, strike, volume, open_interest, str(exp_date))
conn.commit()
def get_exp_dates(symbol):
url = "https://finance.yahoo.com/quote/" + symbol + "/options?p=" + symbol
chromedriver = init_selenium()
chromedriver.get(url)
# Yahoo Finance options dropdown class name (find better way to do this)
select_dropdown = chromedriver.find_element_by_css_selector("div[class="Fl(start) Pend(18px)"] > select")
options_list = [x for x in select_dropdown.find_elements_by_tag_name("option")]
dates = []
for element in options_list:
dates.append(element.get_attribute("value"))
return dates
def read_ticker_file():
file1 = open(config.TICKER_FILE, 'r')
lines = file1.readlines()
count = 0
ticker_arr = []
# loop to read each ticker in file
for line in lines:
count += 1
line = line.strip('n')
line = line.strip()
ticker_arr.append(line)
return ticker_arr
if __name__ == "__main__":
pool = multiprocessing.Pool()
# input list
inputs = read_ticker_file()
# pool object with number of element
pool = multiprocessing.Pool(processes=4)
pool.map(yfin_options, inputs)
pool.close()
pool.join()
1 ответ
Для get_exp_dates
— и все остальное здесь — Селен не нужен. Даты, которые вы ищете, не основаны на AJAX и т. Д., А встроены прямо в HTML:
<select class="Fz(s) H(25px) Bd Bdc($seperatorColor)" data-reactid="5">
<option selected="" value="1626998400" data-reactid="6">July 23, 2021</option>
<option value="1627603200" data-reactid="7">July 30, 2021</option>
<option value="1628208000" data-reactid="8">August 6, 2021</option>
<option value="1628812800" data-reactid="9">August 13, 2021</option>
<option value="1629417600" data-reactid="10">August 20, 2021</option>
<option value="1630022400" data-reactid="11">August 27, 2021</option>
<option value="1631836800" data-reactid="12">September 17, 2021</option>
<option value="1634256000" data-reactid="13">October 15, 2021</option>
<option value="1637280000" data-reactid="14">November 19, 2021</option>
<option value="1642723200" data-reactid="15">January 21, 2022</option>
<option value="1647561600" data-reactid="16">March 18, 2022</option>
<option value="1655424000" data-reactid="17">June 17, 2022</option>
<option value="1663286400" data-reactid="18">September 16, 2022</option>
<option value="1674172800" data-reactid="19">January 20, 2023</option>
<option value="1679011200" data-reactid="20">March 17, 2023</option>
<option value="1686873600" data-reactid="21">June 16, 2023</option>
</select>
Так что просто получите HTML через запросы и проанализируйте его через BeautifulSoup.
for x in range(0, arr_length):
может бросить 0,
по умолчанию.
Поскольку все внутри arr.values[i]
имеет позиционное значение, стоит распаковывать; что-то вроде
@dataclass
class SymbolRow:
contract_name: str
last_trade_date: datetime
strike: Decimal
last_price: Decimal
bid: Decimal
ask: Decimal
change: Decimal
change_percent: float
volume: int
open_interest: int
implied_volatility: float
...
row = SymbolRow(*arr.values[i])
^ В этой версии предполагается, что поля уже должным образом десериализованы из разметки в разумные типы, что, вероятно, не так. Конструктор может это сделать.
"C:\temp\rh\options{0}{1}.xlsx"
не должны быть жестко закодированы в вашем методе и должны существовать либо в конфигурации, либо, возможно, в отдельной глобальной переменной.
f = open(bad_tickers_file_dir, "a")
следует использовать with
.
conn_string
не должно существовать в вашем sql_insert
, и действительно sql_insert
метод не должен быть соединительным; это следует делать на внешнем уровне и только один раз за запуск программы.
[x for x in select_dropdown.find_elements_by_tag_name("option")]
может просто быть
list(select_dropdown.find_elements_by_tag_name("option"))
Нет никакой гарантии для вашего соединения и курсоров. На основе код это не похоже на их реализацию __exit__
разумно, так что просто try/finally/.close()
.
Конфигурация вашего регистратора не имеет особого смысла. Вам, вероятно, следует создать свой собственный экземпляр регистратора для конкретного модуля (вместо вызова базовой конфигурации и использования корневого регистратора) — и иметь только один регистратор с несколькими обработчиками, каждый из которых имеет разный уровень.
Следующий код демонстрирует (без вашего SQL-кода) способ очистки Yahoo Finance без необходимости yahoo_fin
библиотека, охватывающая некоторые из вышеперечисленных, а также демонстрирующая два различных вида предварительной компиляции селектора пути HTML.
import enum
import locale
from datetime import datetime
from decimal import Decimal
from enum import Enum
from locale import setlocale, LC_NUMERIC
from pprint import pprint
from typing import Iterable, Optional, Dict
import pytz
import soupsieve
from soupsieve import SoupSieve
from bs4 import BeautifulSoup, SoupStrainer, Tag
from requests import Session
DATE_STRAINER = SoupStrainer(
'select', class_='Fz(s) H(25px) Bd Bdc($seperatorColor)',
)
OPTION_STRAINER = SoupStrainer(
'section', attrs={'data-yaft-module': 'tdv2-applet-OptionContracts'},
)
CALLS_SIEVE = soupsieve.compile('table.calls')
PUTS_SIEVE = soupsieve.compile('table.puts')
@enum.unique
class OptionKind(Enum):
CALL = enum.auto()
PUT = enum.auto()
class Option:
def __init__(self, row: Dict[str, Tag], kind: OptionKind):
self.kind = kind
self.contract_name = row['Contract Name'].text
self.contract_path = row['Contract Name'].a['href']
dt, tz = row['Last Trade Date'].text.rsplit(maxsplit=1)
# Ugh.
tz = {
'EDT': 'US/Eastern',
# ...
}.get(tz, tz)
self.last_trade_date = datetime.strptime(
dt, '%Y-%m-%d %I:%M%p'
).replace(tzinfo=pytz.timezone(tz))
self.strike = money_or_none(row['Strike'].text)
self.last_price = money_or_none(row['Last Price'].text)
self.bid = money_or_none(row['Bid'].text)
self.ask = money_or_none(row['Ask'].text)
self.change = money_or_none(row['Change'].text)
self.percent_change = percent_or_none(row['% Change'].text)
self.volume = int_or_none(row['Volume'].text)
self.open_interest = int_or_none(row['Open Interest'].text)
self.implied_volatility = percent_or_none(row['Implied Volatility'].text)
def get_yfin(
session: Session,
symbol: str,
when: Optional[int] = None,
straddle: bool = False,
) -> str:
params = {
'p': symbol,
'straddle': straddle,
}
if when is not None:
params['date'] = when
with session.get(
f'https://finance.yahoo.com/quote/{symbol}/options',
params=params,
headers={'Accept': 'text/html'},
) as resp:
resp.raise_for_status()
return resp.text
def get_exp_dates(session: Session, symbol: str) -> Iterable[int]:
doc = BeautifulSoup(
get_yfin(session, symbol),
features="html.parser", parse_only=DATE_STRAINER,
)
for option in doc.find_all('option'):
yield int(option['value'])
def int_or_none(s: str) -> Optional[int]:
if s == '-':
return None
return locale.atoi(s)
def percent_or_none(s: str) -> Optional[float]:
if s == '-':
return None
return float(s.rsplit('%', 1)[0])
def money_or_none(s: str) -> Optional[Decimal]:
if s == '-':
return None
return Decimal(s)
def table_to_dicts(parent: Tag, sieve: SoupSieve) -> Iterable[Dict[str, Tag]]:
table, = sieve.select(parent, limit=1)
heads = [th.text for th in table.thead.tr.find_all('th')]
for tr in table.tbody.find_all('tr'):
yield dict(zip(heads, tr.find_all('td')))
def get_options(session: Session, symbol: str, when: int) -> Iterable:
doc = BeautifulSoup(
get_yfin(session, symbol, when),
features="html.parser", parse_only=OPTION_STRAINER,
)
for call in table_to_dicts(doc, CALLS_SIEVE):
yield Option(call, OptionKind.CALL)
for put in table_to_dicts(doc, PUTS_SIEVE):
yield Option(put, OptionKind.PUT)
def main():
setlocale(LC_NUMERIC, 'en_US.UTF8')
with Session() as session:
session.headers = {'User-Agent': 'Mozilla/5.0'}
dates = tuple(get_exp_dates(session, 'msft'))
for option in get_options(session, 'msft', dates[2]):
pprint(option.__dict__)
if __name__ == '__main__':
main()