Skip to content

Commit

Permalink
introduced future-future spreads for div scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
holohup committed Aug 20, 2023
1 parent 01a85fb commit 7ea1b05
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 76 deletions.
2 changes: 1 addition & 1 deletion bot/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async def tasks(*args, **kwargs):
'buy': ('Buy ASAP', sellbuy),
'dump': ('Dump it', sellbuy),
'scan': ('Spread scanner', scan),
'dscan': ('Dividend scanned', dividend_scan)
'dscan': ('Dividend scanner', dividend_scan)
}


Expand Down
Empty file added bot/scanner/__init__.py
Empty file.
287 changes: 218 additions & 69 deletions bot/scanner/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
from decimal import Decimal
from typing import NamedTuple, Union

from settings import CURRENT_INTEREST_RATE, TCS_RO_TOKEN
from settings import (CURRENT_INTEREST_RATE, MAX_FUTURES_AHEAD,
PERCENT_THRESHOLD, TCS_RO_TOKEN)
from tinkoff.invest import Future, Share
from tinkoff.invest.retrying.aio.client import AsyncRetryingClient
from tinkoff.invest.retrying.settings import RetryClientSettings
from tinkoff.invest.schemas import MoneyValue, Quotation, RealExchange
from tinkoff.invest.utils import quotation_to_decimal
from tools.get_patch_prepare_data import (get_current_prices,
get_current_prices_by_uid)
from tools.trading_hours import FutureTradingHours, StockTradingHours
from tools.trading_time import SimpleTradingTime


@dataclass
Expand All @@ -25,6 +29,20 @@ class StockData(LegData):
pass


@dataclass(frozen=True)
class OrderBook:
bid: Decimal
ask: Decimal

@property
def buy_price(self):
return self.ask

@property
def sell_price(self):
return self.bid


@dataclass
class FutureData(LegData):
basic_asset_size: int = 0
Expand Down Expand Up @@ -54,6 +72,41 @@ class DividendSpread(NamedTuple):
dividend: float
days_till_expiration: int
yld: float
can_trade: bool


class DivSpread(NamedTuple):
sell_leg: Union[Share, Future]
buy_leg: Future

@property
def _can_trade_sell_leg(self):
return all(
(
self.sell_leg.api_trade_available_flag,
self.sell_leg.sell_available_flag,
self.sell_leg.short_enabled_flag,
)
)

@property
def _can_trade_buy_leg(self):
return all(
(
self.buy_leg.api_trade_available_flag,
self.buy_leg.buy_available_flag,
)
)

@property
def can_trade(self):
return all((self._can_trade_sell_leg, self._can_trade_buy_leg))

@property
def ratio(self):
if isinstance(self.sell_leg, Future):
return Decimal('1')
return quotation_to_decimal(self.buy_leg.basic_asset_size)


class SpreadScanner:
Expand Down Expand Up @@ -332,84 +385,180 @@ async def get_api_response(instrument: str):
return await getattr(client.instruments, instrument)()


async def dividend_scan(command, args):
max_futures_ahead = 3
interest_rate = 12
percent_threshold = 1
shares = await get_api_response('shares')
shares = shares.instruments
filtered_shares = {
share.ticker: share
for share in shares
if share.api_trade_available_flag is True
and share.sell_available_flag is True
and quotation_to_decimal(share.min_price_increment) > Decimal('0')
and share.short_enabled_flag is True
}
futures = await get_api_response('futures')
futures = futures.instruments
filtered_futures = {}
for future in futures:
if (
future.api_trade_available_flag is False
or future.buy_available_flag is False
or quotation_to_decimal(future.min_price_increment) <= Decimal('0')
or future.basic_asset not in filtered_shares.keys()
or future.last_trade_date <= datetime.now(tz=timezone.utc)
):
continue
if future.basic_asset not in filtered_futures:
filtered_futures[future.basic_asset] = []
filtered_futures[future.basic_asset].append(future)

