bn-quik 0.1.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bn_quik-0.1.0 → bn_quik-0.2.1}/PKG-INFO +12 -3
- bn_quik-0.2.1/README.md +11 -0
- {bn_quik-0.1.0 → bn_quik-0.2.1}/bn_quik/QuikBroker.py +167 -75
- {bn_quik-0.1.0 → bn_quik-0.2.1}/bn_quik/QuikData.py +2 -2
- {bn_quik-0.1.0 → bn_quik-0.2.1}/bn_quik/QuikStore.py +63 -66
- {bn_quik-0.1.0 → bn_quik-0.2.1}/pyproject.toml +2 -2
- bn_quik-0.2.1/requirements.txt +2 -0
- bn_quik-0.1.0/README.md +0 -2
- bn_quik-0.1.0/requirements.txt +0 -2
- {bn_quik-0.1.0 → bn_quik-0.2.1}/.gitignore +0 -0
- {bn_quik-0.1.0 → bn_quik-0.2.1}/LICENSE +0 -0
- {bn_quik-0.1.0 → bn_quik-0.2.1}/bn_quik/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bn_quik
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Backtrader-next connector for QUIK terminal
|
|
5
5
|
Project-URL: Homepage, https://github.com/Alex-Shur/bn_quik
|
|
6
6
|
Project-URL: Source, https://github.com/Alex-Shur/bn_quik
|
|
@@ -12,9 +12,18 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Requires-Python: >=3.11
|
|
15
|
-
Requires-Dist: backtrader-next>=2.
|
|
15
|
+
Requires-Dist: backtrader-next>=2.3.2
|
|
16
16
|
Requires-Dist: quik-python>=1.2.0
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
<div align="center">
|
|
20
|
+
|
|
21
|
+
# bn_quik
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org/project/bn_quik/)
|
|
24
|
+
[](https://pypistats.org/packages/bn_quik)
|
|
25
|
+
[](https://python.org "Go to Python homepage")
|
|
26
|
+
[](https://github.com/Alex-Shur/bn_quik/blob/master/LICENSE)
|
|
27
|
+
</div>
|
|
28
|
+
|
|
20
29
|
Live trading intergartion of backtrader-next with QUIK trade terminal
|
bn_quik-0.2.1/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# bn_quik
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/bn_quik/)
|
|
6
|
+
[](https://pypistats.org/packages/bn_quik)
|
|
7
|
+
[](https://python.org "Go to Python homepage")
|
|
8
|
+
[](https://github.com/Alex-Shur/bn_quik/blob/master/LICENSE)
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
Live trading intergartion of backtrader-next with QUIK trade terminal
|
|
@@ -16,7 +16,8 @@ from quik_python.data_structures import Transaction, Trade, TransactionReply
|
|
|
16
16
|
from quik_python.data_structures.money_limit_ex import MoneyLimitEx
|
|
17
17
|
from quik_python.data_structures.order import OrderTradeFlags, State
|
|
18
18
|
from quik_python.data_structures.portfolio_info_ex import PortfolioInfoEx
|
|
19
|
-
from quik_python.data_structures.stop_order import StopOrder
|
|
19
|
+
from quik_python.data_structures.stop_order import StopOrder as QuikStopOrder
|
|
20
|
+
from quik_python.data_structures.order import Order as QuikOrder
|
|
20
21
|
from quik_python.data_structures.transaction_types import TransactionAction, TransactionOperation, TransactionType
|
|
21
22
|
|
|
22
23
|
from .QuikStore import QuikStore, Account
|
|
@@ -32,11 +33,6 @@ class MetaQuikBroker(BrokerBase.__class__):
|
|
|
32
33
|
# noinspection PyProtectedMember,PyArgumentList
|
|
33
34
|
class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
34
35
|
|
|
35
|
-
params = (
|
|
36
|
-
('client_code_for_orders', None), # Номер торгового терминала. У брокера Финам требуется для совершения торговых операций
|
|
37
|
-
('trade_account_id', None),
|
|
38
|
-
)
|
|
39
|
-
|
|
40
36
|
account:Account = None
|
|
41
37
|
|
|
42
38
|
def __init__(self, **kwargs):
|
|
@@ -68,12 +64,12 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
68
64
|
async def __get_account(self):
|
|
69
65
|
if not self.account:
|
|
70
66
|
acc_list = await self.store.get_accounts()
|
|
71
|
-
self.account = next((a for a in acc_list if a.trade_account_id == self.p.trade_account_id), None)
|
|
67
|
+
self.account = next((a for a in acc_list if a.trade_account_id == self.store.p.trade_account_id), None)
|
|
72
68
|
# Если для заявок брокер устанавливает отдельный код клиента, то задаем его в параметре client_code_for_orders
|
|
73
69
|
# В остальных случаях получаем код клиента из заявки (счета). Для фьючерсов кода клиента нет
|
|
74
70
|
if self.account:
|
|
75
|
-
self.account.order_client_code = self.p.client_code_for_orders if self.p.client_code_for_orders else self.account.client_code
|
|
76
|
-
self.account.is_ucp = await self.
|
|
71
|
+
self.account.order_client_code = self.store.p.client_code_for_orders if self.store.p.client_code_for_orders else self.account.client_code
|
|
72
|
+
self.account.is_ucp = await self._is_ucp_client(self.account.firm_id, self.account.client_code)
|
|
77
73
|
self.logger.info(f"Account {self.account.trade_account_id} is UCP: {self.account.is_ucp}")
|
|
78
74
|
return self.account
|
|
79
75
|
|
|
@@ -85,7 +81,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
85
81
|
self.store.broker = self
|
|
86
82
|
self.account = await self.__get_account()
|
|
87
83
|
if not self.account:
|
|
88
|
-
raise ValueError(f'QuikBroker: Не найден счет с trade_account_id={self.p.trade_account_id}')
|
|
84
|
+
raise ValueError(f'QuikBroker: Не найден счет с trade_account_id={self.store.p.trade_account_id}')
|
|
89
85
|
await self._get_all_active_positions()
|
|
90
86
|
self.cash = await self._getcash()
|
|
91
87
|
self.value = await self._getvalue(None)
|
|
@@ -93,7 +89,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
93
89
|
|
|
94
90
|
def start(self):
|
|
95
91
|
super(QuikBroker, self).start()
|
|
96
|
-
|
|
92
|
+
QuikStore.run_sync(self.__start_async())
|
|
97
93
|
|
|
98
94
|
def stop(self):
|
|
99
95
|
super(QuikBroker, self).stop()
|
|
@@ -118,7 +114,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
118
114
|
with self._lock_cash:
|
|
119
115
|
return self.value + self.cash
|
|
120
116
|
else:
|
|
121
|
-
return
|
|
117
|
+
return QuikStore.run_sync(self._getvalue(datas))
|
|
122
118
|
|
|
123
119
|
def getposition(self, data):
|
|
124
120
|
"""Позиция по тикеру
|
|
@@ -129,19 +125,19 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
129
125
|
|
|
130
126
|
def buy(self, owner, data, size, price=None, plimit=None, exectype=None, valid=None, tradeid=0, oco=None, trailamount=None, trailpercent=None, parent=None, transmit=True, **kwargs):
|
|
131
127
|
"""Заявка на покупку"""
|
|
132
|
-
order =
|
|
128
|
+
order = QuikStore.run_sync(self.create_order(owner, data, size, price, plimit, exectype, valid, oco, parent, transmit, True, **kwargs))
|
|
133
129
|
self.notifs.append(order.clone()) # Уведомляем брокера об отправке новой заявки на покупку на биржу
|
|
134
130
|
return order
|
|
135
131
|
|
|
136
132
|
def sell(self, owner, data, size, price=None, plimit=None, exectype=None, valid=None, tradeid=0, oco=None, trailamount=None, trailpercent=None, parent=None, transmit=True, **kwargs):
|
|
137
133
|
"""Заявка на продажу"""
|
|
138
|
-
order =
|
|
134
|
+
order = QuikStore.run_sync(self.create_order(owner, data, size, price, plimit, exectype, valid, oco, parent, transmit, False, **kwargs))
|
|
139
135
|
self.notifs.append(order.clone()) # Уведомляем брокера об отправке новой заявки на продажу на биржу
|
|
140
136
|
return order
|
|
141
137
|
|
|
142
138
|
def cancel(self, order):
|
|
143
139
|
"""Отмена заявки"""
|
|
144
|
-
return
|
|
140
|
+
return QuikStore.run_sync(self.cancel_order(order))
|
|
145
141
|
|
|
146
142
|
def get_notification(self):
|
|
147
143
|
with self._lock_notifs:
|
|
@@ -174,7 +170,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
174
170
|
return order
|
|
175
171
|
|
|
176
172
|
order.addinfo(account=self.account) # Передаем в заявку счет
|
|
177
|
-
si = await self.store.
|
|
173
|
+
si = await self.store.get_ticker_info(class_code, sec_code)
|
|
178
174
|
if not si:
|
|
179
175
|
self.logger.error('create_order: Постановка заявки %s по тикеру %s.%s отменена. Тикер не найден', order.ref, class_code, sec_code)
|
|
180
176
|
order.reject(self)
|
|
@@ -211,12 +207,12 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
211
207
|
sec_code = order.data.sec_code
|
|
212
208
|
quantity = abs(order.size)
|
|
213
209
|
if self.store.p.lots:
|
|
214
|
-
quantity = await self.
|
|
215
|
-
order.size = math.copysign(await self.
|
|
210
|
+
quantity = await self._size_to_lots(class_code, sec_code, quantity)
|
|
211
|
+
order.size = math.copysign(await self._lots_to_size(class_code, sec_code, quantity), order.size)
|
|
216
212
|
|
|
217
213
|
trans_id = int(time.time() * 1000) % 100000000 # time in milliseconds
|
|
218
|
-
order.
|
|
219
|
-
order.
|
|
214
|
+
order.addinfo(trans_id = trans_id)
|
|
215
|
+
order.addinfo(data_id = order.data.data_id)
|
|
220
216
|
|
|
221
217
|
transaction = Transaction()
|
|
222
218
|
transaction.TRANS_ID = trans_id
|
|
@@ -236,10 +232,10 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
236
232
|
if order.exectype == Order.Market:
|
|
237
233
|
transaction.TYPE = TransactionType.M
|
|
238
234
|
if order.data.derivative: # Для деривативов
|
|
239
|
-
last_price = float(await self.store.
|
|
235
|
+
last_price = float(await self.store.get_last_price(class_code, sec_code))
|
|
240
236
|
# Из документации QUIK: При покупке/продаже фьючерсов по рынку нужно ставить цену хуже последней сделки
|
|
241
237
|
last_price = last_price + slippage if order.isbuy() else last_price - slippage
|
|
242
|
-
market_price = await self.store.
|
|
238
|
+
market_price = await self.store.price_to_valid_price(class_code, sec_code, last_price)
|
|
243
239
|
else:
|
|
244
240
|
market_price = Decimal(0) # Цена рыночной заявки должна быть нулевой
|
|
245
241
|
transaction.PRICE = market_price # Рыночную цену QUIK ставим в заявку
|
|
@@ -247,25 +243,25 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
247
243
|
# Лимитная заявка
|
|
248
244
|
elif order.exectype == Order.Limit:
|
|
249
245
|
transaction.TYPE = TransactionType.L # Лимитная заявка
|
|
250
|
-
limit_price = await self.store.
|
|
246
|
+
limit_price = await self.store.price_to_valid_price(class_code, sec_code, order.price)
|
|
251
247
|
transaction.PRICE = limit_price # Лимитную цену QUIK Ставим в заявку
|
|
252
248
|
|
|
253
249
|
# Стоп заявка
|
|
254
250
|
elif order.exectype == Order.Stop:
|
|
255
|
-
stop_price = await self.store.
|
|
251
|
+
stop_price = await self.store.price_to_valid_price(class_code, sec_code, order.price)
|
|
256
252
|
transaction.STOPPRICE = stop_price # Стоп цену QUIK ставим в заявкуСтавим в заявку
|
|
257
253
|
if order.data.derivative: # Для деривативов
|
|
258
254
|
market_price = stop_price + slippage if order.isbuy() else stop_price - slippage
|
|
259
|
-
market_price = await self.store.
|
|
255
|
+
market_price = await self.store.price_to_valid_price(class_code, sec_code, market_price) # Из документации QUIK: При покупке/продаже фьючерсов по рынку нужно ставить цену хуже последней сделки
|
|
260
256
|
else: # Для остальных рынков
|
|
261
257
|
market_price = Decimal(0) # Цена рыночной заявки должна быть нулевой
|
|
262
258
|
transaction.PRICE = market_price # Рыночную цену QUIK ставим в заявку
|
|
263
259
|
|
|
264
260
|
# Стоп-лимитная заявка
|
|
265
261
|
elif order.exectype == Order.StopLimit:
|
|
266
|
-
stop_price = await self.store.
|
|
262
|
+
stop_price = await self.store.price_to_valid_price(class_code, sec_code, order.price)
|
|
267
263
|
transaction.STOPPRICE = stop_price # Стоп цену QUIK ставим в заявку
|
|
268
|
-
limit_price = await self.store.
|
|
264
|
+
limit_price = await self.store.price_to_valid_price(class_code, sec_code, order.pricelimit)
|
|
269
265
|
transaction.PRICE = limit_price # Лимитную цену QUIK Ставим в заявку
|
|
270
266
|
|
|
271
267
|
# Для стоп заявок
|
|
@@ -281,7 +277,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
281
277
|
with self._lock_orders:
|
|
282
278
|
self.orders[trans_id] = order # Сохраняем заявку в списке заявок, отправленных на биржу
|
|
283
279
|
self._save_broker_state()
|
|
284
|
-
trans_id = await self.store.
|
|
280
|
+
trans_id = await self.store.send_transaction(transaction)
|
|
285
281
|
order.submit(self) # Отправляем заявку на биржу (Order.Submitted)
|
|
286
282
|
self._save_broker_state()
|
|
287
283
|
if trans_id < 0: # Если возникла ошибка при постановке заявки на уровне QUIK
|
|
@@ -303,8 +299,9 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
303
299
|
return None
|
|
304
300
|
|
|
305
301
|
order_num = order.info['order_num']
|
|
302
|
+
linked_order = order.info.get('linked_order', 0)
|
|
306
303
|
is_stop = order.exectype in [Order.Stop, Order.StopLimit]
|
|
307
|
-
is_stop_order = is_stop and await self.store.
|
|
304
|
+
is_stop_order = is_stop and await self.store.get_order_by_number(order_num) is None
|
|
308
305
|
|
|
309
306
|
transaction = Transaction()
|
|
310
307
|
transaction.TRANS_ID = trans_id
|
|
@@ -312,14 +309,18 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
312
309
|
transaction.SECCODE = order.data.sec_code
|
|
313
310
|
|
|
314
311
|
if is_stop_order: # Для стоп заявки
|
|
315
|
-
|
|
316
|
-
|
|
312
|
+
if linked_order== 0: # Если нет связанной лимитной заявки, то отменяем стоп-заявку
|
|
313
|
+
transaction.ACTION = TransactionAction.KILL_STOP_ORDER
|
|
314
|
+
transaction.STOP_ORDER_KEY = str(order_num)
|
|
315
|
+
else: # Если есть связанная лимитная заявка, то отменяем лимитную заявку
|
|
316
|
+
transaction.ACTION = TransactionAction.KILL_ORDER
|
|
317
|
+
transaction.ORDER_KEY = str(linked_order)
|
|
317
318
|
else: # Для лимитной заявки
|
|
318
319
|
transaction.ACTION = TransactionAction.KILL_ORDER
|
|
319
320
|
transaction.ORDER_KEY = str(order_num)
|
|
320
321
|
order.addinfo(op='cancel')
|
|
321
322
|
self._save_broker_state()
|
|
322
|
-
await self.store.
|
|
323
|
+
await self.store.send_transaction(transaction)
|
|
323
324
|
return order
|
|
324
325
|
|
|
325
326
|
async def oco_pc_check(self, order):
|
|
@@ -346,7 +347,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
346
347
|
await self.cancel_order(child) # Отменяем дочернюю заявку
|
|
347
348
|
|
|
348
349
|
|
|
349
|
-
async def
|
|
350
|
+
async def on_trans_reply(self, data: TransactionReply):
|
|
350
351
|
"""Обработчик события ответа на транзакцию пользователя"""
|
|
351
352
|
self.logger.debug('on_trans_reply: data=%s', str(data))
|
|
352
353
|
qk_trans_reply = data
|
|
@@ -366,11 +367,12 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
366
367
|
order_op = order.info["op"]
|
|
367
368
|
if order_op == 'new':
|
|
368
369
|
self.logger.debug('on_trans_reply: Заявка %s переведена в статус принята на бирже (Order.Accepted)', order.ref)
|
|
369
|
-
|
|
370
|
+
with self.store.lock_store_data:
|
|
371
|
+
order.accept(self) # Заявка принята на бирже (Order.Accepted)
|
|
370
372
|
order.addinfo(op='done')
|
|
371
373
|
elif order_op == 'cancel':
|
|
372
374
|
self.logger.debug('on_trans_reply: Заявка %s переведена в статус отменена (Order.Canceled)', order.ref)
|
|
373
|
-
with self.store.
|
|
375
|
+
with self.store.lock_store_data:
|
|
374
376
|
order.cancel() # Отменяем существующую заявку (Order.Canceled)
|
|
375
377
|
order.addinfo(op='done')
|
|
376
378
|
elif status in (2, 4, 5, 10, 11, 12, 13, 14, 16): # Транзакция не выполнена (ошибка заявки):
|
|
@@ -384,7 +386,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
384
386
|
return # то заявку не отменяем, выходим, дальше не продолжаем
|
|
385
387
|
try:
|
|
386
388
|
self.logger.debug('on_trans_reply: Заявка %s переведена в статус отклонена (Order.Rejected)', order.ref)
|
|
387
|
-
with self.store.
|
|
389
|
+
with self.store.lock_store_data:
|
|
388
390
|
order.reject(self) # Отклоняем заявку (Order.Rejected)
|
|
389
391
|
except (KeyError, IndexError) as e:
|
|
390
392
|
self.logger.error('on_trans_reply: Exception for change order.status: %s', e)
|
|
@@ -393,7 +395,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
393
395
|
elif status == 6: # Транзакция не прошла проверку лимитов сервера QUIK
|
|
394
396
|
try:
|
|
395
397
|
self.logger.debug('on_trans_reply: Заявка %s переведена в статус не прошла проверку лимитов (Order.Margin)', order.ref)
|
|
396
|
-
with self.store.
|
|
398
|
+
with self.store.lock_store_data:
|
|
397
399
|
order.margin() # Для заявки не хватает средств (Order.Margin)
|
|
398
400
|
except (KeyError, IndexError) as e:
|
|
399
401
|
self.logger.error('on_trans_reply: Exception for change order.status: %s', e)
|
|
@@ -408,7 +410,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
408
410
|
self.logger.debug('on_trans_reply: Заявка %s. Выход', order.ref)
|
|
409
411
|
|
|
410
412
|
|
|
411
|
-
async def
|
|
413
|
+
async def on_trade(self, data: Trade):
|
|
412
414
|
"""Обработчик события получения новой / изменения существующей сделки.
|
|
413
415
|
Выполняется до события изменения существующей заявки. Нужен для определения цены исполнения заявок.
|
|
414
416
|
"""
|
|
@@ -427,7 +429,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
427
429
|
self.logger.debug('on_trade: Заявка %s с номером %s. Номер транзакции %s. Номер сделки %s order=%s', order.ref, order_num, trans_id, trade_num, order)
|
|
428
430
|
class_code = qk_trade.class_code # Код режима торгов
|
|
429
431
|
sec_code = qk_trade.sec_code # Код тикера
|
|
430
|
-
dataname = self.store.
|
|
432
|
+
dataname = self.store.get_ticker_name(class_code, sec_code)
|
|
431
433
|
# Защита от дублей сделок (критичная секция - check-then-act)
|
|
432
434
|
# Используем asyncio.Lock для оптимальной работы в async контексте
|
|
433
435
|
async with self._lock_trades:
|
|
@@ -439,7 +441,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
439
441
|
|
|
440
442
|
size = qk_trade.qty # Абсолютное кол-во
|
|
441
443
|
if self.store.p.lots: # Если входящий остаток в лотах
|
|
442
|
-
size = await self.
|
|
444
|
+
size = await self._lots_to_size(class_code, sec_code, size) # то переводим кол-во из лотов в штуки
|
|
443
445
|
if qk_trade.flags & OrderTradeFlags.IS_SELL.value:
|
|
444
446
|
size *= -1 # Продажа - кол-во ставим отрицательным
|
|
445
447
|
price = float(qk_trade.price) # Цена сделки
|
|
@@ -474,16 +476,52 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
474
476
|
self.value = await self._getvalue(None)
|
|
475
477
|
|
|
476
478
|
|
|
477
|
-
async def
|
|
479
|
+
async def on_order(self, order : QuikOrder):
|
|
478
480
|
self.logger.debug('on_order trans_id=%s order_num=%s ext_status=%s flags=%s state=%s', order.trans_id, order.order_num, order.ext_order_status, order.flags, order.state)
|
|
481
|
+
trans_id = order.trans_id
|
|
482
|
+
with self._lock_orders:
|
|
483
|
+
if trans_id not in self.orders:
|
|
484
|
+
self.logger.debug('on_order: Заявка с номером %s. Номер транзакции %s. Заявка была выставлена не из торговой системы. Выход', order.order_num, trans_id)
|
|
485
|
+
return
|
|
486
|
+
order: Order = self.orders[trans_id]
|
|
487
|
+
if order.exectype in (Order.Stop, Order.StopLimit):
|
|
488
|
+
linked_order = order.info.get('linked_order', 0)
|
|
489
|
+
if linked_order != 0 and order.order_num == linked_order:
|
|
490
|
+
self.logger.debug('on_order: Заявка %s является стоп-заявкой', order.ref)
|
|
491
|
+
notifs = False
|
|
492
|
+
if order.state == State.CANCELED:
|
|
493
|
+
self.logger.debug('on_order: Linked Заявка %s переведена в статус отклонена (Order.Rejected)', order.ref)
|
|
494
|
+
with self.store.lock_store_data:
|
|
495
|
+
order.reject(self) # Отклоняем заявку (Order.Rejected)
|
|
496
|
+
notifs = True
|
|
497
|
+
elif order.state == State.COMPLETED:
|
|
498
|
+
self.logger.debug('on_order: Linked Заявка %s исполнена (Order.Сompleted)', order.ref)
|
|
499
|
+
with self.store.lock_store_data:
|
|
500
|
+
order.completed(self) # Переводим заявку в статус Order.Completed
|
|
501
|
+
notifs = True
|
|
502
|
+
self._save_broker_state()
|
|
503
|
+
if notifs:
|
|
504
|
+
with self._lock_notifs:
|
|
505
|
+
self.notifs.append(order.clone()) # Уведомляем брокера о заявке
|
|
479
506
|
|
|
480
|
-
async def
|
|
507
|
+
async def on_stop_order(self, stop_order:QuikStopOrder):
|
|
481
508
|
self.logger.debug('on_stop_order trans_id=%s order_num=%s linked_order=%s flags=%s state=%s', stop_order.trans_id, stop_order.order_num, stop_order.linked_order, stop_order.flags, stop_order.state)
|
|
482
|
-
|
|
509
|
+
trans_id = stop_order.trans_id
|
|
510
|
+
with self._lock_orders:
|
|
511
|
+
if trans_id not in self.orders:
|
|
512
|
+
self.logger.debug('on_stop_order: Заявка с номером %s. Номер транзакции %s. Заявка была выставлена не из торговой системы. Выход', stop_order.order_num, trans_id)
|
|
513
|
+
return
|
|
514
|
+
order: Order = self.orders[trans_id]
|
|
515
|
+
state = stop_order.state
|
|
516
|
+
if state == State.COMPLETED:
|
|
517
|
+
order.addinfo(linked_order=stop_order.linked_order)
|
|
518
|
+
self._save_broker_state()
|
|
519
|
+
with self._lock_notifs:
|
|
520
|
+
self.notifs.append(order.clone()) # Уведомляем брокера о заявке
|
|
483
521
|
|
|
484
522
|
def submit(self, order):
|
|
485
523
|
"""Отправка заявки на биржу (требуется BrokerBase)"""
|
|
486
|
-
return
|
|
524
|
+
return QuikStore.run_sync(self.place_order(order))
|
|
487
525
|
|
|
488
526
|
|
|
489
527
|
def add_order_history(self, orders, notify=True):
|
|
@@ -510,10 +548,10 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
510
548
|
for ticker_name, position in self._positions.items():
|
|
511
549
|
if datas and not any(data.p.dataname == ticker_name for data in datas):
|
|
512
550
|
continue
|
|
513
|
-
class_code, sec_code = await self.store.
|
|
514
|
-
last_price = await self.store.
|
|
551
|
+
class_code, sec_code = await self.store.parse_ticker_name(ticker_name)
|
|
552
|
+
last_price = await self.store.get_last_price(class_code, sec_code)
|
|
515
553
|
if last_price:
|
|
516
|
-
last_price = await self.store.
|
|
554
|
+
last_price = await self.store.quik_price_to_SUR(class_code, sec_code, last_price)
|
|
517
555
|
value += position.size * last_price
|
|
518
556
|
return value
|
|
519
557
|
|
|
@@ -522,13 +560,14 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
522
560
|
orders = self._load_broker_state()
|
|
523
561
|
if orders is None:
|
|
524
562
|
return
|
|
525
|
-
ord_list = await self.store.
|
|
526
|
-
stop_ord_list = await self.store.
|
|
563
|
+
ord_list = await self.store.get_orders()
|
|
564
|
+
stop_ord_list = await self.store.get_stop_orders()
|
|
527
565
|
founded = set()
|
|
528
566
|
for o in ord_list:
|
|
529
567
|
self.logger.debug('Active Order: %s', str(o))
|
|
530
|
-
|
|
531
|
-
|
|
568
|
+
str_trans_id = str(o.trans_id)
|
|
569
|
+
if str_trans_id in orders:
|
|
570
|
+
order: Order = orders[str_trans_id]
|
|
532
571
|
order.addinfo(order_num=o.order_num)
|
|
533
572
|
match o.state:
|
|
534
573
|
case State.ACTIVE:
|
|
@@ -543,12 +582,13 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
543
582
|
case _:
|
|
544
583
|
pass
|
|
545
584
|
order.price = float(o.price)
|
|
546
|
-
founded.add(
|
|
585
|
+
founded.add(str_trans_id)
|
|
547
586
|
|
|
548
587
|
for o in stop_ord_list:
|
|
549
588
|
self.logger.debug('Active Stop Order: %s', str(o))
|
|
550
|
-
|
|
551
|
-
|
|
589
|
+
str_trans_id = str(o.trans_id)
|
|
590
|
+
if str_trans_id in orders:
|
|
591
|
+
order: Order = orders[str_trans_id]
|
|
552
592
|
order.addinfo(order_num=o.order_num)
|
|
553
593
|
match o.state:
|
|
554
594
|
case State.ACTIVE:
|
|
@@ -561,20 +601,28 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
561
601
|
case _:
|
|
562
602
|
pass
|
|
563
603
|
order.price = float(o.price)
|
|
564
|
-
founded.add(
|
|
604
|
+
founded.add(str_trans_id)
|
|
565
605
|
orders_to_remove = set(orders.keys()) - founded
|
|
566
606
|
for trans_id in orders_to_remove:
|
|
567
607
|
del orders[trans_id]
|
|
568
608
|
with self._lock_orders:
|
|
569
609
|
self.orders = orders
|
|
570
|
-
self._save_broker_state()
|
|
571
|
-
|
|
572
610
|
|
|
573
611
|
def _save_broker_state(self):
|
|
574
612
|
"""Сохранение состояния брокера"""
|
|
613
|
+
def _info2dict(vals:dict):
|
|
614
|
+
info = {}
|
|
615
|
+
for k, v in vals.items():
|
|
616
|
+
if k == 'account':
|
|
617
|
+
info['account'] = v.to_dict()
|
|
618
|
+
else:
|
|
619
|
+
info[k] = v
|
|
620
|
+
return info
|
|
621
|
+
|
|
575
622
|
with self._lock_orders:
|
|
576
623
|
state = {}
|
|
577
624
|
state['version'] = 1
|
|
625
|
+
state['broker_info'] = self.info
|
|
578
626
|
state['orders'] = {}
|
|
579
627
|
state['last_order_ref'] = Order.last_ref()
|
|
580
628
|
state['ocos'] = self.ocos
|
|
@@ -585,9 +633,10 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
585
633
|
ord = {}
|
|
586
634
|
for k, v in self.orders.items():
|
|
587
635
|
ord[k] = v.to_dict()
|
|
636
|
+
ord[k]['info'] = _info2dict(v.info)
|
|
588
637
|
state['orders'] = ord
|
|
589
638
|
with open(self._state_file, 'w') as f:
|
|
590
|
-
json.dump(state, f)
|
|
639
|
+
json.dump(state, f, indent=2)
|
|
591
640
|
|
|
592
641
|
def _load_broker_state(self) -> None|dict:
|
|
593
642
|
"""Загрузка состояния брокера"""
|
|
@@ -603,17 +652,39 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
603
652
|
if version != 1:
|
|
604
653
|
self.logger.error('load_broker_state: Неподдерживаемая версия состояния брокера: %s', version)
|
|
605
654
|
return None
|
|
606
|
-
|
|
655
|
+
orders_state = state.get('orders', {})
|
|
607
656
|
orders = {}
|
|
608
|
-
for k, v in
|
|
609
|
-
|
|
657
|
+
for k, v in orders_state.items():
|
|
658
|
+
params = v.get('params', {})
|
|
659
|
+
info = v.get('info', {})
|
|
660
|
+
data_id = info.get('data_id', None)
|
|
661
|
+
if data_id is None:
|
|
662
|
+
self.logger.error('load_broker_state: Ошибка загрузки заявки %s: нет data_id', k)
|
|
663
|
+
continue
|
|
664
|
+
data = None
|
|
665
|
+
for d in self.cerebro.datas:
|
|
666
|
+
if d.data_id == data_id:
|
|
667
|
+
data = d
|
|
668
|
+
break
|
|
669
|
+
if data is None:
|
|
670
|
+
self.logger.error('load_broker_state: Ошибка загрузки заявки %s: не найден тикер с data_id=%s', k, data_id)
|
|
671
|
+
continue
|
|
672
|
+
params['data'] = data
|
|
673
|
+
order = Order.from_dict(params, v)
|
|
610
674
|
order.broker = self
|
|
611
|
-
|
|
612
|
-
order.
|
|
675
|
+
order.addinfo(account=self.account)
|
|
676
|
+
order.addcomminfo(self.getcommissioninfo(data))
|
|
613
677
|
orders[k] = order
|
|
614
678
|
last_order_ref = state.get('last_order_ref', 0)
|
|
679
|
+
for v in orders.values():
|
|
680
|
+
if v.parent_ref is not None:
|
|
681
|
+
for o in orders.values():
|
|
682
|
+
if o.ref == v.parent_ref:
|
|
683
|
+
v.parent = o
|
|
684
|
+
break
|
|
615
685
|
Order.reset_ref(last_order_ref)
|
|
616
686
|
self.ocos = state.get('ocos', {})
|
|
687
|
+
self.info = state.get('broker_info', {})
|
|
617
688
|
trade_nums_serializable = state.get('trade_nums', defaultdict(set))
|
|
618
689
|
for k, v in trade_nums_serializable.items():
|
|
619
690
|
self.trade_nums[k] = set(v)
|
|
@@ -626,7 +697,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
626
697
|
if not acc:
|
|
627
698
|
raise ValueError('QuikBroker: Не задан account для получения позиций')
|
|
628
699
|
if acc.futures:
|
|
629
|
-
futures_holdings = await self.store.
|
|
700
|
+
futures_holdings = await self.store.get_futures_client_holdings()
|
|
630
701
|
for fut in futures_holdings:
|
|
631
702
|
if fut.total_net != 0:
|
|
632
703
|
self.logger.debug("Futures Position: %s TotalNet: %s AvrPosnPrice: %s", fut.sec_code, fut.total_net, fut.avr_pos_nprice)
|
|
@@ -634,25 +705,25 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
634
705
|
sec_code = fut.sec_code
|
|
635
706
|
size = int(fut.total_net)
|
|
636
707
|
if self.store.p.lots:
|
|
637
|
-
size = await self.
|
|
638
|
-
dataname = self.store.
|
|
639
|
-
price = await self.store.
|
|
708
|
+
size = await self._lots_to_size(class_code, sec_code, size)
|
|
709
|
+
dataname = self.store.get_ticker_name(class_code, sec_code)
|
|
710
|
+
price = await self.store.quik_price_to_SUR(class_code, sec_code, fut.avr_pos_nprice)
|
|
640
711
|
positions[dataname] = Position(size, price)
|
|
641
712
|
else:
|
|
642
|
-
depo_limits = await self.store.
|
|
713
|
+
depo_limits = await self.store.get_all_depo_limits()
|
|
643
714
|
account_depo_limits = [limit for limit in depo_limits
|
|
644
715
|
if limit.client_code == acc.client_code and
|
|
645
716
|
limit.firm_id == acc.firm_id and
|
|
646
717
|
limit.limit_kind.value == self.store.p.limit_kind and
|
|
647
718
|
limit.current_bal != 0]
|
|
648
719
|
for limit in account_depo_limits:
|
|
649
|
-
class_code, sec_code = await self.store.
|
|
720
|
+
class_code, sec_code = await self.store.parse_ticker_name(limit.sec_code)
|
|
650
721
|
size = int(limit.current_bal)
|
|
651
722
|
if self.store.p.lots: # Если входящий остаток в лотах
|
|
652
|
-
size = await self.
|
|
723
|
+
size = await self._lots_to_size(class_code, sec_code, size)
|
|
653
724
|
# Переводим средневзвешенную цену приобретения позиции (входа) в цену в рублях за штуку
|
|
654
|
-
price = await self.store.
|
|
655
|
-
dataname = self.store.
|
|
725
|
+
price = await self.store.quik_price_to_SUR(class_code, sec_code, float(limit.wa_position_price))
|
|
726
|
+
dataname = self.store.get_ticker_name(class_code, sec_code)
|
|
656
727
|
positions[dataname] = Position(size, price)
|
|
657
728
|
with self._lock_positions:
|
|
658
729
|
self._positions = positions
|
|
@@ -660,7 +731,7 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
660
731
|
async def _getcash(self):
|
|
661
732
|
"""Получение текущего баланса по всем счетам"""
|
|
662
733
|
self.logger.debug('call _getcash()')
|
|
663
|
-
money_limits:MoneyLimitEx = await self.store.
|
|
734
|
+
money_limits:MoneyLimitEx = await self.store.get_money_limits()
|
|
664
735
|
if len(money_limits) == 0:
|
|
665
736
|
self.logger.error("_getcash: Ошибка получения баланса - нет лимитов по деньгам")
|
|
666
737
|
return 0.0
|
|
@@ -670,14 +741,14 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
670
741
|
raise ValueError('QuikBroker: Не задан account для получения позиций')
|
|
671
742
|
if acc.futures:
|
|
672
743
|
if self.store.p.edp:
|
|
673
|
-
portf:PortfolioInfoEx = await self.store.
|
|
744
|
+
portf:PortfolioInfoEx = await self.store.get_portfolio_info_ex(acc.firm_id, acc.client_code)
|
|
674
745
|
if portf:
|
|
675
746
|
cash += portf.all_assets
|
|
676
747
|
else:
|
|
677
748
|
self.logger.error('_getcash: QUIK не вернул информацию по счету с firm_id=%s, client_code=%s. Проверьте правильность значений', acc.firm_id, acc.client_code)
|
|
678
749
|
else:
|
|
679
750
|
# Баланс = Лимит откр.поз. + Вариац.маржа + Накоплен.доход
|
|
680
|
-
fut_limits = await self.store.
|
|
751
|
+
fut_limits = await self.store.get_futures_limit(acc.firm_id, acc.trade_account_id, self.store.p.currency)
|
|
681
752
|
if fut_limits:
|
|
682
753
|
cash += fut_limits.cbp_limit + fut_limits.var_margin + fut_limits.accruedint
|
|
683
754
|
else:
|
|
@@ -698,3 +769,24 @@ class QuikBroker(with_metaclass(MetaQuikBroker, BrokerBase)):
|
|
|
698
769
|
cash += balance
|
|
699
770
|
return cash
|
|
700
771
|
|
|
772
|
+
async def _is_ucp_client(self, firm_id: str, client_code: str) -> bool:
|
|
773
|
+
"""Проверка, является ли клиент участником единой денежной позиции (УДП)"""
|
|
774
|
+
self.logger.debug("Checking if client %s.%s is UCP", firm_id, client_code)
|
|
775
|
+
try:
|
|
776
|
+
return await self.store.quik_api.trading.is_ucp_client(firm_id, client_code)
|
|
777
|
+
except Exception as e:
|
|
778
|
+
self.logger.error("Error checking UCP client: %s", e)
|
|
779
|
+
return False
|
|
780
|
+
|
|
781
|
+
async def _size_to_lots(self, class_code:str, sec_code:str, size) -> int:
|
|
782
|
+
info = await self.store.get_ticker_info(class_code, sec_code)
|
|
783
|
+
if info and info.lot_size and info.lot_size > 0:
|
|
784
|
+
return int(size // info.lot_size)
|
|
785
|
+
return size
|
|
786
|
+
|
|
787
|
+
async def _lots_to_size(self, class_code: str, sec_code: str, lots) -> int:
|
|
788
|
+
info = await self.store.get_ticker_info(class_code, sec_code)
|
|
789
|
+
if info and info.lot_size and info.lot_size > 0:
|
|
790
|
+
return int(lots * info.lot_size)
|
|
791
|
+
return lots
|
|
792
|
+
|
|
@@ -42,7 +42,7 @@ class QuikData(with_metaclass(MetaQuikData, AbstractDataBase)):
|
|
|
42
42
|
|
|
43
43
|
def __init__(self, **kwargs):
|
|
44
44
|
self.store = QuikStore(**kwargs) # Хранилище QUIK
|
|
45
|
-
self.class_code, self.sec_code =
|
|
45
|
+
self.class_code, self.sec_code = QuikStore.run_sync(self.store.parse_ticker_name(self.p.dataname))
|
|
46
46
|
# tf = self.store._bt_timeframe_to_str(self.p.timeframe, self.p.compression)
|
|
47
47
|
self.candle_interval = self.store._bt_timeframe_2_quik(self.p.timeframe, self.p.compression)
|
|
48
48
|
self._data_id = self.store._get_data_id(self.class_code, self.sec_code, self.candle_interval)
|
|
@@ -73,7 +73,7 @@ class QuikData(with_metaclass(MetaQuikData, AbstractDataBase)):
|
|
|
73
73
|
if self.p.live_bars: # Если получаем историю и новые бары
|
|
74
74
|
if not self.store._is_subscribed_to_candles(self.class_code, self.sec_code, self.candle_interval):
|
|
75
75
|
self.store._subscribe_to_candles(self.class_code, self.sec_code, self.candle_interval) # Подписываемся на новые бары
|
|
76
|
-
self.info = self.store.
|
|
76
|
+
self.info = self.store.get_ticker_info_sync(self.class_code, self.sec_code)
|
|
77
77
|
|
|
78
78
|
cur_datetime = self.store._get_quik_datetime_now()
|
|
79
79
|
self.put_notification(self.DELAYED) # Отправляем уведомление об отправке исторических (не новых) баров
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import collections
|
|
3
|
+
import os
|
|
3
4
|
import threading
|
|
4
5
|
import asyncio
|
|
5
6
|
import atexit
|
|
@@ -100,20 +101,38 @@ class MetaSingleton(MetaParams):
|
|
|
100
101
|
|
|
101
102
|
|
|
102
103
|
class Account:
|
|
103
|
-
|
|
104
|
-
order_client_code: str = None
|
|
105
|
-
firm_id: str = None
|
|
106
|
-
class_codes: List[str]
|
|
107
|
-
trade_account_id: str
|
|
108
|
-
futures: bool = False
|
|
109
|
-
is_ucp: bool = False
|
|
110
|
-
|
|
104
|
+
'''Account class to hold trade account information'''
|
|
111
105
|
def __init__(self, trade_account_id: str, client_code: str, firm_id: str, class_codes: List[str]):
|
|
112
|
-
self.
|
|
113
|
-
self.
|
|
114
|
-
self.firm_id = firm_id
|
|
115
|
-
self.class_codes = class_codes
|
|
106
|
+
self.client_code: str = client_code
|
|
107
|
+
self.order_client_code: str = client_code
|
|
108
|
+
self.firm_id: str = firm_id
|
|
109
|
+
self.class_codes: List[str] = class_codes
|
|
110
|
+
self.trade_account_id: str = trade_account_id
|
|
111
|
+
self.futures: bool = False
|
|
112
|
+
self.is_ucp: bool = False
|
|
113
|
+
|
|
114
|
+
def to_dict(self):
|
|
115
|
+
return {
|
|
116
|
+
'trade_account_id': self.trade_account_id,
|
|
117
|
+
'client_code': self.client_code,
|
|
118
|
+
'firm_id': self.firm_id,
|
|
119
|
+
'class_codes': self.class_codes,
|
|
120
|
+
'futures': self.futures,
|
|
121
|
+
'is_ucp': self.is_ucp,
|
|
122
|
+
}
|
|
116
123
|
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_dict(cls, odict):
|
|
126
|
+
acc = cls(
|
|
127
|
+
trade_account_id=odict.get('trade_account_id'),
|
|
128
|
+
client_code=odict.get('client_code'),
|
|
129
|
+
firm_id=odict.get('firm_id'),
|
|
130
|
+
class_codes=odict.get('class_codes', []),
|
|
131
|
+
)
|
|
132
|
+
acc.futures = odict.get('futures', False)
|
|
133
|
+
acc.is_ucp = odict.get('is_ucp', False)
|
|
134
|
+
return acc
|
|
135
|
+
|
|
117
136
|
|
|
118
137
|
class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
119
138
|
'''
|
|
@@ -138,10 +157,9 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
138
157
|
('edp', False), # Единая денежная позиция
|
|
139
158
|
('slippage_steps', 10), # Кол-во шагов цены для проскальзывания
|
|
140
159
|
('data_dir', 'DataQuik'), # Каталог для хранения данных
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# ('timeout', 3.0), # timeout between reconnections
|
|
160
|
+
|
|
161
|
+
('client_code_for_orders', None), # Номер торгового терминала. У брокера Финам требуется для совершения торговых операций
|
|
162
|
+
('trade_account_id', None),
|
|
145
163
|
)
|
|
146
164
|
|
|
147
165
|
@classmethod
|
|
@@ -190,7 +208,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
190
208
|
self._lock_qdata = threading.RLock() # Гибридный: Async(_on_new_candle/_register_data/_unregister_data) + может быть sync доступ
|
|
191
209
|
self._lock_notifs = threading.Lock() # Гибридный: Sync(get_notifications/put_notification) + может быть async
|
|
192
210
|
self._lock_accounts = threading.Lock() # Только Async: get_accounts (но может расшириться)
|
|
193
|
-
self.
|
|
211
|
+
self.lock_store_data = threading.RLock() # Гибридный: Используется в QuikBroker для критических операций с данными
|
|
194
212
|
|
|
195
213
|
# Примечание: threading.Lock безопасен в данной архитектуре благодаря отдельному потоку для event loop.
|
|
196
214
|
|
|
@@ -261,8 +279,8 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
261
279
|
self.quik_api.events.remove_on_disconnected(self._on_disconnected)
|
|
262
280
|
self.quik_api.events.remove_on_trans_reply(self._on_transaction_reply)
|
|
263
281
|
self.quik_api.events.remove_on_trans_reply(self._on_trade)
|
|
264
|
-
|
|
265
|
-
|
|
282
|
+
self.quik_api.events.remove_on_order(self._on_order)
|
|
283
|
+
self.quik_api.events.remove_on_stop_order(self._on_stop_order)
|
|
266
284
|
|
|
267
285
|
self.logger.info("Stopping QUIK async loop...")
|
|
268
286
|
self._stop_event.set() # Устанавливаем флаг остановки
|
|
@@ -305,8 +323,8 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
305
323
|
self.quik_api.events.add_on_disconnected(self._on_disconnected)
|
|
306
324
|
self.quik_api.events.add_on_trans_reply(self._on_transaction_reply)
|
|
307
325
|
self.quik_api.events.add_on_trade(self._on_trade)
|
|
308
|
-
|
|
309
|
-
|
|
326
|
+
self.quik_api.events.add_on_order(self._on_order)
|
|
327
|
+
self.quik_api.events.add_on_stop_order(self._on_stop_order)
|
|
310
328
|
|
|
311
329
|
except Exception as e:
|
|
312
330
|
self.logger.error("Ошибка подключения: %s", e)
|
|
@@ -332,23 +350,23 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
332
350
|
async def _on_transaction_reply(self, reply: TransactionReply):
|
|
333
351
|
# self.logger.debug(reply)
|
|
334
352
|
if self.broker:
|
|
335
|
-
await self.broker.
|
|
353
|
+
await self.broker.on_trans_reply(reply)
|
|
336
354
|
|
|
337
355
|
async def _on_order(self, order : Order):
|
|
338
356
|
# self.logger.info(order)
|
|
339
357
|
if self.broker:
|
|
340
|
-
await self.broker.
|
|
358
|
+
await self.broker.on_order(order)
|
|
341
359
|
|
|
342
360
|
async def _on_stop_order(self, stop_order:StopOrder):
|
|
343
361
|
# self.logger.info(stop_order)
|
|
344
362
|
if self.broker:
|
|
345
|
-
await self.broker.
|
|
363
|
+
await self.broker.on_stop_order(stop_order)
|
|
346
364
|
pass
|
|
347
365
|
|
|
348
366
|
async def _on_trade(self, trade: Trade):
|
|
349
367
|
# self.logger.debug(trade)
|
|
350
368
|
if self.broker:
|
|
351
|
-
await self.broker.
|
|
369
|
+
await self.broker.on_trade(trade)
|
|
352
370
|
|
|
353
371
|
def _get_data_by_id(self, data_id: str):
|
|
354
372
|
"""Получение данных по тикеру и временному интервалу"""
|
|
@@ -433,7 +451,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
433
451
|
self.logger.error("Error getting trade accounts: %s", e)
|
|
434
452
|
return []
|
|
435
453
|
|
|
436
|
-
async def
|
|
454
|
+
async def get_ticker_info(self, class_code: str, sec_code: str) -> SecurityInfo | None:
|
|
437
455
|
"""Получение информации о тикере из QUIK"""
|
|
438
456
|
key = (class_code, sec_code)
|
|
439
457
|
self.logger.debug("Getting ticker info for %s.%s", class_code, sec_code)
|
|
@@ -453,19 +471,10 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
453
471
|
|
|
454
472
|
def _get_ticker_info_sync(self, class_code: str, sec_code: str) -> dict:
|
|
455
473
|
"""Синхронное получение информации о тикере из QUIK"""
|
|
456
|
-
info =
|
|
474
|
+
info = QuikStore.run_sync(self.get_ticker_info(class_code, sec_code))
|
|
457
475
|
return info.to_dict() if info else {}
|
|
458
476
|
|
|
459
|
-
async def
|
|
460
|
-
"""Проверка, является ли клиент участником единой денежной позиции (УДП)"""
|
|
461
|
-
self.logger.debug("Checking if client %s.%s is UCP", firm_id, client_code)
|
|
462
|
-
try:
|
|
463
|
-
return await self.quik_api.trading.is_ucp_client(firm_id, client_code)
|
|
464
|
-
except Exception as e:
|
|
465
|
-
self.logger.error("Error checking UCP client: %s", e)
|
|
466
|
-
return False
|
|
467
|
-
|
|
468
|
-
async def _get_portfolio_info_ex(self, firm_id: str, client_code: str) -> PortfolioInfoEx | None:
|
|
477
|
+
async def get_portfolio_info_ex(self, firm_id: str, client_code: str) -> PortfolioInfoEx | None:
|
|
469
478
|
self.logger.debug("Getting extended portfolio info for %s.%s", firm_id, client_code)
|
|
470
479
|
try:
|
|
471
480
|
return await self.quik_api.trading.get_portfolio_info_ex(firm_id, client_code)
|
|
@@ -482,7 +491,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
482
491
|
self.logger.error("Error getting extended ticker info: %s", e)
|
|
483
492
|
return None
|
|
484
493
|
|
|
485
|
-
async def
|
|
494
|
+
async def get_all_depo_limits(self) -> List[DepoLimitEx]:
|
|
486
495
|
"""Получение всех лимитов по бумагам из QUIK"""
|
|
487
496
|
try:
|
|
488
497
|
return await self.quik_api.trading.get_depo_limits()
|
|
@@ -490,7 +499,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
490
499
|
self.logger.error("Error getting all depo limits: %s", e)
|
|
491
500
|
return []
|
|
492
501
|
|
|
493
|
-
async def
|
|
502
|
+
async def get_money_limits(self) -> List[MoneyLimitEx]:
|
|
494
503
|
"""Получение всех лимитов по деньгам из QUIK"""
|
|
495
504
|
try:
|
|
496
505
|
return await self.quik_api.trading.get_money_limits()
|
|
@@ -498,7 +507,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
498
507
|
self.logger.error("Error getting all money limits: %s", e)
|
|
499
508
|
return []
|
|
500
509
|
|
|
501
|
-
async def
|
|
510
|
+
async def get_futures_limit(self, firm_id: str, acc_id: str, curr_code: str = "") -> FuturesLimits | None:
|
|
502
511
|
"""Получение всех лимитов по фьючерсам из QUIK"""
|
|
503
512
|
try:
|
|
504
513
|
return await self.quik_api.trading.get_futures_limit(firm_id, acc_id, FuturesLimitType.MONEY, curr_code)
|
|
@@ -506,7 +515,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
506
515
|
self.logger.error("Error getting futures limits: %s", e)
|
|
507
516
|
return None
|
|
508
517
|
|
|
509
|
-
async def
|
|
518
|
+
async def get_orders(self) -> list[Order]:
|
|
510
519
|
"""Получение всех заявок из QUIK"""
|
|
511
520
|
try:
|
|
512
521
|
return await self.quik_api.orders.get_orders()
|
|
@@ -514,7 +523,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
514
523
|
self.logger.error("Error getting orders: %s", e)
|
|
515
524
|
return []
|
|
516
525
|
|
|
517
|
-
async def
|
|
526
|
+
async def get_stop_orders(self) -> list[StopOrder]:
|
|
518
527
|
"""Получение всех стоп-заявок из QUIK"""
|
|
519
528
|
try:
|
|
520
529
|
return await self.quik_api.stop_orders.get_stop_orders()
|
|
@@ -522,7 +531,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
522
531
|
self.logger.error("Error getting stop orders: %s", e)
|
|
523
532
|
return []
|
|
524
533
|
|
|
525
|
-
async def
|
|
534
|
+
async def get_order_by_number(self, order_num:int) -> Order | None:
|
|
526
535
|
"""Получение информации о заявке по её номеру"""
|
|
527
536
|
try:
|
|
528
537
|
return await self.quik_api.orders.get_order_by_number(order_num)
|
|
@@ -530,7 +539,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
530
539
|
self.logger.error("Error getting order by number: %s", e)
|
|
531
540
|
return None
|
|
532
541
|
|
|
533
|
-
async def
|
|
542
|
+
async def get_futures_client_holdings(self) -> list[FuturesClientHolding]:
|
|
534
543
|
"""Получение всех позиций по фьючерсам из QUIK"""
|
|
535
544
|
try:
|
|
536
545
|
return await self.quik_api.trading.get_futures_client_holdings()
|
|
@@ -560,7 +569,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
560
569
|
return datetime.now(self.tz_msk).replace(tzinfo=None) # То время МСК получаем из локального времени
|
|
561
570
|
|
|
562
571
|
|
|
563
|
-
async def
|
|
572
|
+
async def parse_ticker_name(self, name:str) -> tuple[str, str] | None:
|
|
564
573
|
parts = name.split('.')
|
|
565
574
|
if len(parts) >= 2:
|
|
566
575
|
return (parts[0], '.'.join(parts[1:]))
|
|
@@ -576,7 +585,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
576
585
|
self._sec_code2name[sec_code] = (class_code, sec_code)
|
|
577
586
|
return class_code, sec_code
|
|
578
587
|
|
|
579
|
-
def
|
|
588
|
+
def get_ticker_name(self, class_code: str, sec_code: str) -> str:
|
|
580
589
|
return f'{class_code}.{sec_code}'
|
|
581
590
|
|
|
582
591
|
|
|
@@ -656,9 +665,9 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
656
665
|
"""
|
|
657
666
|
Получение уникального идентификатора данных по тикеру и временному интервалу
|
|
658
667
|
"""
|
|
659
|
-
return f'{class_code}.{sec_code}
|
|
668
|
+
return f'{class_code}.{sec_code}.{interval.name}'
|
|
660
669
|
|
|
661
|
-
async def
|
|
670
|
+
async def send_transaction(self, transaction) -> int:
|
|
662
671
|
"""Отправка транзакции в QUIK"""
|
|
663
672
|
try:
|
|
664
673
|
trans_id = await self.quik_api.trading.send_transaction(transaction)
|
|
@@ -690,19 +699,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
690
699
|
self.logger.error('get_accounts: Ошибка получения списка счетов: %s', e)
|
|
691
700
|
return self._accounts
|
|
692
701
|
|
|
693
|
-
async def
|
|
694
|
-
info = await self._get_ticker_info(class_code, sec_code)
|
|
695
|
-
if info and info.lot_size and info.lot_size > 0:
|
|
696
|
-
return int(lots * info.lot_size)
|
|
697
|
-
return lots
|
|
698
|
-
|
|
699
|
-
async def _size_to_lots(self, class_code:str, sec_code:str, size) -> int:
|
|
700
|
-
info = await self._get_ticker_info(class_code, sec_code)
|
|
701
|
-
if info and info.lot_size and info.lot_size > 0:
|
|
702
|
-
return int(size // info.lot_size)
|
|
703
|
-
return size
|
|
704
|
-
|
|
705
|
-
async def _get_last_price(self, class_code: str, sec_code: str) -> float | None:
|
|
702
|
+
async def get_last_price(self, class_code: str, sec_code: str) -> float | None:
|
|
706
703
|
with self._lock_qdata:
|
|
707
704
|
data = self.qdata_last.get((class_code, sec_code), None)
|
|
708
705
|
if data and len(data):
|
|
@@ -726,11 +723,11 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
726
723
|
self._ticker_stepprice[key] = (step_price, datetime.now(self.tz_msk))
|
|
727
724
|
return step_price
|
|
728
725
|
|
|
729
|
-
async def
|
|
726
|
+
async def quik_price_to_SUR(self, class_code: str, sec_code: str, quik_price: float) -> float:
|
|
730
727
|
"""
|
|
731
728
|
Перевод цены QUIK в цену в рублях за штуку
|
|
732
729
|
"""
|
|
733
|
-
si = await self.
|
|
730
|
+
si = await self.get_ticker_info(class_code, sec_code)
|
|
734
731
|
if not si:
|
|
735
732
|
return quik_price # то цена не изменяется
|
|
736
733
|
if class_code in ('TQOB', 'TQCB', 'TQRD', 'TQIR'): # Для облигаций (Т+ Гособлигации, Т+ Облигации, Т+ Облигации Д, Т+ Облигации ПИР)
|
|
@@ -745,9 +742,9 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
745
742
|
return lot_price / lot_size # Цена за штуку
|
|
746
743
|
return quik_price
|
|
747
744
|
|
|
748
|
-
async def
|
|
745
|
+
async def price_to_valid_price(self, class_code: str, sec_code: str, price: float) -> Decimal:
|
|
749
746
|
"""Перевод цены в рублях за штуку в корректную цену QUIK"""
|
|
750
|
-
si = await self.
|
|
747
|
+
si = await self.get_ticker_info(class_code, sec_code)
|
|
751
748
|
if not si:
|
|
752
749
|
return Decimal(str(price))
|
|
753
750
|
# Возращаем цену, которую примет QUIK в заявке
|
|
@@ -758,7 +755,7 @@ class QuikStore(with_metaclass(MetaSingleton, object)):
|
|
|
758
755
|
|
|
759
756
|
async def _price_to_quik_price(self, class_code: str, sec_code: str, price: float) -> Decimal:
|
|
760
757
|
"""Перевод цены в рублях за штуку в цену QUIK"""
|
|
761
|
-
si = await self.
|
|
758
|
+
si = await self.get_ticker_info(class_code, sec_code)
|
|
762
759
|
if not si:
|
|
763
760
|
return Decimal(str(price))
|
|
764
761
|
min_price_step = si.min_price_step
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bn_quik"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Backtrader-next connector for QUIK terminal"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "GPL-3.0-or-later" }
|
|
@@ -16,7 +16,7 @@ classifiers = [
|
|
|
16
16
|
|
|
17
17
|
requires-python = ">=3.11"
|
|
18
18
|
dependencies = [
|
|
19
|
-
"backtrader-next>=2.
|
|
19
|
+
"backtrader-next>=2.3.2",
|
|
20
20
|
"quik-python>=1.2.0",
|
|
21
21
|
]
|
|
22
22
|
[project.urls]
|
bn_quik-0.1.0/README.md
DELETED
bn_quik-0.1.0/requirements.txt
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|