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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bn_quik
3
- Version: 0.1.0
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.1.0
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
- # bn_quik
19
+ <div align="center">
20
+
21
+ # bn_quik
22
+
23
+ [![PyPi Release](https://img.shields.io/pypi/v/bn_quik?color=32a852&label=PyPi)](https://pypi.org/project/bn_quik/)
24
+ [![Total downloads](https://img.shields.io/pepy/dt/bn_quik?label=%E2%88%91&color=skyblue)](https://pypistats.org/packages/bn_quik)
25
+ [![Made with Python](https://img.shields.io/badge/Python-3.11+-c7a002?logo=python&logoColor=white)](https://python.org "Go to Python homepage")
26
+ [![License](https://img.shields.io/github/license/Alex-Shur/bn_quik?color=9c2400)](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
@@ -0,0 +1,11 @@
1
+ <div align="center">
2
+
3
+ # bn_quik
4
+
5
+ [![PyPi Release](https://img.shields.io/pypi/v/bn_quik?color=32a852&label=PyPi)](https://pypi.org/project/bn_quik/)
6
+ [![Total downloads](https://img.shields.io/pepy/dt/bn_quik?label=%E2%88%91&color=skyblue)](https://pypistats.org/packages/bn_quik)
7
+ [![Made with Python](https://img.shields.io/badge/Python-3.11+-c7a002?logo=python&logoColor=white)](https://python.org "Go to Python homepage")
8
+ [![License](https://img.shields.io/github/license/Alex-Shur/bn_quik?color=9c2400)](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.store._is_ucp_client(self.account.firm_id, self.account.client_code)
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
- self.store.__class__.run_sync(self.__start_async())
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 self.store.__class__.run_sync(self._getvalue(datas))
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 = self.store.__class__.run_sync(self.create_order(owner, data, size, price, plimit, exectype, valid, oco, parent, transmit, True, **kwargs))
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 = self.store.__class__.run_sync(self.create_order(owner, data, size, price, plimit, exectype, valid, oco, parent, transmit, False, **kwargs))
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 self.store.__class__.run_sync(self.cancel_order(order))
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._get_ticker_info(class_code, sec_code)
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.store._size_to_lots(class_code, sec_code, quantity)
215
- order.size = math.copysign(await self.store._lots_to_size(class_code, sec_code, quantity), order.size)
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.info["trans_id"] = trans_id
219
- order.info["data_id"] = order.data.data_id
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._get_last_price(class_code, sec_code))
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._price_to_valid_price(class_code, sec_code, last_price)
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._price_to_valid_price(class_code, sec_code, order.price)
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._price_to_valid_price(class_code, sec_code, order.price)
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._price_to_valid_price(class_code, sec_code, market_price) # Из документации QUIK: При покупке/продаже фьючерсов по рынку нужно ставить цену хуже последней сделки
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._price_to_valid_price(class_code, sec_code, order.price)
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._price_to_valid_price(class_code, sec_code, order.pricelimit)
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._send_transaction(transaction)
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._get_order_by_number(order_num) is None
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
- transaction.ACTION = TransactionAction.KILL_STOP_ORDER
316
- transaction.STOP_ORDER_KEY = str(order_num)
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._send_transaction(transaction)
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 _on_trans_reply(self, data: TransactionReply):
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
- order.accept(self) # Заявка принята на бирже (Order.Accepted)
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._lock_store_data:
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._lock_store_data:
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._lock_store_data:
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 _on_trade(self, data: Trade):
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._get_ticker_name(class_code, sec_code)
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.store._lots_to_size(class_code, sec_code, size) # то переводим кол-во из лотов в штуки
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 _on_order(self, order : Order):
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 _on_stop_order(self, stop_order:StopOrder):
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 self.store.__class__.run_sync(self.place_order(order))
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._parse_ticker_name(ticker_name)
514
- last_price = await self.store._get_last_price(class_code, sec_code)
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._quik_price_to_SUR(class_code, sec_code, last_price)
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._get_orders()
526
- stop_ord_list = await self.store._get_stop_orders()
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
- if o.trans_id in orders:
531
- order: Order = orders[o.trans_id]
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(o.trans_id)
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
- if o.trans_id in orders:
551
- order: Order = orders[o.trans_id]
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(o.trans_id)
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
- orders_dict = state.get('orders', {})
655
+ orders_state = state.get('orders', {})
607
656
  orders = {}
608
- for k, v in orders_dict.items():
609
- order = Order.from_dict(v)
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
- data_id = order.info["data_id"]
612
- order.data = self.store._get_data_by_id(data_id)
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._get_futures_client_holdings()
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.store._lots_to_size(class_code, sec_code, size)
638
- dataname = self.store._get_ticker_name(class_code, sec_code)
639
- price = await self.store._quik_price_to_SUR(class_code, sec_code, fut.avr_pos_nprice)
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._get_all_depo_limits()
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._parse_ticker_name(limit.sec_code)
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.store._lots_to_size(class_code, sec_code, size)
723
+ size = await self._lots_to_size(class_code, sec_code, size)
653
724
  # Переводим средневзвешенную цену приобретения позиции (входа) в цену в рублях за штуку
654
- price = await self.store._quik_price_to_SUR(class_code, sec_code, float(limit.wa_position_price))
655
- dataname = self.store._get_ticker_name(class_code, sec_code)
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._get_money_limits()
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._get_portfolio_info_ex(acc.firm_id, acc.client_code)
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._get_futures_limit(acc.firm_id, acc.trade_account_id, self.store.p.currency)
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 = self.store.__class__.run_sync(self.store._parse_ticker_name(self.p.dataname))
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._get_ticker_info_sync(self.class_code, self.sec_code)
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
- client_code: str = None
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.trade_account_id = trade_account_id
113
- self.client_code = client_code
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
- #('notifyall', False),
142
- #('_debug', False),
143
- # ('reconnect', 3), # -1 forever, 0 No, > 0 number of retries
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._lock_store_data = threading.RLock() # Гибридный: Используется в QuikBroker для критических операций с данными
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
- # self.quik_api.events.remove_on_order(self._on_order)
265
- # self.quik_api.events.remove_on_stop_order(self._on_stop_order)
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
- # self.quik_api.events.add_on_order(self._on_order)
309
- # self.quik_api.events.add_on_stop_order(self._on_stop_order)
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._on_trans_reply(reply)
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._on_order(order)
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._on_stop_order(stop_order)
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._on_trade(trade)
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 _get_ticker_info(self, class_code: str, sec_code: str) -> SecurityInfo | None:
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 = self.__class__.run_sync(self._get_ticker_info(class_code, sec_code))
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 _is_ucp_client(self, firm_id: str, client_code: str) -> bool:
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 _get_all_depo_limits(self) -> List[DepoLimitEx]:
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 _get_money_limits(self) -> List[MoneyLimitEx]:
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 _get_futures_limit(self, firm_id: str, acc_id: str, curr_code: str = "") -> FuturesLimits | None:
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 _get_orders(self) -> list[Order]:
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 _get_stop_orders(self) -> list[StopOrder]:
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 _get_order_by_number(self, order_num:int) -> Order | None:
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 _get_futures_client_holdings(self) -> list[FuturesClientHolding]:
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 _parse_ticker_name(self, name:str) -> tuple[str, str] | None:
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 _get_ticker_name(self, class_code: str, sec_code: str) -> str:
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}_{interval.name}'
668
+ return f'{class_code}.{sec_code}.{interval.name}'
660
669
 
661
- async def _send_transaction(self, transaction) -> int:
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 _lots_to_size(self, class_code: str, sec_code: str, lots) -> int:
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 _quik_price_to_SUR(self, class_code: str, sec_code: str, quik_price: float) -> float:
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._get_ticker_info(class_code, sec_code)
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 _price_to_valid_price(self, class_code: str, sec_code: str, price: float) -> Decimal:
745
+ async def price_to_valid_price(self, class_code: str, sec_code: str, price: float) -> Decimal:
749
746
  """Перевод цены в рублях за штуку в корректную цену QUIK"""
750
- si = await self._get_ticker_info(class_code, sec_code)
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._get_ticker_info(class_code, sec_code)
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.0"
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.1.0",
19
+ "backtrader-next>=2.3.2",
20
20
  "quik-python>=1.2.0",
21
21
  ]
22
22
  [project.urls]
@@ -0,0 +1,2 @@
1
+ backtrader-next>=2.3.2
2
+ quik-python>=1.2.0
bn_quik-0.1.0/README.md DELETED
@@ -1,2 +0,0 @@
1
- # bn_quik
2
- Live trading intergartion of backtrader-next with QUIK trade terminal
@@ -1,2 +0,0 @@
1
- backtrader-next>=2.1.0
2
- quik-python>=1.2.0
File without changes
File without changes
File without changes