onetick-py 1.177.0__py3-none-any.whl → 1.180.0__py3-none-any.whl

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,7 +1,9 @@
1
1
  import itertools
2
2
  import warnings
3
- from typing import Union, Iterable, Tuple, Optional, Any, Literal
3
+ from collections import defaultdict
4
+ from typing import Union, Iterable, Tuple, Optional, Literal
4
5
  from datetime import date as dt_date, datetime, timedelta
6
+ from functools import wraps
5
7
 
6
8
  import pandas as pd
7
9
  from dateutil.tz import gettz
@@ -19,6 +21,39 @@ def _datetime2date(dt: Union[dt_date, datetime]) -> dt_date:
19
21
  return dt_date(dt.year, dt.month, dt.day)
20
22
 
21
23
 
24
+ def _method_cache(meth):
25
+ """
26
+ Cache the output of class method.
27
+ Cache is created inside of the self object (in self.__cache property)
28
+ and will be deleted when self object is destroyed.
29
+
30
+ This is a rewrite of functools.cache,
31
+ but it doesn't add self argument to cache key and thus doesn't keep reference to self forever.
32
+ """
33
+ @wraps(meth)
34
+ def wrapper(self, *args, **kwargs):
35
+
36
+ # cache key is a tuple of all arguments
37
+ key = args
38
+ for kw_tup in kwargs.items():
39
+ key += kw_tup
40
+ key = hash(key)
41
+
42
+ if not hasattr(self, '__cache'):
43
+ self.__cache = defaultdict(dict)
44
+
45
+ method_cache = self.__cache[meth.__name__]
46
+
47
+ miss = object()
48
+ result = method_cache.get(key, miss)
49
+ if result is miss:
50
+ result = meth(self, *args, **kwargs)
51
+ method_cache[key] = result
52
+ return result
53
+
54
+ return wrapper
55
+
56
+
22
57
  class DB:
23
58
 
24
59
  """
@@ -37,7 +72,11 @@ class DB:
37
72
  self._locator_date_ranges = None
38
73
 
39
74
  def __eq__(self, obj):
40
- return str(self) == str(obj)
75
+ return all((
76
+ self.name == obj.name,
77
+ self.description == obj.description,
78
+ self.context == obj.context,
79
+ ))
41
80
 
42
81
  def __lt__(self, obj):
43
82
  return str(self) < str(obj)
@@ -45,6 +84,7 @@ class DB:
45
84
  def __str__(self):
46
85
  return self.name
47
86
 
87
+ @_method_cache
48
88
  def access_info(self, deep_scan=False, username=None) -> Union[pd.DataFrame, dict]:
49
89
  """
50
90
  Get access info for this database and ``username``.
@@ -111,13 +151,19 @@ class DB:
111
151
  >> otq.WhereClause(where=f'DB_NAME = "{name}"')
112
152
  )
113
153
  graph = otq.GraphQuery(node)
154
+
155
+ self._set_intervals()
156
+ # start and end times don't matter, but need to fit in the configured time ranges
157
+ start, end = self._locator_date_ranges[-1]
158
+
114
159
  df = otp.run(graph,
115
- symbols='LOCAL::',
116
- # start and end times don't matter
117
- start=db_constants.DEFAULT_START_DATE,
118
- end=db_constants.DEFAULT_END_DATE,
160
+ symbols=f'{self.name}::',
161
+ start=start,
162
+ end=end,
119
163
  # and timezone is GMT, because timestamp parameters in ACL are in GMT
120
164
  timezone='GMT',
165
+ # ACCESS_INFO can return ACL violation error if we use database name as symbol
166
+ query_properties={'IGNORE_TICKS_IN_UNENTITLED_TIME_RANGE': 'TRUE'},
121
167
  username=username,
122
168
  context=self.context)
123
169
  if not df.empty:
@@ -255,6 +301,20 @@ class DB:
255
301
  end = start + otp.Day(1)
