onetick-py 1.172.0__py3-none-any.whl → 1.174.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.
onetick/py/types.py CHANGED
@@ -2,15 +2,16 @@ import ctypes
2
2
  import functools
3
3
  import inspect
4
4
  import warnings
5
+ import decimal as _decimal
5
6
  from typing import Optional, Type, Union
6
- from packaging.version import parse as parse_version
7
-
8
- import pandas as pd
9
- import numpy as np
10
7
  from datetime import date as _date
11
8
  from datetime import datetime as _datetime
9
+ from datetime import timedelta as _timedelta
12
10
 
11
+ import pandas as pd
12
+ import numpy as np
13
13
  from pandas.tseries import offsets
14
+ from packaging.version import parse as parse_version
14
15
 
15
16
  import onetick.py as otp
16
17
  from onetick.py.otq import otq, pyomd
@@ -707,18 +708,23 @@ class _inf(float, metaclass=_nan_base):
707
708
  inf = _inf()
708
709
 
709
710
 
710
- class _decimal_str(type):
711
- def __str__(cls):
712
- return 'decimal'
713
-
714
-
715
- class decimal(float, metaclass=_decimal_str):
711
+ class decimal:
716
712
  """
717
713
  Object that represents decimal OneTick value.
718
714
  Decimal is 128 bit base 10 floating point number.
719
715
 
716
+ Parameters
717
+ ----------
718
+ value: int, float, str
719
+ The value to initialize decimal from.
720
+ Note that float values may be converted with precision lost.
721
+
720
722
  Examples
721
723
  --------
724
+
725
+ :py:class:`~onetick.py.types.decimal` objects can be used in tick generators
726
+ and column operations as any other onetick-py type:
727
+
722
728
  >>> t = otp.Ticks({'A': [otp.decimal(1), otp.decimal(2)]})
723
729
  >>> t['B'] = otp.decimal(1.23456789)
724
730
  >>> t['C'] = t['A'] / 0
@@ -727,43 +733,107 @@ class decimal(float, metaclass=_decimal_str):
727
733
  Time A B C D
728
734
  0 2003-12-01 00:00:00.000 1.0 1.234568 inf NaN
729
735
  1 2003-12-01 00:00:00.001 2.0 1.234568 inf NaN
730
- """
731
- def __wrap(self, res):
732
- # if parent class doesn't support some operation, it returns NotImplemented and so do we
733
- # In other case we wrap float result with our decimal class
734
- if isinstance(res, type(NotImplemented)):
735
- return NotImplemented
736
- return self.__class__(res)
737
736
 
738
- def __add__(self, other):
739
- return self.__wrap(super().__add__(other))
740
-
741
- def __radd__(self, other):
742
- return self.__wrap(super().__radd__(other))
743
-
744
- def __sub__(self, other):
745
- return self.__wrap(super().__sub__(other))
737
+ Additionally, any arithmetic operation with :py:class:`~onetick.py.types.decimal` object will return
738
+ an :py:class:`~onetick.py.Operation` object:
746
739
 
747
- def __rsub__(self, other):
748
- return self.__wrap(super().__rsub__(other))
740
+ >>> t = otp.Tick(A=1)
741
+ >>> t['X'] = otp.decimal(1) / 0
742
+ >>> otp.run(t)
743
+ Time A X
744
+ 0 2003-12-01 1 inf
745
+
746
+ Note that converting from float (first row) may result in losing precision.
747
+ :py:class:`~onetick.py.types.decimal` objects are created from strings or integers, so they don't lose precision:
748
+
749
+ >>> t0 = otp.Tick(A=0.1)
750
+ >>> t1 = otp.Tick(A=otp.decimal(0.01))
751
+ >>> t2 = otp.Tick(A=otp.decimal('0.001'))
752
+ >>> t3 = otp.Tick(A=otp.decimal(1) / otp.decimal(10_000))
753
+ >>> t = otp.merge([t0, t1, t2, t3], enforce_order=True)
754
+ >>> t['STR_A'] = t['A'].decimal.str(34)
755
+ >>> otp.run(t)
756
+ Time A STR_A
757
+ 0 2003-12-01 0.1000 0.1000000000000000055511151231257827
758
+ 1 2003-12-01 0.0100 0.0100000000000000000000000000000000
759
+ 2 2003-12-01 0.0010 0.0010000000000000000000000000000000
760
+ 3 2003-12-01 0.0001 0.0001000000000000000000000000000000
749
761
 
