Skip to content

Commit

Permalink
dividend scanned beta
Browse files Browse the repository at this point in the history
  • Loading branch information
holohup committed Aug 19, 2023
1 parent 28027fb commit 2250660
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 12 deletions.
5 changes: 3 additions & 2 deletions bot/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from instant_commands import (check_health, get_current_spread_prices, help,
test)
from place_stops import place_stops, process_nuke_command
from scanner.scanner import scan
from scanner.scanner import dividend_scan, scan
from sellbuy import sellbuy
from spreads import spreads
from stop_orders import restore_stops, save_stops
Expand Down Expand Up @@ -60,7 +60,8 @@ async def tasks(*args, **kwargs):
'sell': ('Sell ASAP', sellbuy),
'buy': ('Buy ASAP', sellbuy),
'dump': ('Dump it', sellbuy),
'scan': ('Spread scanner', scan)
'scan': ('Spread scanner', scan),
'dscan': ('Dividend scanned', dividend_scan)
}


Expand Down
99 changes: 96 additions & 3 deletions bot/scanner/scanner.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from typing import Union
from typing import NamedTuple, Union

from settings import CURRENT_INTEREST_RATE, TCS_RO_TOKEN
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
from tools.get_patch_prepare_data import (get_current_prices,
get_current_prices_by_uid)


@dataclass
Expand Down Expand Up @@ -47,6 +48,14 @@ class YieldingSpread:
days: int = 0


class DividendSpread(NamedTuple):
stock_ticker: str
future_ticker: str
dividend: float
days_till_expiration: int
yld: float


class SpreadScanner:
def __init__(self, rate: str = '') -> None:
if rate and not rate.isnumeric():
Expand Down Expand Up @@ -101,7 +110,8 @@ def _build_stock_and_future_lists(self):
self._stocks.append(StockData(instrument.figi, instrument.ticker))
for instrument in self._responses['futures']:
if instrument.basic_asset in self._futures_with_counterparts and (
instrument.asset_type == 'TYPE_SECURITY'
instrument.asset_type
== 'TYPE_SECURITY'
# or instrument.asset_type == 'TYPE_COMMODITY'
):
self._futures.append(
Expand Down Expand Up @@ -320,3 +330,86 @@ async def get_api_response(instrument: str):
RetryClientSettings(use_retry=True, max_retry_attempt=10),
) as client:
return await getattr(client.instruments, instrument)()


async def dividend_scan():
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]
days_till_expiration = (
future.expiration_date - datetime.now(tz=timezone.utc)
).days
honest_stock_price = (
price
* quotation_to_decimal(future.basic_asset_size)
* (
(1 + Decimal(interest_rate / 100))
** Decimal((days_till_expiration / 365))
)
)
delta = f_price - honest_stock_price
if delta < 0:
norm_delta = -delta / quotation_to_decimal(
future.basic_asset_size
)
result.append(
DividendSpread(
ticker,
future.ticker,
round(float(norm_delta), 2),
days_till_expiration,
round(float(norm_delta / price) * 100, 2),
)
)
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)
10 changes: 10 additions & 0 deletions bot/tools/get_patch_prepare_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ def get_current_prices(assets):
return assets


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)
return {
item.instrument_uid: quotation_to_decimal(item.price)
for item in response.last_prices
}


def get_portfolio_positions():
with RetryingClient(TCS_RO_TOKEN, RETRY_SETTINGS) as client:
response = client.operations.get_portfolio(account_id=TCS_ACCOUNT_ID)
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ exclude =
conftest.py
per-file-ignores =
*/settings.py:E501
max-complexity = 5
max-complexity = 8
8 changes: 4 additions & 4 deletions tests/bot_tests/spread_yield_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def futures_response():
2022, 9, 1, 20, 59, 59, tzinfo=datetime.timezone.utc
),
last_trade_date=datetime.datetime(
2023, 6, 15, 20, 59, 59, tzinfo=datetime.timezone.utc
2033, 6, 15, 20, 59, 59, tzinfo=datetime.timezone.utc
),
futures_type='DELIVERY_TYPE_PHYSICAL_DELIVERY',
asset_type='TYPE_SECURITY',
Expand All @@ -43,7 +43,7 @@ def futures_response():
country_of_risk_name='Российская Федерация',
sector='SECTOR_TELECOM',
expiration_date=datetime.datetime(
2023, 6, 16, 0, 0, tzinfo=datetime.timezone.utc
2033, 6, 16, 0, 0, tzinfo=datetime.timezone.utc
),
trading_status=STS.SECURITY_TRADING_STATUS_NORMAL_TRADING,
otc_flag=False,
Expand Down Expand Up @@ -190,7 +190,7 @@ def futures_response():
country_of_risk_name='Российская Федерация',
sector='SECTOR_MATERIALS',
expiration_date=datetime.datetime(
2022, 12, 16, 0, 0, tzinfo=datetime.timezone.utc
2033, 12, 16, 0, 0, tzinfo=datetime.timezone.utc
),
trading_status=STS.SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING,
otc_flag=False,
Expand Down Expand Up @@ -239,7 +239,7 @@ def futures_response():
country_of_risk_name='Российская Федерация',
sector='SECTOR_CONSUMER',
expiration_date=datetime.datetime(
2023, 3, 17, 0, 0, tzinfo=datetime.timezone.utc
2033, 3, 17, 0, 0, tzinfo=datetime.timezone.utc
),
trading_status=STS.SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING,
otc_flag=False,
Expand Down
4 changes: 2 additions & 2 deletions trademan/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
django==2.2.19
tinkoff-investments==0.2.0b36
tinkoff-investments==0.2.0b54
python-dotenv==0.21.0
protobuf==3.20.1
protobuf==3.20.2
djangorestframework==3.12.4
pytest-django==4.5.2

0 comments on commit 2250660

Please sign in to comment.