256
302
  return self._fit_time_interval_in_acl(start, end, timezone)
257
303
 
304
+ @_method_cache
305
+ def _show_configured_time_ranges(self):
306
+ graph = otq.GraphQuery(otq.DbShowConfiguredTimeRanges(db_name=self.name).tick_type("ANY")
307
+ >> otq.Table(fields='long START_DATE, long END_DATE'))
308
+ result = otp.run(graph,
309
+ symbols=f'{self.name}::',
310
+ # start and end times don't matter for this query, use some constants
311
+ start=db_constants.DEFAULT_START_DATE,
312
+ end=db_constants.DEFAULT_END_DATE,
313
+ # GMT, because start/end timestamp in locator are in GMT
314
+ timezone='GMT',
315
+ context=self.context)
316
+ return result
317
+
258
318
  def _set_intervals(self):
259
319
  """
260
320
  Finds all date ranges from locators.
@@ -264,18 +324,7 @@ class DB:
264
324
  """
265
325
 
266
326
  if self._locator_date_ranges is None:
267
- graph = otq.GraphQuery(otq.DbShowConfiguredTimeRanges(db_name=self.name).tick_type("ANY")
268
- >> otq.Table(fields='long START_DATE, long END_DATE'))
269
-
270
- result = otp.run(graph,
271
- symbols=f'{self.name}::',
272
- # start and end times don't matter for this query, use some constants
273
- start=db_constants.DEFAULT_START_DATE,
274
- end=db_constants.DEFAULT_END_DATE,
275
- # GMT, because start/end timestamp in locator are in GMT
276
- timezone='GMT',
277
- context=self.context)
278
-
327
+ result = self._show_configured_time_ranges()
279
328
  date_ranges = []
280
329
 
281
330
  tz_gmt = gettz('GMT')
@@ -316,10 +365,20 @@ class DB:
316
365
 
317
366
  def _show_loaded_time_ranges(self, start, end, only_last=False, prefer_speed_over_accuracy=False):
318
367
  kwargs = {}
368
+ # PY-1421: we aim to make this query as fast as possible
369
+ # There are two problems with this EP:
370
+ # 1. executing this query without using cache
371
+ # and/or without setting prefer_speed_over_accuracy parameter
372
+ # may be very slow for big time range
373
+ # 2. using cache sometimes returns not precise results
374
+ # So in case prefer_speed_over_accuracy parameter is available we are disabling cache.
319
375
  if prefer_speed_over_accuracy:
320
- kwargs['prefer_speed_over_accuracy'] = prefer_speed_over_accuracy
376
+ kwargs['prefer_speed_over_accuracy'] = True
377
+ kwargs['use_cache'] = False
378
+ else:
379
+ kwargs['use_cache'] = True
321
380
 
322
- eps = otq.DbShowLoadedTimeRanges(use_cache=True, **kwargs).tick_type('ANY')
381
+ eps = otq.DbShowLoadedTimeRanges(**kwargs).tick_type('ANY')
323
382
  eps = eps >> otq.WhereClause(where='NUM_LOADED_PARTITIONS > 0')
324
383
  if only_last:
325
384
  eps = eps >> otq.LastTick()
@@ -528,32 +587,45 @@ class DB:
528
587
  ['QTE', 'TRD']
