mirror of
https://github.com/wassname/pandas-ta.git
synced 2026-07-30 12:20:22 +08:00
457 lines
13 KiB
Python
457 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
import math
|
|
from pathlib import Path
|
|
from time import perf_counter
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from functools import reduce
|
|
from operator import mul
|
|
from sys import float_info as sflt
|
|
|
|
TRADING_DAYS_PER_YEAR = 251
|
|
TRADING_HOURS_PER_DAY = 6.5
|
|
MINUTES_PER_HOUR = 60
|
|
|
|
|
|
def _above_below(
|
|
series_a: pd.Series,
|
|
series_b: pd.Series,
|
|
above: bool = True,
|
|
asint: bool = True,
|
|
offset: int = None,
|
|
**kwargs
|
|
):
|
|
series_a = verify_series(series_a)
|
|
series_b = verify_series(series_b)
|
|
offset = get_offset(offset)
|
|
|
|
series_a.apply(zero)
|
|
series_b.apply(zero)
|
|
|
|
# Calculate Result
|
|
if above:
|
|
current = series_a >= series_b
|
|
else:
|
|
current = series_a <= series_b
|
|
|
|
if asint:
|
|
current = current.astype(int)
|
|
|
|
# Offset
|
|
if offset != 0:
|
|
current = current.shift(offset)
|
|
|
|
# Name & Category
|
|
current.name = f"{series_a.name}_{'A' if above else 'B'}_{series_b.name}"
|
|
current.category = 'utility'
|
|
|
|
return current
|
|
|
|
|
|
def above(
|
|
series_a: pd.Series,
|
|
series_b: pd.Series,
|
|
asint: bool = True,
|
|
offset: int = None,
|
|
**kwargs
|
|
):
|
|
return _above_below(series_a, series_b, above=True, asint=asint, offset=offset, **kwargs)
|
|
|
|
|
|
def above_value(
|
|
series_a: pd.Series,
|
|
value: float,
|
|
asint: bool = True,
|
|
offset: int = None,
|
|
**kwargs
|
|
):
|
|
if not isinstance(value, (int, float, complex)):
|
|
print("[X] value is not a number")
|
|
return
|
|
series_b = pd.Series(value, index=series_a.index, name=f"{value}".replace('.','_'))
|
|
return _above_below(series_a, series_b, above=True, asint=asint, offset=offset, **kwargs)
|
|
|
|
|
|
def below(
|
|
series_a: pd.Series,
|
|
series_b: pd.Series,
|
|
asint: bool =True,
|
|
offset: int =None
|
|
,**kwargs
|
|
):
|
|
return _above_below(series_a, series_b, above=False, asint=asint, offset=offset, **kwargs)
|
|
|
|
|
|
def below_value(
|
|
series_a: pd.Series,
|
|
value: float,
|
|
asint: bool = True,
|
|
offset: int = None,
|
|
**kwargs
|
|
):
|
|
if not isinstance(value, (int, float, complex)):
|
|
print("[X] value is not a number")
|
|
return
|
|
series_b = pd.Series(value, index=series_a.index, name=f"{value}".replace('.','_'))
|
|
return _above_below(series_a, series_b, above=False, asint=asint, offset=offset, **kwargs)
|
|
|
|
|
|
def category_files(category: str) -> list:
|
|
"""Helper function to return all filenames in the category directory."""
|
|
files = [x.stem for x in list(Path(f"pandas_ta/{category}/").glob("*.py")) if x.stem != "__init__"]
|
|
return files
|
|
|
|
|
|
def combination(**kwargs):
|
|
"""https://stackoverflow.com/questions/4941753/is-there-a-math-ncr-function-in-python"""
|
|
n = int(math.fabs(kwargs.pop('n', 1)))
|
|
r = int(math.fabs(kwargs.pop('r', 0)))
|
|
|
|
if kwargs.pop('repetition', False) or kwargs.pop('multichoose', False):
|
|
n = n + r - 1
|
|
|
|
# if r < 0: return None
|
|
r = min(n, n - r)
|
|
if r == 0:
|
|
return 1
|
|
|
|
numerator = reduce(mul, range(n, n - r, -1), 1)
|
|
denominator = reduce(mul, range(1, r + 1), 1)
|
|
return numerator // denominator
|
|
|
|
|
|
def cross_value(
|
|
series_a: pd.Series,
|
|
value: float,
|
|
above: bool = True,
|
|
asint: bool = True,
|
|
offset: int = None,
|
|
**kwargs
|
|
):
|
|
series_b = pd.Series(value, index=series_a.index, name=f"{value}".replace('.','_'))
|
|
return cross(series_a, series_b, above, asint, offset, **kwargs)
|
|
|
|
|
|
def cross(
|
|
series_a: pd.Series,
|
|
series_b: pd.Series,
|
|
above: bool = True,
|
|
asint: bool = True,
|
|
offset: int = None,
|
|
**kwargs
|
|
):
|
|
series_a = verify_series(series_a)
|
|
series_b = verify_series(series_b)
|
|
offset = get_offset(offset)
|
|
|
|
series_a.apply(zero)
|
|
series_b.apply(zero)
|
|
|
|
# Calculate Result
|
|
current = series_a > series_b # current is above
|
|
previous = series_a.shift(1) < series_b.shift(1) # previous is below
|
|
# above if both are true, below if both are false
|
|
cross = current & previous if above else ~current & ~previous
|
|
|
|
if asint:
|
|
cross = cross.astype(int)
|
|
|
|
# Offset
|
|
if offset != 0:
|
|
cross = cross.shift(offset)
|
|
|
|
# Name & Category
|
|
cross.name = f"{series_a.name}_{'XA' if above else 'XB'}_{series_b.name}"
|
|
cross.category = 'utility'
|
|
|
|
return cross
|
|
|
|
|
|
def is_datetime_ordered(df: pd.DataFrame or pd.Series) -> bool:
|
|
"""Returns True if the index is a datetime and ordered."""
|
|
index_is_datetime = pd.api.types.is_datetime64_any_dtype(df.index)
|
|
try:
|
|
ordered = df.index[0] < df.index[-1]
|
|
except RuntimeWarning: pass
|
|
finally:
|
|
return True if index_is_datetime and ordered else False
|
|
|
|
|
|
def signals(indicator, xa, xb, cross_values, xserie, xserie_a, xserie_b, cross_series, offset) -> pd.DataFrame:
|
|
df = pd.DataFrame()
|
|
if xa is not None and isinstance(xa, (int, float)):
|
|
if cross_values:
|
|
crossed_above_start = cross_value(indicator, xa, above=True, offset=offset)
|
|
crossed_above_end = cross_value(indicator, xa, above=False, offset=offset)
|
|
df[crossed_above_start.name] = crossed_above_start
|
|
df[crossed_above_end.name] = crossed_above_end
|
|
else:
|
|
crossed_above = above_value(indicator, xa, offset=offset)
|
|
df[crossed_above.name] = crossed_above
|
|
|
|
if xb is not None and isinstance(xb, (int, float)):
|
|
if cross_values:
|
|
crossed_below_start = cross_value(indicator, xb, above=True, offset=offset)
|
|
crossed_below_end = cross_value(indicator, xb, above=False, offset=offset)
|
|
df[crossed_below_start.name] = crossed_below_start
|
|
df[crossed_below_end.name] = crossed_below_end
|
|
else:
|
|
crossed_below = below_value(indicator, xb, offset=offset)
|
|
df[crossed_below.name] = crossed_below
|
|
|
|
# xseries is the default value for both xserie_a and xserie_b
|
|
if xserie_a is None:
|
|
xserie_a = xserie
|
|
if xserie_b is None:
|
|
xserie_b = xserie
|
|
|
|
if xserie_a is not None and verify_series(xserie_a):
|
|
if cross_series:
|
|
cross_serie_above = cross(indicator, xserie_a, above=True, offset=offset)
|
|
else:
|
|
cross_serie_above = above(indicator, xserie_a, offset=offset)
|
|
|
|
df[cross_serie_above.name] = cross_serie_above
|
|
|
|
if xserie_b is not None and verify_series(xserie_b):
|
|
if cross_series:
|
|
cross_serie_below = cross(indicator, xserie_b, above=False, offset=offset)
|
|
else:
|
|
cross_serie_below = below(indicator, xserie_b, offset=offset)
|
|
|
|
df[cross_serie_below.name] = cross_serie_below
|
|
|
|
return df
|
|
|
|
|
|
def df_error_analysis(dfA: pd.DataFrame, dfB: pd.DataFrame, **kwargs) -> pd.DataFrame:
|
|
""" """
|
|
col = kwargs.pop('col', None)
|
|
corr_method = kwargs.pop('corr_method', 'pearson')
|
|
|
|
# Find their differences
|
|
diff = dfA - dfB
|
|
df = pd.DataFrame({'diff': diff.describe()})
|
|
extra = pd.DataFrame(
|
|
[diff.var(), diff.mad(), diff.sem(), dfA.corr(dfB, method=corr_method)],
|
|
index=['var', 'mad', 'sem', 'corr']
|
|
)
|
|
|
|
# Append the differences to the DataFrame
|
|
df = df['diff'].append(extra, ignore_index=False)[0]
|
|
|
|
# For plotting
|
|
if kwargs.pop('plot', False):
|
|
diff.hist()
|
|
if diff[diff > 0].any():
|
|
diff.plot(kind='kde')
|
|
|
|
if col is not None:
|
|
return df[col]
|
|
else:
|
|
return df
|
|
|
|
|
|
def fibonacci(**kwargs) -> np.ndarray:
|
|
"""Fibonacci Sequence as a numpy array"""
|
|
n = int(math.fabs(kwargs.pop('n', 2)))
|
|
zero = kwargs.pop('zero', False)
|
|
weighted = kwargs.pop('weighted', False)
|
|
|
|
if zero:
|
|
a, b = 0, 1
|
|
else:
|
|
n -= 1
|
|
a, b = 1, 1
|
|
|
|
result = np.array([a])
|
|
for i in range(0, n):
|
|
a, b = b, a + b
|
|
result = np.append(result, a)
|
|
|
|
if weighted:
|
|
fib_sum = np.sum(result)
|
|
if fib_sum > 0:
|
|
return result / fib_sum
|
|
else:
|
|
return result
|
|
else:
|
|
return result
|
|
|
|
|
|
def final_time(stime):
|
|
time_diff = perf_counter() - stime
|
|
return f"{time_diff * 1000:2.4f} ms ({time_diff:2.4f} s)"
|
|
|
|
|
|
def get_drift(x: int) -> int:
|
|
"""Returns an int if not zero, otherwise defaults to one."""
|
|
return int(x) if x and x != 0 else 1
|
|
|
|
|
|
def get_offset(x: int) -> int:
|
|
"""Returns an int, otherwise defaults to zero."""
|
|
return int(x) if x else 0
|
|
|
|
|
|
def is_percent(x: int or float) -> bool:
|
|
if isinstance(x, (int, float)):
|
|
return x is not None and x >= 0 and x <= 100
|
|
return False
|
|
|
|
|
|
def non_zero_range(high: pd.Series, low: pd.Series) -> pd.Series:
|
|
"""Returns the difference of two series and adds epsilon if
|
|
to any zero values. This occurs commonly in crypto data when
|
|
high = low.
|
|
"""
|
|
diff = high - low
|
|
if diff.eq(0).any().any():
|
|
diff += sflt.epsilon
|
|
return diff
|
|
|
|
|
|
def pascals_triangle(n: int = None, **kwargs) -> np.ndarray:
|
|
"""Pascal's Triangle
|
|
|
|
Returns a numpy array of the nth row of Pascal's Triangle.
|
|
n=4 => triangle: [1, 4, 6, 4, 1]
|
|
=> weighted: [0.0625, 0.25, 0.375, 0.25, 0.0625]
|
|
=> inverse weighted: [0.9375, 0.75, 0.625, 0.75, 0.9375]
|
|
"""
|
|
n = int(math.fabs(n)) if n is not None else 0
|
|
weighted = kwargs.pop('weighted', False)
|
|
inverse = kwargs.pop('inverse', False)
|
|
|
|
# Calculation
|
|
triangle = np.array([combination(n=n, r=i) for i in range(0, n + 1)])
|
|
triangle_sum = np.sum(triangle)
|
|
triangle_weights = triangle / triangle_sum
|
|
inverse_weights = 1 - triangle_weights
|
|
|
|
if weighted and inverse:
|
|
return inverse_weights
|
|
if weighted:
|
|
return triangle_weights
|
|
if inverse:
|
|
return None
|
|
|
|
return triangle
|
|
|
|
|
|
def recent_maximum_index(x):
|
|
return int(np.argmax(x[::-1]))
|
|
|
|
|
|
def recent_minimum_index(x):
|
|
return int(np.argmin(x[::-1]))
|
|
|
|
|
|
def signed_series(series: pd.Series, initial: int =None) -> pd.Series:
|
|
"""Returns a Signed Series with or without an initial value
|
|
|
|
Default Example:
|
|
series = pd.Series([3, 2, 2, 1, 1, 5, 6, 6, 7, 5])
|
|
and returns:
|
|
sign = pd.Series([NaN, -1.0, 0.0, -1.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0])
|
|
"""
|
|
series = verify_series(series)
|
|
sign = series.diff(1)
|
|
sign[sign > 0] = 1
|
|
sign[sign < 0] = -1
|
|
sign.iloc[0] = initial
|
|
return sign
|
|
|
|
|
|
def symmetric_triangle(n: int = None, **kwargs) -> list:
|
|
"""Symmetric Triangle with n >= 2
|
|
|
|
Returns a numpy array of the nth row of Symmetric Triangle.
|
|
n=4 => triangle: [1, 2, 2, 1]
|
|
=> weighted: [0.16666667 0.33333333 0.33333333 0.16666667]
|
|
"""
|
|
n = int(math.fabs(n)) if n is not None else 2
|
|
weighted = kwargs.pop('weighted', False)
|
|
|
|
if n == 2:
|
|
triangle = [1, 1]
|
|
|
|
if n > 2:
|
|
if n % 2 == 0:
|
|
front = [i + 1 for i in range(0, math.floor(n/2))]
|
|
triangle = front + front[::-1]
|
|
else:
|
|
front = [i + 1 for i in range(0, math.floor(0.5 * (n + 1)))]
|
|
triangle = front.copy()
|
|
front.pop()
|
|
triangle += front[::-1]
|
|
|
|
if weighted:
|
|
triangle_sum = np.sum(triangle)
|
|
triangle_weights = triangle / triangle_sum
|
|
return triangle_weights
|
|
|
|
return triangle
|
|
|
|
|
|
def unsigned_differences(series: pd.Series, amount: int = None, **kwargs) -> pd.Series:
|
|
"""Unsigned Differences
|
|
Returns two Series, an unsigned positive and unsigned negative series based
|
|
on the differences of the original series. The positive series are only the
|
|
increases and the negative series is only the decreases.
|
|
|
|
Default Example:
|
|
series = pd.Series([3, 2, 2, 1, 1, 5, 6, 6, 7, 5, 3]) and returns
|
|
postive = pd.Series([0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0])
|
|
negative = pd.Series([0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1])
|
|
"""
|
|
amount = int(amount) if amount is not None else 1
|
|
negative = series.diff(amount)
|
|
negative.fillna(0, inplace=True)
|
|
positive = negative.copy()
|
|
|
|
positive[positive <= 0] = 0
|
|
positive[positive > 0] = 1
|
|
|
|
negative[negative >= 0] = 0
|
|
negative[negative < 0] = 1
|
|
|
|
if kwargs.pop('asint', False):
|
|
positive = positive.astype(int)
|
|
negative = negative.astype(int)
|
|
|
|
return positive, negative
|
|
|
|
|
|
def verify_series(series: pd.Series) -> pd.Series:
|
|
"""If a Pandas Series return it."""
|
|
if series is not None and isinstance(series, pd.core.series.Series):
|
|
return series
|
|
|
|
|
|
def weights(w):
|
|
def _dot(x):
|
|
return np.dot(w, x)
|
|
return _dot
|
|
|
|
|
|
def zero(x: [int, float]) -> [int, float]:
|
|
"""If the value is close to zero, then return zero.
|
|
Otherwise return the value."""
|
|
return 0 if -sflt.epsilon < x and x < sflt.epsilon else x
|
|
|
|
# Candle Functions
|
|
|
|
def candle_color(open_, close):
|
|
color = close.copy().astype(int)
|
|
color[close >= open_] = 1
|
|
color[close < open_] = -1
|
|
return color
|
|
|
|
def real_body(open_, close):
|
|
return non_zero_range(open_, close)
|
|
|
|
def high_low_range(high, low):
|
|
return non_zero_range(high, low) |