750
- def __mul__(self, other):
751
- return self.__wrap(super().__mul__(other))
762
+ Note that :py:class:`otp.Ticks <onetick.py.Ticks>` will convert everything from string under the hood,
763
+ so even the float values will not lose precision:
752
764
 
753
- def __rmul__(self, other):
754
- return self.__wrap(super().__rmul__(other))
765
+ >>> t = otp.Ticks({'A': [0.1, otp.decimal(0.01), otp.decimal('0.001'), otp.decimal(1e-4)]})
766
+ >>> t['STR_A'] = t['A'].decimal.str(34)
767
+ >>> otp.run(t)
768
+ Time A STR_A
769
+ 0 2003-12-01 00:00:00.000 0.1000 0.1000000000000000000000000000000000
770
+ 1 2003-12-01 00:00:00.001 0.0100 0.0100000000000000000000000000000000
771
+ 2 2003-12-01 00:00:00.002 0.0010 0.0010000000000000000000000000000000
772
+ 3 2003-12-01 00:00:00.003 0.0001 0.0001000000000000000000000000000000
773
+ """
774
+ def __new__(cls, *args, **kwargs):
775
+ # this method dynamically adds properties and methods
776
+ # from otp.Operation class to this one
777
+
778
+ # otp.decimal class doesn't fit well in onetick-py type system,
779
+ # so this class is a mix of both type and Operation logic
780
+
781
+ # Basically it works like this:
782
+ # otp.decimal is a OneTick type
783
+ # otp.decimal(1) is a decimal type object
784
+ # Doing anything with this object returns an otp.Operation:
785
+ # otp.decimal(1) / 2
786
+
787
+ def proxy_wrap(attr, value):
788
+ if callable(value):
789
+ @functools.wraps(value)
790
+ def f(self, *args, **kwargs):
791
+ op = self.to_operation()
792
+ return getattr(op, attr)(*args, **kwargs)
793
+ return f
794
+ else:
795
+ @functools.wraps(value)
796
+ def f(self):
797
+ op = self.to_operation()
798
+ return getattr(op, attr)
799
+ return property(f)
800
+
801
+ for attr, value in inspect.getmembers(otp.Operation):
802
+ # comparison methods are defined by default for some reason,
803
+ # but we want to get them from otp.Operation
804
+ if not hasattr(cls, attr) or attr in ('__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__'):
805
+ setattr(cls, attr, proxy_wrap(attr, value))
806
+
807
+ return super().__new__(cls)
808
+
809
+ def __init__(self, value):
810
+ supported_types = (str, int, float)
811
+ if not isinstance(value, supported_types):
812
+ raise TypeError("Parameter 'value' must be one of these types: {supported_types}")
813
+ self.__value = value
814
+
815
+ @classmethod
816
+ def _to_onetick_type_string(cls):
817
+ # called by ott.type2str
818
+ return 'decimal'
755
819
 
756
- def __truediv__(self, other):
757
- return self.__wrap(super().__truediv__(other))
820
+ def _to_onetick_string(self):
821
+ # called by ott.value2str
822
+ value = str(self.__value)
823
+ return f'STRING_TO_DECIMAL({value2str(value)})'
758
824
 
759
- def __rtruediv__(self, other):
760
- return self.__wrap(super().__rtruediv__(other))
825
+ def to_operation(self):
826
+ return otp.Operation(op_str=self._to_onetick_string(), dtype=decimal)
761
827
 
762
828
  def __str__(self):
763
- return super().__repr__()
829
+ # called by otp.CSV, we don't need to convert the value with OneTick functions in this case
830
+ return str(self.__value)
764
831
 
765
832
  def __repr__(self):
766
- return f"{self.__class__.__name__}({self})"
833
+ return f"{self.__class__.__name__}({value2str(self.__value)})"
834
+
835
+ def __format__(self, __format_spec: str) -> str:
836
+ return _decimal.Decimal(self.__value).__format__(__format_spec)
767
837
 
768
838
  # --------------------------------------------------------------- #
769
839
  # AUXILIARY FUNCTIONS
@@ -809,7 +879,7 @@ def get_source_base_type(value):
809
879
  value_type = nsectime
810
880
 
811
881
  # check valid value type
812
- if get_base_type(value_type) not in [int, float, str, bool]:
882
+ if get_base_type(value_type) not in [int, float, str, bool, decimal]:
813
883
  raise TypeError(f'Type "{repr(value_type)}" is not supported.')
814
884
 
815
885
  if not is_type_basic(value_type):
@@ -818,7 +888,7 @@ def get_source_base_type(value):
818
888
 
819
889
 
820
890
  def is_type_supported(dtype):
821
- return get_base_type(dtype) in [int, float, str, bool] or issubclass(dtype, (datetime, date))
891
+ return get_base_type(dtype) in [int, float, str, bool, decimal] or issubclass(dtype, (datetime, date))
822
892
 
823
893
 
824
894
  def get_base_type(obj):
@@ -830,6 +900,8 @@ def get_base_type(obj):
830
900
  return int
831
901
  elif issubclass(obj, float):
832
902
  return float
903
+ elif issubclass(obj, decimal):
904
+ return decimal
833
905
 
834
906
  return type(None)
835
907
 
@@ -1095,7 +1167,9 @@ class datetime(AbstractTime):
1095
1167
  def _process_timezones_args(self, tz, tzinfo):
1096
1168
  if tz is not None:
1097
1169
  if tzinfo is None:
1098
- tzinfo = get_tzfile_by_name(tz) # pandas is broken https://github.com/pandas-dev/pandas/issues/31929
1170
+ # parameter tz in pandas.Timestamp is broken https://github.com/pandas-dev/pandas/issues/31929
1171
+ # it is fixed in pandas>=2.0.0, but we need to support older versions
1172
+ tzinfo = get_tzfile_by_name(tz)
1099
1173
  tz = None
1100
1174
  else:
1101
1175
  raise ValueError(
@@ -1684,6 +1758,8 @@ def type2str(t):
1684
1758
  return "double"
1685
1759
  if t is None:
1686
1760
  return ''
1761
+ if t is decimal:
1762
+ return t._to_onetick_type_string()
1687
1763
  return str(t)
1688
1764
 
1689
1765
 
@@ -1839,15 +1915,16 @@ def value2str(v):
1839
1915
  # there is no escape, so replacing double quotes with concatenation with it
1840
1916
  return '"' + str(v).replace('"', '''"+'"'+"''') + '"'
1841
1917
 
1842
- if isinstance(v, (float, decimal)) and not (isinstance(v, (_inf, _nan))):
1918
+ if isinstance(v, decimal):
1919
+ return v._to_onetick_string()
1920
+
1921
+ if isinstance(v, float) and not (isinstance(v, (_inf, _nan))):
1843
1922
  # PY-286: support science notation
1844
1923
  s = str(v)
1845
1924
  if "e" in s:
1846
- s = f"{v:.20f}".rstrip("0")
1925
+ return f'atof({value2str(s)})'
1847
1926
  if s == "nan":
1848
1927
  return str(nan)
1849
- if isinstance(v, decimal):
1850
- return f'DECIMAL({s})'
1851
1928
  return s
1852
1929
 
1853
1930
  if is_time_type(v):
@@ -1936,21 +2013,12 @@ def is_time_type(time):
1936
2013
 
1937
2014
  def next_day(dt_obj: Union[_date, _datetime, date, datetime, pd.Timestamp]) -> _datetime:
1938
2015
  """