529
588
  """
530
589
  date = self.last_date if date is None else date
590
+
531
591
  if timezone is None:
532
592
  timezone = configuration.config.tz
533
- time_params: dict[str, Any] = {}
534
593
 
535
- if date is not None:
536
- time_params['start'], time_params['end'] = self._fit_date_in_acl(date, timezone=timezone)
594
+ if date is None:
595
+ # in the usual case it would mean that there is no data in the database,
596
+ # but _show_loaded_time_ranges doesn't return dates for database views
597
+ # in this case let's just try to get the database schema with default time range
598
+ start = end = utils.adaptive
599
+ # also it seems that show_schema=True doesn't work for views either
600
+ show_schema = False
601
+ else:
602
+ start, end = self._fit_date_in_acl(date, timezone=timezone) # type: ignore[assignment]
603
+ show_schema = True
537
604
 
538
605
  # PY-458: don't use cache, it can return different result in some cases
539
- result = otp.run(otq.DbShowTickTypes(use_cache=False,
540
- show_schema=False,
541
- include_memdb=True),
542
- symbols=f'{self.name}::',
543
- **time_params,
544
- timezone=timezone,
545
- context=self.context)
546
-
606
+ result = self._get_schema(use_cache=False, start=start, end=end, timezone=timezone, show_schema=show_schema)
547
607
  if len(result) == 0:
548
608
  return []
549
609
 
550
- return result['TICK_TYPE_NAME'].tolist()
610
+ return result['TICK_TYPE_NAME'].unique().tolist()
551
611
 
552
612
  def min_locator_date(self):
553
613
  self._set_intervals()
554
614
  min_date = min(obj[0] for obj in self._locator_date_ranges)
555
615
  return _datetime2date(min_date)
556
616
 
617
+ @_method_cache
618
+ def _get_schema(self, start, end, timezone, use_cache, show_schema):
619
+ ep = otq.DbShowTickTypes(use_cache=use_cache,
620
+ show_schema=show_schema,
621
+ include_memdb=True)
622
+ return otp.run(ep,
623
+ symbols=f'{self.name}::',
624
+ start=start,
625
+ end=end,
626
+ timezone=timezone,
627
+ context=self.context)
628
+
557
629
  def schema(self, date=None, tick_type=None, timezone=None, check_index_file=utils.adaptive) -> dict[str, type]:
558
630
  """
559
631
  Gets the schema of the database.
@@ -615,25 +687,18 @@ class DB:
615
687
 
616
688
  start, end = self._fit_date_in_acl(date, timezone=timezone)
617
689
 
618
- # TODO: refactor into global method, use in tick_types()
619
- def get_schema(use_cache: bool = True):
620
- return otp.run(otq.DbShowTickTypes(use_cache=use_cache,
621
- show_schema=True,
622
- include_memdb=True)
623
- >> otq.WhereClause(where=f'TICK_TYPE_NAME="{tick_type}"'),
624
- symbols=f'{self.name}::',
625
- start=start,
626
- end=end,
627
- timezone=timezone,
628
- context=self.context)
629
-
630
- result = get_schema(use_cache=True)
690
+ kwargs = dict(
691
+ start=start, end=end, timezone=timezone, show_schema=True,
692
+ )
693
+ # PY-458, BEXRTS-1220, PY-1421
694
+ # the results of the query may vary depending on using use_cache parameter, so we are trying both
695
+ result = self._get_schema(use_cache=False, **kwargs)
631
696
  if result.empty:
632
- # in case cache settings in database are bad (e.g. BEXRTS-1220)
633
- result = get_schema(use_cache=False)
697
+ result = self._get_schema(use_cache=True, **kwargs)
634
698
 
635
- fields: Iterable
699
+ fields: Iterable = []
636
700
  if len(result):
701
+ result = result[result['TICK_TYPE_NAME'] == tick_type]
637
702
  # filter schema by date
638
703
  date_to_filter = None
639
704
  if orig_date:
@@ -648,8 +713,6 @@ class DB:
648
713
  fields = zip(result['FIELD_NAME'].tolist(),
649
714
  result['FIELD_TYPE_NAME'].tolist(),
650
715
  result['FIELD_SIZE'].tolist())
651
- else:
652
- fields = []
653
716
 
654
717
  schema = {}
655
718
 
onetick/py/oqd/sources.py CHANGED
@@ -14,12 +14,29 @@ COMMON_SOURCE_DOC_PARAMS = [_start_doc, _end_doc, _symbol_doc]
14
14
  OQD_TICK_TYPE = 'OQD::*'
