-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathfval.py
180 lines (136 loc) · 7.07 KB
/
fval.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
from decimal import Decimal, InvalidOperation
from typing import Any, Union
from rotkehlchen.errors import ConversionError
# Here even though we got __future__ annotations using FVal does not seem to work
AcceptableFValInitInput = Union[float, bytes, Decimal, int, str, 'FVal']
AcceptableFValOtherInput = Union[int, 'FVal']
class FVal():
"""A value to represent numbers for financial applications. At the moment
we use the python Decimal library but the abstraction will help us change the
underlying implementation if needed.
At the moment we do not allow any operations against floating points. Even though
floating points could be converted to Decimals before each operation we will
use this restriction to make sure floating point numbers are rooted from the codebase first.
"""
__slots__ = ('num',)
def __init__(self, data: AcceptableFValInitInput = 0):
try:
if isinstance(data, float):
self.num = Decimal(str(data))
elif isinstance(data, bytes):
# assume it's an ascii string and try to decode the bytes to one
self.num = Decimal(data.decode())
elif isinstance(data, bool): # type: ignore
# This elif has to come before the isinstance(int) check due to
# https://stackoverflow.com/questions/37888620/comparing-boolean-and-int-using-isinstance
raise ValueError('Invalid type bool for data given to FVal constructor')
elif isinstance(data, (Decimal, int, str)):
self.num = Decimal(data)
elif isinstance(data, FVal):
self.num = data.num
else:
raise ValueError(f'Invalid type {type(data)} of data given to FVal constructor')
except InvalidOperation:
raise ValueError(
'Expected string, int, float, or Decimal to initialize an FVal.'
'Found {}.'.format(type(data)),
)
def __str__(self) -> str:
return str(self.num)
def __repr__(self) -> str:
return 'FVal({})'.format(str(self.num))
def __gt__(self, other: AcceptableFValOtherInput) -> bool:
evaluated_other = evaluate_input(other)
return self.num.compare_signal(evaluated_other) == Decimal('1')
def __lt__(self, other: AcceptableFValOtherInput) -> bool:
evaluated_other = evaluate_input(other)
return self.num.compare_signal(evaluated_other) == Decimal('-1')
def __le__(self, other: AcceptableFValOtherInput) -> bool:
evaluated_other = evaluate_input(other)
return self.num.compare_signal(evaluated_other) in (Decimal('-1'), Decimal('0'))
def __ge__(self, other: AcceptableFValOtherInput) -> bool:
evaluated_other = evaluate_input(other)
return self.num.compare_signal(evaluated_other) in (Decimal('1'), Decimal('0'))
def __eq__(self, other: object) -> bool:
evaluated_other = evaluate_input(other)
return self.num.compare_signal(evaluated_other) == Decimal('0')
def __add__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__add__(evaluated_other))
def __sub__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__sub__(evaluated_other))
def __mul__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__mul__(evaluated_other))
def __truediv__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__truediv__(evaluated_other))
def __floordiv__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__floordiv__(evaluated_other))
def __pow__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__pow__(evaluated_other))
def __radd__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__radd__(evaluated_other))
def __rsub__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__rsub__(evaluated_other))
def __rmul__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__rmul__(evaluated_other))
def __rtruediv__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__rtruediv__(evaluated_other))
def __rfloordiv__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__rfloordiv__(evaluated_other))
def __mod__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__mod__(evaluated_other))
def __rmod__(self, other: AcceptableFValOtherInput) -> 'FVal':
evaluated_other = evaluate_input(other)
return FVal(self.num.__rmod__(evaluated_other))
def __float__(self) -> float:
return float(self.num)
# --- Unary operands
def __neg__(self) -> 'FVal':
return FVal(self.num.__neg__())
def __abs__(self) -> 'FVal':
return FVal(self.num.copy_abs())
# --- Other operations
def fma(self, other: AcceptableFValOtherInput, third: AcceptableFValOtherInput) -> 'FVal':
"""
Fused multiply-add. Return self*other+third with no rounding of the
intermediate product self*other
"""
evaluated_other = evaluate_input(other)
evaluated_third = evaluate_input(third)
return FVal(self.num.fma(evaluated_other, evaluated_third))
def to_percentage(self, precision: int = 4) -> str:
return '{:.{}%}'.format(self.num, precision)
def to_int(self, exact: bool) -> int:
"""
Tries to convert to int, If `exact` is true then it will convert only if
it is a whole decimal number; i.e.: if it has got nothing after the decimal point
Raises:
ConversionError: If exact was True but the FVal is actually not an exact integer.
"""
if exact and self.num.to_integral_exact() != self.num:
raise ConversionError(f'Tried to ask for exact int from {self.num}')
return int(self.num)
def is_close(self, other: AcceptableFValInitInput, max_diff: str = "1e-6") -> bool:
evaluated_max_diff = FVal(max_diff)
if not isinstance(other, FVal):
other = FVal(other)
diff_num = abs(self.num - other.num)
return diff_num <= evaluated_max_diff.num
def evaluate_input(other: Any) -> Union[Decimal, int]:
"""Evaluate 'other' and return its Decimal representation"""
if isinstance(other, FVal):
return other.num
elif not isinstance(other, int):
raise NotImplementedError("Expected either FVal or int.")
return other