1939
- Return next day of ``dt_obj`` as datetime.datetime.
1940
- """
1941
- updated_dt = dt_obj + Day(1)
1942
-
1943
- # If we switch ts on timezone transition time, ts changes its timezone offset
1944
- if hasattr(dt_obj, "tzinfo") and updated_dt.tzinfo != dt_obj.tzinfo:
1945
- dt_offset = dt_obj.tzinfo.utcoffset(dt_obj)
1946
- updated_dt_offset = updated_dt.tzinfo.utcoffset(updated_dt)
1947
-
1948
- tz_diff = dt_offset - updated_dt_offset
1949
- updated_dt += tz_diff
2016
+ Return the start of the next day of ``dt_obj`` as timezone-naive :py:class:`datetime.datetime`.
1950
2017
 
1951
- updated_date = updated_dt.date()
1952
-
1953
- return _datetime(updated_date.year, updated_date.month, updated_date.day)
2018
+ Next day in this case means simply incrementing day/month/year number,
2019
+ not adding 24 hours (which may return the same date on DST days).
2020
+ """
2021
+ return _datetime(dt_obj.year, dt_obj.month, dt_obj.day) + _timedelta(days=1)
1954
2022
 
1955
2023
 
1956
2024
  def default_by_type(dtype):
@@ -1967,7 +2035,7 @@ def default_by_type(dtype):
1967
2035
  >>> otp.default_by_type(float)
1968
2036
  nan
1969
2037
  >>> otp.default_by_type(otp.decimal)
1970
- decimal(0.0)
2038
+ decimal(0)
1971
2039
  >>> otp.default_by_type(int)
1972
2040
  0
1973
2041
  >>> otp.default_by_type(otp.ulong)