15
15
 
16
16
 
17
+ def _parse_time(time_expr):
18
+ # get datetime from string in OneTick
19
+ return f'parse_time("%Y%m%d %H:%M:%S.%q", {time_expr}, "GMT")'
20
+
21
+
22
+ def _get_start_time_start_of_day():
23
+ # getting the beggining of the start date day
24
+ # (e.g. from 2025-01-01 14:00:00 to 2025-01-01 00:00:00)
25
+ return _parse_time('time_format("%Y%m%d", _START_TIME, _TIMEZONE) + " 00:00:00.000"')
26
+
27
+
28
+ def _get_end_time_end_of_day():
29
+ # by default we change the end time to the end of the day
30
+ # (e.g. from 2025-01-01 15:00:00 to 2025-01-01 24:00:00)
31
+ end_of_day = _parse_time('time_format("%Y%m%d", _END_TIME, _TIMEZONE) + " 24:00:00.000"')
32
+ # but if we have end time as the start of the next day (e.g. 2025-01-02 00:00:00) then we keep it
33
+ return f'case(time_format("%H%M%S%J", _END_TIME, _TIMEZONE), "000000000000000", _END_TIME, {end_of_day})'
34
+
35
+
17
36
  def _modify_query_times(src):
18
37
  src.sink(otq.ModifyQueryTimes(
19
- start_time=('parse_time("%Y%m%d %H:%M:%S.%q", '
20
- 'time_format("%Y%m%d", _START_TIME, _TIMEZONE) + " 00:00:00.000", "GMT")'),
21
- end_time=('parse_time("%Y%m%d %H:%M:%S.%q", '
22
- 'time_format("%Y%m%d", _END_TIME, _TIMEZONE) + " 24:00:00.000", "GMT")'),
38
+ start_time=_get_start_time_start_of_day(),
39
+ end_time=_get_end_time_end_of_day(),
23
40
  output_timestamp='min(max(TIMESTAMP,_START_TIME),_END_TIME)'
24
41
  ))
25
42
 
@@ -111,7 +128,7 @@ class CorporateActions(otp.Source):
111
128
  actions for a symbol.
112
129
 
113
130
  This source will return all corporate action fields available for a symbol
114
- with EX-Dates between the query start time and end time. The
131
+ with EX-Dates between the query start time and end time (end time is not inclusive). The
115
132
  timestamp of the series is equal to the EX-Date of the corporate
116
133
  action with a time of 0:00:00 GMT.
117
134
 
@@ -130,9 +147,6 @@ class CorporateActions(otp.Source):
130
147
  CASH:0.205@USD NORMAL
131
148
  1 2021-05-07 9706 17098817 CASH_DIVIDEND 0.220 USD 20210428 20210507 20210513 20210510\
132
149
  CASH:0.22@USD NORMAL