for stock_ticker in filtered_futures:
filtered_futures[stock_ticker] = sorted(
filtered_futures[stock_ticker], key=lambda d: d.expiration_date
)[:max_futures_ahead]
filtered_shares = {
ticker: uid
for ticker, uid in filtered_shares.items()
if ticker in filtered_futures.keys()
}
share_prices = get_current_prices_by_uid(list(filtered_shares.values()))
future_prices = get_current_prices_by_uid(
[d for f in filtered_futures.values() for d in f]
)
result = []
for ticker in filtered_shares:
price = share_prices[filtered_shares[ticker].uid]
for future in filtered_futures[ticker]:
f_price = future_prices[future.uid]
class DividendScanner:
def __init__(self) -> None:
self._ir = float(CURRENT_INTEREST_RATE)

async def scan_spreads(self):
await self._load_instruments()
await self._prefilter_instruments()
self._generate_div_spreads()
await self._update_orderbooks()

result = []
for spread in self._div_spreads:
buy_leg = spread.buy_leg
sell_leg = spread.sell_leg
sell_price = self._orderbooks[sell_leg.uid].sell_price
buy_price = self._orderbooks[buy_leg.uid].buy_price
days_till_expiration = (
future.expiration_date - datetime.now(tz=timezone.utc)
buy_leg.expiration_date - datetime.now(tz=timezone.utc)
).days
honest_stock_price = (
price
* quotation_to_decimal(future.basic_asset_size)

time_adjusted_sell_price = (
sell_price
* spread.ratio
* (
(1 + Decimal(interest_rate / 100))
(1 + Decimal(self._ir / 100))
** Decimal((days_till_expiration / 365))
)
)
delta = f_price - honest_stock_price
delta = buy_price - time_adjusted_sell_price
if delta < 0:
norm_delta = -delta / quotation_to_decimal(
future.basic_asset_size
)
norm_delta = -delta / spread.ratio
result.append(
DividendSpread(
ticker,
future.ticker,
sell_leg.ticker,
buy_leg.ticker,
round(float(norm_delta), 2),
days_till_expiration,
round(float(norm_delta / price) * 100, 2),
round(float(norm_delta / sell_price) * 100, 2),
can_trade=spread.can_trade,
)
)
ordered = sorted(result, key=lambda s: s.yld, reverse=True)
spreads = [
f'{s.stock_ticker} - {s.future_ticker}: {s.dividend} RUB, {s.yld}%'
f', {s.days_till_expiration} days, {s.can_trade}'
for s in ordered
if s.yld >= PERCENT_THRESHOLD
]
return '\n'.join(spreads)

async def _load_instruments(self) -> None:
shares = await get_api_response('shares')
self._shares = shares.instruments
futures = await get_api_response('futures')
self._futures = futures.instruments

def _shares_prefilter(self, share) -> bool:
return all(
(quotation_to_decimal(share.min_price_increment) > Decimal('0'),)
)

def _futures_prefilter(self, future) -> bool:
return all(
(
quotation_to_decimal(future.min_price_increment)
> Decimal('0'),
future.basic_asset in self._filtered_shares.keys(),
future.last_trade_date > datetime.now(tz=timezone.utc),
)
)

async def _prefilter_instruments(self) -> None:
self._filtered_shares = {
share.ticker: share
for share in self._shares
if self._shares_prefilter(share) is True
}

self._filtered_futures = {}
for future in self._futures:
if not self._futures_prefilter(future):
continue
if future.basic_asset not in self._filtered_futures:
self._filtered_futures[future.basic_asset] = []
self._filtered_futures[future.basic_asset].append(future)

for stock_ticker in self._filtered_futures:
self._filtered_futures[stock_ticker] = sorted(
self._filtered_futures[stock_ticker],
key=lambda d: d.expiration_date,
)[:MAX_FUTURES_AHEAD]
self._filtered_shares = {
ticker: uid
for ticker, uid in self._filtered_shares.items()
if ticker in self._filtered_futures.keys()
}

def _generate_div_spreads(self):
self._div_spreads: list[DivSpread] = []
self._generate_stock_future_spreads()
self._generate_future_future_spreads()

def _generate_future_future_spreads(self):
for futures in self._filtered_futures.values():
amount = len(futures)
if amount <= 1:
continue
for sell_future_pos in range(amount):
for buy_future_pos in range(sell_future_pos + 1, amount):
self._div_spreads.append(
DivSpread(
sell_leg=futures[sell_future_pos],
buy_leg=futures[buy_future_pos],
)
)

def _generate_stock_future_spreads(self):
for ticker, share in self._filtered_shares.items():
for future in self._filtered_futures[ticker]:
self._div_spreads.append(
DivSpread(sell_leg=share, buy_leg=future)
)
ordered = sorted(result, key=lambda s: s.yld, reverse=True)
spreads = [
f'{s.stock_ticker} - {s.future_ticker}: {s.dividend} RUB, {s.yld}%'
f', {s.days_till_expiration} days'
for s in ordered
if s.yld >= percent_threshold
]
return '\n'.join(spreads)

async def _update_orderbooks(self):
self._orderbooks = {}
future_prices, share_prices = {}, {}
futures_to_update = [
d for f in self._filtered_futures.values() for d in f
]
stocks_to_update = list(self._filtered_shares.values())
if not SimpleTradingTime(FutureTradingHours()).is_trading_now:
future_prices = await get_current_prices_by_uid(futures_to_update)
self._orderbooks.update(
{
uid: OrderBook(bid=price, ask=price)
for uid, price in future_prices.items()
}
)
futures_to_update = []
if not SimpleTradingTime(StockTradingHours()).is_trading_now:
share_prices = await get_current_prices_by_uid(stocks_to_update)
self._orderbooks.update(
{
uid: OrderBook(bid=price, ask=price)
for uid, price in share_prices.items()
}
)
stocks_to_update = []
uids_to_update = futures_to_update + stocks_to_update
if uids_to_update:
await self._update_from_orderbooks(uids_to_update)

async def _update_from_orderbooks(self, uids_to_update):
result = {}
async with AsyncRetryingClient(
TCS_RO_TOKEN, RetryClientSettings()
) as client:
for uid in uids_to_update:
if uid not in result.keys():
ob = await client.market_data.get_order_book(
instrument_id=uid, depth=1
)
result[uid] = OrderBook(
bid=quotation_to_decimal(ob.bids[0].price),
ask=quotation_to_decimal(ob.asks[0].price)
)
self._orderbooks.update(result)


async def dividend_scan(command, args):
return await DividendScanner().scan_spreads()


if __name__ == '__main__':
import asyncio

print(asyncio.run(dividend_scan(1, 1)))
7 changes: 6 additions & 1 deletion bot/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from dotenv import load_dotenv
from tinkoff.invest.retrying.settings import RetryClientSettings

CURRENT_INTEREST_RATE = '7.5'
CURRENT_INTEREST_RATE = '12'

# place stops and shorts

Expand All @@ -16,6 +16,11 @@

NUKE_LEVELS = ('0', '1', '5', '7')

# Dividend scanner for futures

MAX_FUTURES_AHEAD = 3
PERCENT_THRESHOLD = 1

# tinkoff settings
SLEEP_PAUSE = 1
RETRY_SETTINGS = RetryClientSettings(use_retry=True, max_retry_attempt=100)
Expand Down
7 changes: 4 additions & 3 deletions bot/tools/get_patch_prepare_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from queue_handler import QUEUE
from settings import (ENDPOINT_HOST, ENDPOINTS, RETRY_SETTINGS, TCS_ACCOUNT_ID,
TCS_RO_TOKEN)
from tinkoff.invest.retrying.aio.client import AsyncRetryingClient
from tinkoff.invest.retrying.sync.client import RetryingClient
from tinkoff.invest.utils import quotation_to_decimal
from tools.adapters import SellBuyToJsonAdapter, SpreadToJsonAdapter
Expand Down Expand Up @@ -90,10 +91,10 @@ def get_current_prices(assets):
return assets


def get_current_prices_by_uid(assets):
async def get_current_prices_by_uid(assets):
uids = [asset.uid for asset in assets]
with RetryingClient(TCS_RO_TOKEN, RETRY_SETTINGS) as client:
response = client.market_data.get_last_prices(instrument_id=uids)
async with AsyncRetryingClient(TCS_RO_TOKEN, RETRY_SETTINGS) as client:
response = await client.market_data.get_last_prices(instrument_id=uids)
return {
item.instrument_uid: quotation_to_decimal(item.price)
for item in response.last_prices
Expand Down
Loading

0 comments on commit 7ea1b05

Please sign in to comment.