Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
wiseplat
GitHub Repository: wiseplat/python-code
Path: blob/master/ invest-robot-contest_tinvest_robot-master/tinvest_robot_perevalov/trader.py
5933 views
1
import os
2
from uuid import uuid4
3
4
from tinkoff.invest import Client
5
from tinkoff.invest import schemas
6
7
from tinvest_robot_perevalov import _db
8
from tinvest_robot_perevalov._config import _IS_SANDBOX, app_name, USD_FIGI, CLASS_CODE_SPB, FEE
9
10
import logging
11
12
logging.basicConfig(
13
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
14
)
15
16
logger = logging.getLogger(__name__)
17
18
19
20
def _quotation_to_float(q: schemas.Quotation) -> float:
21
"""Convert quotation to float
22
23
Args:
24
q (schemas.Quotation): Quotation value to convert
25
26
Returns:
27
float: converted float number
28
"""
29
return float(str(q.units) + '.' + str(q.nano))
30
31
32
def _float_to_quotation(f: float) -> schemas.Quotation:
33
"""Convert float to quotation
34
35
Args:
36
f (float): Float number to convert
37
38
Returns:
39
schemas.Quotation: converted quotation number
40
"""
41
return schemas.Quotation(units=int(str(f).split('.')[0]), nano=int(str(f).split('.')[1][:10]))
42
43
44
def _post_order(figi: str, quantity: int, price: schemas.Quotation, direction: schemas.OrderDirection, account_id: str, order_type: schemas.OrderType, news_id: int) -> schemas.PostOrderResponse:
45
"""Posts order to Tinkoff API
46
47
Args:
48
figi (str): FIGI of share
49
quantity (int): quantity of share
50
price (schemas.Quotation): price of share
51
direction (schemas.OrderDirection): order direction (SELL or BUY)
52
account_id (str): account ID for order
53
order_type (schemas.OrderType): type of order (MARKET or LIMIT)
54
news_id (int): ID of news which caused order
55
56
Returns:
57
schemas.PostOrderResponse: response from Tinkoff API or None
58
"""
59
60
with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:
61
order_id = str(uuid4())
62
if _IS_SANDBOX:
63
response = client.sandbox.post_sandbox_order(figi=figi, quantity=quantity, direction=direction, account_id=account_id, order_type=order_type, order_id=order_id)
64
else:
65
response = client.orders.post_order(figi=figi, quantity=quantity, direction=direction, account_id=account_id, order_type=order_type, order_id=order_id)
66
_db.put_order_in_db(figi=figi, quantity=quantity, price=_quotation_to_float(response.initial_order_price_pt), direction=int(direction), account_id=account_id, order_type=int(order_type), order_id=order_id, news_id=news_id)
67
return response
68
69
70
def _get_shares_by(tickers: list) -> list:
71
"""Get list of shares by tickers
72
73
Args:
74
tickers (list): Tickers of shares
75
76
Returns:
77
list: share objects
78
"""
79
shares = list()
80
with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:
81
for ticker in tickers:
82
try:
83
share = client.instruments.share_by(id_type=schemas.InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER, class_code=CLASS_CODE_SPB, id=ticker).instrument
84
shares.append(share)
85
except Exception as e:
86
logger.error(str(e))
87
return shares
88
89
90
def _get_position_by(positions: list, figi: str) -> schemas.PortfolioPosition:
91
"""Searches for position by its figi
92
93
Args:
94
positions (list): list of positions (or any assets)
95
figi (str): figi of position to search for
96
97
Returns:
98
schemas.PortfolioPosition: position object or None
99
"""
100
for p in positions:
101
if p.figi == figi:
102
return p
103
return None
104
105
106
def _get_best_price(share: object, direction: object) -> float:
107
"""Get best price for share from order book (for BUY orders take the first ask, for SELL orders take the first bid)
108
109
Args:
110
share (object): share or asset to get price for
111
direction (object): direction of order (BUY or SELL)
112
113
Returns:
114
float: best price for share or 0.0 if no orders in order book
115
"""
116
try:
117
with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:
118
order_book = client.market_data.get_order_book(figi=share.figi, depth=1)
119
if direction == schemas.OrderDirection.ORDER_DIRECTION_BUY:
120
return _quotation_to_float(order_book.asks[0].price)
121
elif direction == schemas.OrderDirection.ORDER_DIRECTION_SELL:
122
return _quotation_to_float(order_book.bids[0].price)
123
except Exception as e:
124
logger.error(str(e) + " Maybe order book is empty")
125
return 0.0
126
127
def _get_balance(account_id: str, figi: str = USD_FIGI) -> float:
128
"""Get balance for account according to FIGI
129
130
Args:
131
account_id (str): account id to get balance for
132
figi (str, optional): FIGI for getting balance. Defaults to USD_FIGI.
133
134
Returns:
135
float: float number of balance
136
"""
137
with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:
138
if _IS_SANDBOX:
139
portfolio = client.sandbox.get_sandbox_portfolio(account_id=account_id)
140
position = _get_position_by(portfolio.positions, USD_FIGI)
141
return _quotation_to_float(position.quantity)
142
else:
143
portfolio = client.operations.get_portfolio(account_id=account_id)
144
position = _get_position_by(portfolio.positions, USD_FIGI)
145
return _quotation_to_float(position.quantity)
146
147
148
def _get_position_from_account(share: object, account_id: str) -> schemas.PortfolioPosition:
149
"""Get position from account
150
151
Args:
152
share (object): a particular share to get position for
153
account_id (str): Account ID to get position for
154
155
Returns:
156
float: found position or None
157
"""
158
with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:
159
if _IS_SANDBOX:
160
portfolio = client.sandbox.get_sandbox_portfolio(account_id=account_id)
161
position = _get_position_by(portfolio.positions, share.figi)
162
return position
163
else:
164
portfolio = client.operations.get_portfolio(account_id=account_id)
165
position = _get_position_by(portfolio.positions, share.figi)
166
return position
167
168
169
def _get_current_order(share: object, account_id: str) -> schemas.Order:
170
"""Get current order for share from account
171
172
Args:
173
share (object): A particular share to get current order for
174
account_id (str): An account ID to get current order for
175
176
Returns:
177
schemas.Order: an order object or None
178
"""
179
with Client(token=os.environ["TINVEST_TOKEN"], app_name=app_name) as client:
180
if _IS_SANDBOX:
181
orders = client.sandbox.get_sandbox_orders(account_id=account_id)
182
order = _get_position_by(orders.orders, share.figi)
183
return order
184
else:
185
orders = client.orders.get_orders(account_id=account_id)
186
order = _get_position_by(orders.orders, share.figi)
187
return order
188
189
190
def _handle_match(sentiment: str, share: object, account_id: str, news_id: int, quantity: int = 1) -> schemas.PostOrderResponse:
191
"""Handles occurence of an asset name in the news according to sentiment
192
193
Args:
194
sentiment (str): Sentiment class of the news
195
share (object): A particular share to handle match for
196
account_id (str): An account ID to handle match for
197
news_id (int): ID of the news which triggered the match
198
quantity (int, optional): A quantity of shares to buy (Selling all always). Defaults to 1.
199
200
Returns:
201
schemas.PostOrderResponse: Response from Tinkoff API or None if no order placed
202
"""
203
204
logger.info("Handling match for {}".format(share.name))
205
206
response = None
207
order = _get_current_order(share, account_id)
208
if order:
209
logger.warning("There is already an order for {}".format(share.name))
210
return response
211
212
if sentiment == 'positive':
213
balance = _get_balance(account_id)
214
price = _get_best_price(share, schemas.OrderDirection.ORDER_DIRECTION_BUY)
215
216
if price == 0.0: # no orders in order book
217
return response
218
219
if balance*(1 + FEE) > price: # if we have enough money to buy
220
response = _post_order(share.figi, 1, _float_to_quotation(price), schemas.OrderDirection.ORDER_DIRECTION_BUY, account_id, schemas.OrderType.ORDER_TYPE_MARKET, news_id)
221
222
elif sentiment == 'negative':
223
position = _get_position_from_account(share, account_id)
224
if position: # if we have a position in our portfolio
225
price = _get_best_price(share, schemas.OrderDirection.ORDER_DIRECTION_SELL)
226
227
if price == 0.0: # no orders in order book
228
return response
229
230
response = _post_order(share.figi, quantity, _float_to_quotation(price), schemas.OrderDirection.ORDER_DIRECTION_SELL, account_id, schemas.OrderType.ORDER_TYPE_MARKET, news_id)
231
232
return response
233
234
235
def trade(tickers: list, account_id: str):
236
"""Main function for searching occurrences of assets in the news and trading them
237
238
Args:
239
tickers (list): Tickers to search for
240
account_id (str): Account ID to trade with
241
"""
242
news = _db.select_not_checked()
243
shares = _get_shares_by(tickers)
244
245
for n in news:
246
for share in shares:
247
if share.name.lower() in n['title'].lower(): # just dummy check, should be more accurate
248
try:
249
_handle_match(sentiment=n['sentiment'], share=share, account_id=account_id, news_id=n['news_id'])
250
except Exception as e:
251
logger.error(str(e))
252
_db.update_is_checked(n['news_id'])
253
254
255