133
- 2 2021-08-06 9706 17331864 CASH_DIVIDEND 0.220 USD 20210727 20210806 20210812 20210809\
134
- CASH:0.22@USD NORMAL
135
-
136
150
  """
137
151
 
138
152
  @docstring(parameters=COMMON_SOURCE_DOC_PARAMS, add_self=True)
onetick/py/otq.py CHANGED
@@ -58,7 +58,10 @@ elif otp.__webapi__:
58
58
 
59
59
  def run(*args, **kwargs):
60
60
  from onetick.py import config # noqa
61
- from onetick.py.compatibility import is_max_concurrency_with_webapi_supported
61
+ from onetick.py.compatibility import (
62
+ is_max_concurrency_with_webapi_supported,
63
+ is_webapi_access_token_scope_supported
64
+ )
62
65
 
63
66
  if not config.http_address and 'http_address' not in kwargs:
64
67
  raise ValueError('otp.run() http_address keyword param, '
@@ -149,7 +152,20 @@ elif otp.__webapi__:
149
152
  if not param_value:
150
153
  raise ValueError(f'`access_token_url` parameter set, however `{param_name}` parameter missing.')
151
154
 
152
- kwargs['access_token'] = otq.get_access_token(access_token_url, client_id, client_secret)
155
+ token_kwargs = {}
156
+
157
+ if 'scope' in kwargs:
158
+ scope = kwargs.pop('scope')
159
+ else:
160
+ scope = config.access_token_scope
161
+
162
+ if scope:
163
+ if not is_webapi_access_token_scope_supported():
164
+ raise RuntimeError('Parameter `scope` is not supported on used version of OneTick')
165
+
166
+ token_kwargs['scope'] = scope
167
+
168
+ kwargs['access_token'] = otq.get_access_token(access_token_url, client_id, client_secret, **token_kwargs)
153
169
 
154
170
  if 'access_token_url' in kwargs:
155
171
  del kwargs['access_token_url']
onetick/py/run.py CHANGED
@@ -465,9 +465,6 @@ def run(query: Union[Callable, Dict, otp.Source, otp.MultiOutputSource, # NOSON
465
465
  "by installed onetick.query_webapi library.")
466
466
 
467
467
  output_structure, output_structure_for_otq = _process_output_structure(output_structure)
468
- if symbol_date:
469
- # otq.run supports only strings and datetime.date
470
- symbol_date = utils.symbol_date_to_str(symbol_date)
471
468
 
472
469
  require_dict = require_dict or _is_dict_required(symbols)
473
470
 
@@ -495,6 +492,10 @@ def run(query: Union[Callable, Dict, otp.Source, otp.MultiOutputSource, # NOSON
495
492
  # we assume it's a dictionary of sources for the MultiOutputSource object
496
493
  query = otp.MultiOutputSource(query)
497
494
 
495
+ if symbol_date:
496
+ # otq.run supports only strings and datetime.date
497
+ symbol_date = utils.symbol_date_to_str(symbol_date)
498
+
498
499
  params_saved_to_otq = {}
499
500
  if isinstance(query, (otp.Source, otp.MultiOutputSource)):
500
501
  start = None if start is utils.adaptive else start
@@ -512,8 +513,9 @@ def run(query: Union[Callable, Dict, otp.Source, otp.MultiOutputSource, # NOSON
512
513
  end_time_expression=end_time_expression,
513
514
  require_dict=require_dict,
514
515
  running_query_flag=running,
515
- node_name=node_name, has_output=None)
516
- query, require_dict, node_name = param_upd
516
+ node_name=node_name, has_output=None,
517
+ symbol_date=symbol_date)
518
+ query, require_dict, node_name, symbol_date_to_run = param_upd
517
519
  # symbols and start/end times should be already stored in the query and should not be passed again
518
520
  symbols = None
519
521
  start = None
@@ -521,6 +523,8 @@ def run(query: Union[Callable, Dict, otp.Source, otp.MultiOutputSource, # NOSON
521
523
  start_time_expression = None
522
524
  end_time_expression = None
523
525
  time_as_nsec = True
526
+ # PY-1423: except for symbol date
527
+ symbol_date = symbol_date_to_run
524
528
 
525
529
  elif isinstance(query, (otq.graph_components.EpBase, otq.Chainlet)):
526
530
  query = otq.GraphQuery(query)
@@ -6,6 +6,7 @@ from .data_source import DataSource, Custom
6
6
  from .cache import ReadCache
7
7
  from .csv import CSV, LocalCSVTicks
8
8
  from .data_file import DataFile
9
+ from .dataframe import ReadFromDataFrame
9
10
  from .empty import Empty
10
11
  from .custom import Orders, Quotes, Trades, NBBO
11
12
  from .order_book import ObSnapshot, ObSnapshotFlat, ObSnapshotWide, ObSummary, ObSize, ObVwap, ObNumLevels