PandasTA 原始碼解析(十四)

绝不原创的飞龙發表於2024-04-15

.\pandas-ta\pandas_ta\trend\xsignals.py

# -*- coding: utf-8 -*-
# 從 numpy 中匯入 nan 並重新命名為 npNaN
from numpy import nan as npNaN
# 從 pandas 中匯入 DataFrame
from pandas import DataFrame
# 從當前包中匯入 tsignals 模組
from .tsignals import tsignals
# 從 pandas_ta.utils._signals 中匯入 cross_value 函式
from pandas_ta.utils._signals import cross_value
# 從 pandas_ta.utils 中匯入 get_offset 和 verify_series 函式
from pandas_ta.utils import get_offset, verify_series

# 定義函式 xsignals,用於計算交叉訊號
def xsignals(signal, xa, xb, above:bool=True, long:bool=True, asbool:bool=None, trend_reset:int=0, trade_offset:int=None, offset:int=None, **kwargs):
    """Indicator: Cross Signals"""
    # 驗證引數
    signal = verify_series(signal)
    offset = get_offset(offset)

    # 計算結果
    if above:
        # 如果 above 為 True,計算 signal 與 xa 交叉的位置
        entries = cross_value(signal, xa)
        # 計算 signal 與 xb 交叉的位置,注意指定 above=False
        exits = -cross_value(signal, xb, above=False)
    else:
        # 如果 above 為 False,計算 signal 與 xa 交叉的位置,注意指定 above=False
        entries = cross_value(signal, xa, above=False)
        # 計算 signal 與 xb 交叉的位置
        exits = -cross_value(signal, xb)
    # 計算交叉訊號
    trades = entries + exits

    # 修改交叉訊號以填充趨勢間的間隙
    trades.replace({0: npNaN}, inplace=True)
    trades.interpolate(method="pad", inplace=True)
    trades.fillna(0, inplace=True)

    # 將交叉訊號轉換為趨勢
    trends = (trades > 0).astype(int)
    if not long:
        trends = 1 - trends

    # 構建傳遞給 tsignals 函式的關鍵字引數字典
    tskwargs = {
        "asbool":asbool,
        "trade_offset":trade_offset,
        "trend_reset":trend_reset,
        "offset":offset
    }
    # 呼叫 tsignals 函式計算趨勢訊號
    df = tsignals(trends, **tskwargs)

    # 處理偏移,由 tsignals 函式處理
    DataFrame({
        f"XS_LONG": df.TS_Trends,
        f"XS_SHORT": 1 - df.TS_Trends
    })

    # 處理填充
    if "fillna" in kwargs:
        df.fillna(kwargs["fillna"], inplace=True)
    if "fill_method" in kwargs:
        df.fillna(method=kwargs["fill_method"], inplace=True)

    # 設定名稱和類別
    df.name = f"XS"
    df.category = "trend"

    return df

# 設定函式文件字串
xsignals.__doc__ = \
"""Cross Signals (XSIGNALS)

Cross Signals returns Trend Signal (TSIGNALS) results for Signal Crossings. This
is useful for indicators like RSI, ZSCORE, et al where one wants trade Entries
and Exits (and Trends).

Cross Signals has two kinds of modes: above and long.

The first mode 'above', default True, xsignals determines if the signal first
crosses above 'xa' and then below 'xb'. If 'above' is False, xsignals determines
if the signal first crosses below 'xa' and then above 'xb'.

The second mode 'long', default True, passes the long trend result into
tsignals so it can determine the appropriate Entries and Exits. When 'long' is
False, it does the same but for the short side.

Example:
# These are two different outcomes and depends on the indicator and it's
# characteristics. Please check BOTH outcomes BEFORE making an Issue.
rsi = df.ta.rsi()
# Returns tsignal DataFrame when RSI crosses above 20 and then below 80
ta.xsignals(rsi, 20, 80, above=True)
# Returns tsignal DataFrame when RSI crosses below 20 and then above 80
ta.xsignals(rsi, 20, 80, above=False)

Source: Kevin Johnson

Calculation:
    Default Inputs:
        asbool=False, trend_reset=0, trade_offset=0, drift=1

    trades = trends.diff().shift(trade_offset).fillna(0).astype(int)
    entries = (trades > 0).astype(int)
    exits = (trades < 0).abs().astype(int)

Args:
"""
    # 定義一個布林值,表示訊號是在'xa'之上首次穿越,然後再穿越'xb',還是在'xa'之下首次穿越,然後再穿越'xb'
    above (bool): When the signal crosses above 'xa' first and then 'xb'. When
        False, then when the signal crosses below 'xa' first and then 'xb'.
        Default: True
    # 將長期趨勢傳遞給tsignals的趨勢引數。當為False時,將短期趨勢傳遞給tsignals的趨勢引數
    long (bool): Passes the long trend into tsignals' trend argument. When
        False, it passes the short trend into tsignals trend argument.
        Default: True
    # 差異期。預設值為1
    drift (int): The difference period. Default: 1
    # 結果的偏移量。預設值為0
    offset (int): How many periods to offset the result. Default: 0

    # TSIGNAL傳遞引數
    # 如果為True,則將Trends、Entries和Exits列轉換為布林值。當為布林值時,也可用於使用vectorbt的Portfolio.from_signal(close, entries, exits)進行回測
    asbool (bool): If True, it converts the Trends, Entries and Exits columns to
        booleans. When boolean, it is also useful for backtesting with
        vectorbt's Portfolio.from_signal(close, entries, exits) Default: False
    # 用於識別趨勢是否結束的值。預設值為0
    trend_reset (value): Value used to identify if a trend has ended. Default: 0
    # 用於移動交易進出的值。使用1進行回測,使用0進行實時交易。預設值為0
    trade_offset (value): Value used shift the trade entries/exits Use 1 for
        backtesting and 0 for live. Default: 0
# 函式引數說明,使用關鍵字引數傳遞給函式的引數列表
Kwargs:
    # fillna引數,用於填充缺失值的值,採用pd.DataFrame.fillna(value)方式
    fillna (value, optional): pd.DataFrame.fillna(value)
    # fill_method引數,填充缺失值的方法型別
    fill_method (value, optional): Type of fill method

# 返回值說明,返回一個pd.DataFrame物件,其包含以下列:
Returns:
    # Trends列,趨勢(有趨勢: 1,無趨勢: 0)
    Trends (trend: 1, no trend: 0),
    # Trades列,交易(進入: 1,退出: -1,其他: 0)
    Trades (Enter: 1, Exit: -1, Otherwise: 0),
    # Entries列,入口(入口: 1,無: 0)
    Entries (entry: 1, nothing: 0),
    # Exits列,出口(出口: 1,無: 0)
    Exits (exit: 1, nothing: 0)

.\pandas-ta\pandas_ta\trend\__init__.py

# -*- coding: utf-8 -*-  
# 指定檔案編碼為 UTF-8,確保正確處理中文字元

# 匯入各個指標模組
from .adx import adx  # 匯入 adx 指標模組
from .amat import amat  # 匯入 amat 指標模組
from .aroon import aroon  # 匯入 aroon 指標模組
from .chop import chop  # 匯入 chop 指標模組
from .cksp import cksp  # 匯入 cksp 指標模組
from .decay import decay  # 匯入 decay 指標模組
from .decreasing import decreasing  # 匯入 decreasing 指標模組
from .dpo import dpo  # 匯入 dpo 指標模組
from .increasing import increasing  # 匯入 increasing 指標模組
from .long_run import long_run  # 匯入 long_run 指標模組
from .psar import psar  # 匯入 psar 指標模組
from .qstick import qstick  # 匯入 qstick 指標模組
from .short_run import short_run  # 匯入 short_run 指標模組
from .tsignals import tsignals  # 匯入 tsignals 指標模組
from .ttm_trend import ttm_trend  # 匯入 ttm_trend 指標模組
from .vhf import vhf  # 匯入 vhf 指標模組
from .vortex import vortex  # 匯入 vortex 指標模組
from .xsignals import xsignals  # 匯入 xsignals 指標模組

.\pandas-ta\pandas_ta\utils\data\alphavantage.py

# -*- coding: utf-8 -*-
# 匯入 DataFrame 類
from pandas import DataFrame
# 匯入 Imports 物件,RATE 物件,version 物件
from pandas_ta import Imports, RATE, version

# 定義 av 函式,獲取 alphaVantage 資料
def av(ticker: str, **kwargs):
    # 列印關鍵字引數 kwargs
    print(f"[!] kwargs: {kwargs}")
    # 從 kwargs 中彈出 verbose 引數,預設為 False
    verbose = kwargs.pop("verbose", False)
    # 從 kwargs 中彈出 kind 引數,預設為 "history"
    kind = kwargs.pop("kind", "history")
    # 將 kind 轉換為小寫
    kind = kind.lower()
    # 從 kwargs 中彈出 interval 引數,預設為 "D"
    interval = kwargs.pop("interval", "D")
    # 從 kwargs 中彈出 show 引數,預設為 None
    show = kwargs.pop("show", None)
    # 從 kwargs 中彈出 last 引數,但是沒有使用到

    # 如果 ticker 不為空且是字串型別,則將其轉換為大寫,否則為 None
    ticker = ticker.upper() if ticker is not None and isinstance(ticker, str) else None

    # 如果 alphaVantage-api 可用且 ticker 不為空
    if Imports["alphaVantage-api"] and ticker is not None:
        # 匯入 alphaVantageAPI 模組並重新命名為 AV
        import alphaVantageAPI as AV
        # 定義 AVC 字典,包含 API 金鑰和其他引數
        AVC = {"api_key": "YOUR API KEY", "clean": True, "export": False, "output_size": "full", "premium": False}
        # 從 kwargs 中獲取 av_kwargs 引數,如果不存在則使用 AVC
        _config = kwargs.pop("av_kwargs", AVC)
        # 建立 AlphaVantage 物件 av
        av = AV.AlphaVantage(**_config)

        # 從 kwargs 中獲取 period 引數,預設為 av.output_size
        period = kwargs.pop("period", av.output_size)

        # 定義 _all 列表和 div 變數
        _all, div = ["all"], "=" * 53 # Max div width is 80

        # 如果 kind 在 _all 列表中或者 verbose 為真,則執行下面的程式碼
        if kind in _all or verbose: pass

        # 如果 kind 在 _all 列表或者 ["history", "h"] 列表中
        if kind in _all + ["history", "h"]:
            # 如果 verbose 為真
            if verbose:
                # 列印資訊,顯示 Pandas TA 版本和 alphaVantage-api
                print("\n====  Chart History       " + div + f"\n[*] Pandas TA v{version} & alphaVantage-api")
                # 列印下載資訊,顯示下載的股票資訊和時間間隔
                print(f"[+] Downloading {ticker}[{interval}:{period}] from {av.API_NAME} (https://www.alphavantage.co/)")
            # 獲取股票資料並儲存到 df 變數中
            df = av.data(ticker, interval)
            # 設定 DataFrame 的名稱為 ticker
            df.name = ticker
            # 如果 show 不為空且是正整數且大於 0
            if show is not None and isinstance(show, int) and show > 0:
                # 列印 DataFrame 最後幾行資料
                print(f"\n{df.name}\n{df.tail(show)}\n")
            # 返回 DataFrame 物件
            return df

    # 如果上述條件都不滿足,則返回一個空的 DataFrame 物件
    return DataFrame()

# `.\pandas-ta\pandas_ta\utils\data\yahoofinance.py`

```py
# -*- coding: utf-8 -*-
# 匯入 DataFrame 類
from pandas import DataFrame
# 匯入 Imports、RATE、version 變數
from pandas_ta import Imports, RATE, version
# 匯入 _camelCase2Title 函式和 ytd 函式
from .._core import _camelCase2Title
from .._time import ytd

# 定義函式 yf,用於包裝 yfinance
def yf(ticker: str, **kwargs):
    """yf - yfinance wrapper

    It retrieves market data (ohlcv) from Yahoo Finance using yfinance.
    To install yfinance. (pip install yfinance) This method can also pull
    additional data using the 'kind' kwarg. By default kind=None and retrieves
    Historical Chart Data.

    Other options of 'kind' include:
    * All: "all"
        - Prints everything below but only returns Chart History to Pandas TA
    * Company Information: "info"
    * Institutional Holders: "institutional_holders" or "ih"
    * Major Holders: "major_holders" or "mh"
    * Mutual Fund Holders: "mutualfund_holders" or "mfh"
    * Recommendations (YTD): "recommendations" or "rec"
    * Earnings Calendar: "calendar" or "cal"
    * Earnings: "earnings" or "earn"
    * Sustainability/ESG Scores: "sustainability", "sus" or "esg"
    * Financials: "financials" or "fin"
        - Returns in order: Income Statement, Balance Sheet and Cash Flow
    * Option Chain: "option_chain" or "oc"
        - Uses the nearest expiration date by default
        - Change the expiration date using kwarg "exp"
        - Show ITM options, set kwarg "itm" to True. Or OTM options, set
        kwarg "itm" to False.
    * Chart History:
        - The only data returned to Pandas TA.

    Args:
        ticker (str): Any string for a ticker you would use with yfinance.
            Default: "SPY"
    Kwargs:
        calls (bool): When True, prints only Option Calls for the Option Chain.
            Default: None
        desc (bool): Will print Company Description when printing Company
            Information. Default: False
        exp (str): Used to print other Option Chains for the given Expiration
            Date. Default: Nearest Expiration Date for the Option Chains
        interval (str): A yfinance argument. Default: "1d"
        itm (bool): When printing Option Chains, shows ITM Options when True.
            When False, it shows OTM Options: Default: None
        kind (str): Options see above. Default: None
        period (str): A yfinance argument. Default: "max"
        proxy (dict): Proxy for yfinance to use. Default: {}
        puts (bool): When True, prints only Option Puts for the Option Chain.
            Default: None
        show (int > 0): How many last rows of Chart History to show.
            Default: None
        snd (int): How many recent Splits and Dividends to show in Company
            Information. Default: 5
        verbose (bool): Prints Company Information "info" and a Chart History
            header to the screen. Default: False

    Returns:
        Exits if the DataFrame is empty or None
        Otherwise it returns a DataFrame of the Chart History
    """
    # 從 kwargs 中獲取 verbose 引數,預設為 False
    verbose = kwargs.pop("verbose", False)
    # 如果 ticker 不為空且為字串型別且長度大於0,則將 ticker 轉換為大寫
    if ticker is not None and isinstance(ticker, str) and len(ticker):
        ticker = ticker.upper()
    else:
        # 如果 ticker 為空或不是字串型別或長度為0,則將 ticker 設定為 "SPY"
        ticker = "SPY"

    # 從 kwargs 中彈出 "kind" 鍵對應的值,如果不存在則為 None
    kind = kwargs.pop("kind", None)
    # 如果 kind 不為空且為字串型別且長度大於0,則將 kind 轉換為小寫
    if kind is not None and isinstance(kind, str) and len(kind):
        kind = kind.lower()

    # 從 kwargs 中彈出 "period" 鍵對應的值,如果不存在則為 "max"
    period = kwargs.pop("period", "max")
    # 從 kwargs 中彈出 "interval" 鍵對應的值,如果不存在則為 "1d"
    interval = kwargs.pop("interval", "1d")
    # 從 kwargs 中彈出 "proxy" 鍵對應的值,如果不存在則為一個空字典
    proxy = kwargs.pop("proxy", {})
    # 從 kwargs 中彈出 "show" 鍵對應的值,如果不存在則為 None
    show = kwargs.pop("show", None)

    # 如果 Imports 中沒有 yfinance 模組,則列印提示資訊並返回
    if not Imports["yfinance"]:
        print(f"[X] Please install yfinance to use this method. (pip install yfinance)")
        return
    else:
        # 如果有 yfinance 模組,則返回一個空的 DataFrame 物件
        return DataFrame()

.\pandas-ta\pandas_ta\utils\data\__init__.py

# 設定檔案編碼為 UTF-8,以支援包含非 ASCII 字元的內容
# 匯入自定義模組中的 alphavantage 和 yahoofinance 子模組
from .alphavantage import av
from .yahoofinance import yf

.\pandas-ta\pandas_ta\utils\_candles.py

# -*- coding: utf-8 -*-
# 匯入 Series 類
from pandas import Series
# 匯入 non_zero_range 函式
from ._core import non_zero_range

# 計算蠟燭圖的顏色
def candle_color(open_: Series, close: Series) -> Series:
    # 複製收盤價 Series,並將其型別轉換為整數
    color = close.copy().astype(int)
    # 當收盤價大於等於開盤價時,將顏色設定為1
    color[close >= open_] = 1
    # 當收盤價小於開盤價時,將顏色設定為-1
    color[close < open_] = -1
    # 返回顏色 Series
    return color

# 計算最高價和最低價的範圍
def high_low_range(high: Series, low: Series) -> Series:
    # 呼叫 non_zero_range 函式計算高低價的範圍
    return non_zero_range(high, low)

# 計算實體部分(實體部分指收盤價與開盤價之間的絕對值)
def real_body(open_: Series, close: Series) -> Series:
    # 呼叫 non_zero_range 函式計算實體部分
    return non_zero_range(close, open_)

.\pandas-ta\pandas_ta\utils\_core.py

# 設定檔案編碼為 utf-8
# 匯入 re 模組並重新命名為 re_
# 從 pathlib 模組中匯入 Path 類
# 從 sys 模組中匯入 float_info 並重新命名為 sflt
# 從 numpy 模組中匯入 argmax 和 argmin 函式
# 從 pandas 模組中匯入 DataFrame 和 Series 類
# 從 pandas 模組中匯入 is_datetime64_any_dtype 函式
# 從 pandas_ta 模組中匯入 Imports

def _camelCase2Title(x: str):
    """將駝峰命名轉換為標題格式"""
    return re_.sub("([a-z])([A-Z])","\g<1> \g<2>", x).title()

def category_files(category: str) -> list:
    """返回類別目錄中所有檔名的幫助函式"""
    files = [
        x.stem
        for x in list(Path(f"pandas_ta/{category}/").glob("*.py"))
        if x.stem != "__init__"
    ]
    return files

def get_drift(x: int) -> int:
    """如果不為零,則返回一個整數,否則預設為一"""
    return int(x) if isinstance(x, int) and x != 0 else 1

def get_offset(x: int) -> int:
    """返回一個整數,否則預設為零"""
    return int(x) if isinstance(x, int) else 0

def is_datetime_ordered(df: DataFrame or Series) -> bool:
    """如果索引是日期時間且有序,則返回 True"""
    index_is_datetime = 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 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: Series, low: Series) -> Series:
    """返回兩個序列的差異,並對任何零值新增 epsilon。在加密資料中常見情況是 'high' = 'low'。"""
    diff = high - low
    if diff.eq(0).any().any():
        diff += sflt.epsilon
    return diff

def recent_maximum_index(x):
    """返回最近最大值的索引"""
    return int(argmax(x[::-1]))

def recent_minimum_index(x):
    """返回最近最小值的索引"""
    return int(argmin(x[::-1]))

def signed_series(series: Series, initial: int = None) -> Series:
    """返回帶有或不帶有初始值的有符號序列"""
    series = verify_series(series)
    sign = series.diff(1)
    sign[sign > 0] = 1
    sign[sign < 0] = -1
    sign.iloc[0] = initial
    return sign

def tal_ma(name: str) -> int:
    """返回 TA Lib 的 MA 型別的列舉值"""
    # 檢查是否匯入了 talib 模組,並且 name 是否是字串型別,並且長度大於1
    if Imports["talib"] and isinstance(name, str) and len(name) > 1:
        # 如果滿足條件,從 talib 模組匯入 MA_Type
        from talib import MA_Type
        # 將 name 轉換為小寫
        name = name.lower()
        # 根據 name 的不同取值返回對應的 MA_Type 列舉值
        if   name == "sma":   return MA_Type.SMA   # 0
        elif name == "ema":   return MA_Type.EMA   # 1
        elif name == "wma":   return MA_Type.WMA   # 2
        elif name == "dema":  return MA_Type.DEMA  # 3
        elif name == "tema":  return MA_Type.TEMA  # 4
        elif name == "trima": return MA_Type.TRIMA # 5
        elif name == "kama":  return MA_Type.KAMA  # 6
        elif name == "mama":  return MA_Type.MAMA  # 7
        elif name == "t3":    return MA_Type.T3    # 8
    # 如果不滿足條件,返回預設值 0,代表 SMA
    return 0 # Default: SMA -> 0
# 定義一個函式,計算給定 Series 的無符號差值
def unsigned_differences(series: Series, amount: int = None, **kwargs) -> Series:
    """Unsigned Differences
    返回兩個 Series,一個是原始 Series 的無符號正差值,另一個是無符號負差值。
    正差值 Series 僅包含增加值,負差值 Series 僅包含減少值。

    預設示例:
    series   = Series([3, 2, 2, 1, 1, 5, 6, 6, 7, 5, 3]) 返回
    positive  = Series([0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0])
    negative = Series([0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1])
    """
    # 如果未提供 amount 引數,則預設為 1
    amount = int(amount) if amount is not None else 1
    # 計算 Series 的負差值
    negative = series.diff(amount)
    # 將 NaN 值填充為 0
    negative.fillna(0, inplace=True)
    # 複製負差值 Series 以備後用
    positive = negative.copy()

    # 將正差值 Series 中小於等於 0 的值設定為 0
    positive[positive <= 0] = 0
    # 將正差值 Series 中大於 0 的值設定為 1
    positive[positive > 0] = 1

    # 將負差值 Series 中大於等於 0 的值設定為 0
    negative[negative >= 0] = 0
    # 將負差值 Series 中小於 0 的值設定為 1
    negative[negative < 0] = 1

    # 如果 kwargs 中包含 asint 引數且值為 True,則將 Series 轉換為整數型別
    if kwargs.pop("asint", False):
        positive = positive.astype(int)
        negative = negative.astype(int)

    # 返回正差值 Series 和負差值 Series
    return positive, negative


# 定義一個函式,驗證給定的 Series 是否滿足指示器的最小長度要求
def verify_series(series: Series, min_length: int = None) -> Series:
    """If a Pandas Series and it meets the min_length of the indicator return it."""
    # 判斷是否指定了最小長度,並且最小長度是整數型別
    has_length = min_length is not None and isinstance(min_length, int)
    # 如果給定的 series 不為空且是 Pandas Series 型別
    if series is not None and isinstance(series, Series):
        # 如果指定了最小長度,並且 series 的大小小於最小長度,則返回 None,否則返回 series
        return None if has_length and series.size < min_length else series

.\pandas-ta\pandas_ta\utils\_math.py

# 設定檔案編碼為 UTF-8
# 匯入 functools 模組中的 reduce 函式
# 從 math 模組中匯入 floor 函式並將其命名為 mfloor
# 從 operator 模組中匯入 mul 函式
# 從 sys 模組中匯入 float_info 物件並將其命名為 sflt
# 從 typing 模組中匯入 List、Optional 和 Tuple 型別
from functools import reduce
from math import floor as mfloor
from operator import mul
from sys import float_info as sflt
from typing import List, Optional, Tuple

# 從 numpy 模組中匯入 ones、triu、all、append、array、corrcoef、dot、fabs、exp、log、nan、ndarray、seterr、sqrt 和 sum 函式
from numpy import ones, triu
from numpy import all as npAll
from numpy import append as npAppend
from numpy import array as npArray
from numpy import corrcoef as npCorrcoef
from numpy import dot as npDot
from numpy import fabs as npFabs
from numpy import exp as npExp
from numpy import log as npLog
from numpy import nan as npNaN
from numpy import ndarray as npNdArray
from numpy import seterr
from numpy import sqrt as npSqrt
from numpy import sum as npSum

# 從 pandas 模組中匯入 DataFrame 和 Series 類
from pandas import DataFrame, Series

# 從 pandas_ta 包中匯入 Imports 模組
from pandas_ta import Imports
# 從 ._core 模組中匯入 verify_series 函式
from ._core import verify_series

# 定義 combination 函式,接收任意關鍵字引數並返回整型值
def combination(**kwargs: dict) -> int:
    """https://stackoverflow.com/questions/4941753/is-there-a-math-ncr-function-in-python"""
    # 從 kwargs 中取出鍵為 "n" 的值,若不存在則預設為 1,並轉換為整型
    n = int(npFabs(kwargs.pop("n", 1)))
    # 從 kwargs 中取出鍵為 "r" 的值,若不存在則預設為 0,並轉換為整型
    r = int(npFabs(kwargs.pop("r", 0)))

    # 如果引數中存在 "repetition" 或 "multichoose" 鍵,則執行以下操作
    if kwargs.pop("repetition", False) or kwargs.pop("multichoose", False):
        # 計算修正後的 n 值
        n = n + r - 1

    # 若 r 小於 0,則返回 None
    # 如果 r 大於 n,則令 r 等於 n
    r = min(n, n - r)
    # 若 r 為 0,則返回 1
    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


# 定義錯誤函式 erf(x),接收一個引數 x,返回 erf(x) 的值
def erf(x):
    """Error Function erf(x)
    The algorithm comes from Handbook of Mathematical Functions, formula 7.1.26.
    Source: https://stackoverflow.com/questions/457408/is-there-an-easily-available-implementation-of-erf-for-python
    """
    # 儲存 x 的符號
    sign = 1 if x >= 0 else -1
    x = abs(x)

    # 定義常數
    a1 =  0.254829592
    a2 = -0.284496736
    a3 =  1.421413741
    a4 = -1.453152027
    a5 =  1.061405429
    p  =  0.3275911

    # 使用 A&S 公式 7.1.26 計算 erf(x) 的值
    t = 1.0 / (1.0 + p * x)
    y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * npExp(-x * x)
    # 返回 erf(x) 的值,若 x 為負數,則取相反數
    return sign * y # erf(-x) = -erf(x)


# 定義斐波那契數列函式 fibonacci,接收一個整型引數 n 和任意關鍵字引數,返回一個 numpy 陣列
def fibonacci(n: int = 2, **kwargs: dict) -> npNdArray:
    """Fibonacci Sequence as a numpy array"""
    # 將 n 轉換為非負整數
    n = int(npFabs(n)) if n >= 0 else 2

    # 從 kwargs 中取出鍵為 "zero" 的值,若存在且為 True,則斐波那契數列以 0 開頭
    zero = kwargs.pop("zero", False)
    if zero:
        a, b = 0, 1
    else:
        # 若不以 0 開頭,則斐波那契數列從 1 開始,n 減 1
        n -= 1
        a, b = 1, 1

    # 初始化結果陣列,包含斐波那契數列的第一個元素
    result = npArray([a])
    # 迴圈生成斐波那契數列
    for _ in range(0, n):
        a, b = b, a + b
        result = npAppend(result, a)

    # 從 kwargs 中取出鍵為 "weighted" 的值,若存在且為 True,則返回加權後的斐波那契數列
    weighted = kwargs.pop("weighted", False)
    if weighted:
        # 計算斐波那契數列的總和
        fib_sum = npSum(result)
        # 若總和大於 0,則返回斐波那契數列的每個元素除以總和的結果
        if fib_sum > 0:
            return result / fib_sum
        else:
            # 若總和小於等於 0,則直接返回斐波那契數列
            return result
    else:
        # 若不加權,則直接返回斐波那契數
    """Classic Linear Regression in Numpy or Scikit-Learn"""
    # 確保 x 和 y 是 Series 型別的資料
    x, y = verify_series(x), verify_series(y)
    # 獲取 x 和 y 的大小
    m, n = x.size, y.size

    # 如果 x 和 y 的大小不相等,則列印錯誤資訊並返回空字典
    if m != n:
        print(f"[X] Linear Regression X and y have unequal total observations: {m} != {n}")
        return {}

    # 如果匯入了 sklearn 模組,則使用 sklearn 進行線性迴歸
    if Imports["sklearn"]:
        return _linear_regression_sklearn(x, y)
    # 否則使用 numpy 進行線性迴歸
    else:
        return _linear_regression_np(x, y)
# 返回給定序列的對數幾何平均值
def log_geometric_mean(series: Series) -> float:
    n = series.size  # 獲取序列的大小
    if n < 2: return 0  # 如果序列大小小於2,則返回0
    else:
        series = series.fillna(0) + 1  # 將序列中的空值填充為0,並加1
        if npAll(series > 0):  # 檢查序列中的所有值是否大於0
            return npExp(npLog(series).sum() / n) - 1  # 計算序列的對數和的均值的指數,然後減去1
        return 0  # 如果序列中存在小於等於0的值,則返回0


# 返回帕斯卡三角形的第n行
def pascals_triangle(n: int = None, **kwargs: dict) -> npNdArray:
    n = int(npFabs(n)) if n is not None else 0  # 將n轉換為整數並取絕對值,如果n為None則設為0

    # 計算
    triangle = npArray([combination(n=n, r=i) for i in range(0, n + 1)])  # 建立帕斯卡三角形的第n行
    triangle_sum = npSum(triangle)  # 計算三角形的總和
    triangle_weights = triangle / triangle_sum  # 計算每個元素的權重
    inverse_weights = 1 - triangle_weights  # 計算逆權重

    weighted = kwargs.pop("weighted", False)  # 獲取weighted引數,預設為False
    inverse = kwargs.pop("inverse", False)  # 獲取inverse引數,預設為False
    if weighted and inverse:  # 如果weighted和inverse都為True
        return inverse_weights  # 返回逆權重
    if weighted:  # 如果weighted為True
        return triangle_weights  # 返回權重
    if inverse:  # 如果inverse為True
        return None  # 返回None

    return triangle  # 返回帕斯卡三角形的第n行


# 返回對稱三角形的第n行
def symmetric_triangle(n: int = None, **kwargs: dict) -> Optional[List[int]]:
    n = int(npFabs(n)) if n is not None else 2  # 將n轉換為整數並取絕對值,如果n為None則設為2

    triangle = None
    if n == 2:  # 如果n為2
        triangle = [1, 1]  # 返回固定的列表

    if n > 2:  # 如果n大於2
        if n % 2 == 0:  # 如果n為偶數
            front = [i + 1 for i in range(0, mfloor(n / 2))]  # 建立前半部分列表
            triangle = front + front[::-1]  # 建立對稱三角形
        else:
            front = [i + 1 for i in range(0, mfloor(0.5 * (n + 1)))]  # 建立前半部分列表
            triangle = front.copy()  # 複製前半部分列表
            front.pop()  # 移除最後一個元素
            triangle += front[::-1]  # 建立對稱三角形

    if kwargs.pop("weighted", False) and isinstance(triangle, list):  # 如果weighted為True且triangle是列表型別
        triangle_sum = npSum(triangle)  # 計算三角形的總和
        triangle_weights = triangle / triangle_sum  # 計算每個元素的權重
        return triangle_weights  # 返回權重

    return triangle  # 返回對稱三角形的第n行


# 返回權重與值x的點積
def weights(w: npNdArray):
    def _dot(x):
        return npDot(w, x)
    return _dot


# 如果值接近於零,則返回零,否則返回自身
def zero(x: Tuple[int, float]) -> Tuple[int, float]:
    return 0 if abs(x) < sflt.epsilon else x


# DataFrame相關性分析輔助函式
def df_error_analysis(dfA: DataFrame, dfB: DataFrame, **kwargs: dict) -> DataFrame:
    corr_method = kwargs.pop("corr_method", "pearson")  # 獲取相關性計算方法,預設為pearson

    # 計算它們的差異和相關性
    diff = dfA - dfB  # 計算DataFrame的差異
    corr = dfA.corr(dfB, method=corr_method)  # 計算DataFrame的相關性

    # 用於繪圖
    if kwargs.pop("plot", False):  # 如果plot為True
        diff.hist()  # 繪製差異的直方圖
        if diff[diff > 0].any():  # 如果差異中存在大於0的值
            diff.plot(kind="kde")  # 繪製密度曲線圖

    if kwargs.pop("triangular", False):  # 如果triangular為True
        return corr.where(triu(ones(corr.shape)).astype(bool))  # 返回上三角部分的相關性矩陣

    return corr  # 返回相關性矩陣


# 私有函式
# 使用 Numpy 實現簡單的線性迴歸,適用於沒有安裝 sklearn 包的環境,接受兩個一維陣列作為輸入
def _linear_regression_np(x: Series, y: Series) -> dict:
    # 初始化結果字典,所有值設為 NaN
    result = {"a": npNaN, "b": npNaN, "r": npNaN, "t": npNaN, "line": npNaN}
    # 計算 x 和 y 的總和
    x_sum = x.sum()
    y_sum = y.sum()

    # 如果 x 的總和不為 0
    if int(x_sum) != 0:
        # 計算 x 和 y 之間的相關係數
        r = npCorrcoef(x, y)[0, 1]

        m = x.size
        # 計算迴歸係數 b
        r_mix = m * (x * y).sum() - x_sum * y_sum
        b = r_mix // (m * (x * x).sum() - x_sum * x_sum)
        # 計算截距 a 和迴歸線
        a = y.mean() - b * x.mean()
        line = a + b * x

        # 臨時儲存 Numpy 的錯誤設定
        _np_err = seterr()
        # 忽略除零和無效值的錯誤
        seterr(divide="ignore", invalid="ignore")
        # 更新結果字典
        result = {
            "a": a, "b": b, "r": r,
            "t": r / npSqrt((1 - r * r) / (m - 2)),
            "line": line,
        }
        # 恢復 Numpy 的錯誤設定
        seterr(divide=_np_err["divide"], invalid=_np_err["invalid"])

    return result

# 使用 Scikit Learn 實現簡單的線性迴歸,適用於安裝了 sklearn 包的環境,接受兩個一維陣列作為輸入
def _linear_regression_sklearn(x: Series, y: Series) -> dict:
    # 匯入 LinearRegression 類
    from sklearn.linear_model import LinearRegression

    # 將 x 轉換為 DataFrame,建立 LinearRegression 模型並擬合資料
    X = DataFrame(x)
    lr = LinearRegression().fit(X, y=y)
    # 計算決定係數
    r = lr.score(X, y=y)
    # 獲取截距和斜率
    a, b = lr.intercept_, lr.coef_[0]

    # 更新結果字典
    result = {
        "a": a, "b": b, "r": r,
        "t": r / npSqrt((1 - r * r) / (x.size - 2)),
        "line": a + b * x
    }
    return result

.\pandas-ta\pandas_ta\utils\_metrics.py

# -*- coding: utf-8 -*-
# 引入需要的型別提示模組
from typing import Tuple

# 引入 numpy 庫的 log、nan、sqrt 函式
from numpy import log as npLog
from numpy import nan as npNaN
from numpy import sqrt as npSqrt

# 引入 pandas 庫的 Series、Timedelta 類
from pandas import Series, Timedelta

# 引入自定義的核心模組中的 verify_series 函式
from ._core import verify_series

# 引入自定義的時間模組中的 total_time 函式
from ._time import total_time

# 引入自定義的數學模組中的 linear_regression、log_geometric_mean 函式
from ._math import linear_regression, log_geometric_mean

# 引入 pandas_ta 庫中的 RATE 常量
from pandas_ta import RATE

# 引入 pandas_ta 庫中的效能模組中的 drawdown、log_return、percent_return 函式
from pandas_ta.performance import drawdown, log_return, percent_return


def cagr(close: Series) -> float:
    """複合年增長率

    Args:
        close (pd.Series): 'close' 的序列

    >>> result = ta.cagr(df.close)
    """
    # 確保 close 是有效的 Series
    close = verify_series(close)
    # 獲取序列的起始和結束值
    start, end = close.iloc[0], close.iloc[-1]
    # 計算並返回複合年增長率
    return ((end / start) ** (1 / total_time(close))) - 1


def calmar_ratio(close: Series, method: str = "percent", years: int = 3) -> float:
    """Calmar 比率通常是在過去三年內的最大回撤率的百分比。

    Args:
        close (pd.Series): 'close' 的序列
        method (str): 最大回撤計算選項:'dollar'、'percent'、'log'。預設值:'dollar'
        years (int): 使用的年數。預設值:3

    >>> result = ta.calmar_ratio(close, method="percent", years=3)
    """
    if years <= 0:
        # 如果年數引數小於等於 0,則列印錯誤訊息並返回
        print(f"[!] calmar_ratio 'years' 引數必須大於零。")
        return
    # 確保 close 是有效的 Series
    close = verify_series(close)

    # 獲取指定年數前的日期
    n_years_ago = close.index[-1] - Timedelta(days=365.25 * years)
    # 從指定日期開始擷取序列
    close = close[close.index > n_years_ago]

    # 計算並返回 Calmar 比率
    return cagr(close) / max_drawdown(close, method=method)


def downside_deviation(returns: Series, benchmark_rate: float = 0.0, tf: str = "years") -> float:
    """Sortino 比率的下行偏差。假定基準利率是年化的。根據資料中每年的期數進行調整。

    Args:
        returns (pd.Series): 'returns' 的序列
        benchmark_rate (float): 要使用的基準利率。預設值:0.0
        tf (str): 時間範圍選項:'days'、'weeks'、'months'、'years'。預設值:'years'

    >>> result = ta.downside_deviation(returns, benchmark_rate=0.0, tf="years")
    """
    # 用於去年化基準利率和年化結果的天數
    # 確保 returns 是有效的 Series
    returns = verify_series(returns)
    days_per_year = returns.shape[0] / total_time(returns, tf)

    # 調整後的基準利率
    adjusted_benchmark_rate = ((1 + benchmark_rate) ** (1 / days_per_year)) - 1

    # 計算下行偏差
    downside = adjusted_benchmark_rate - returns
    downside_sum_of_squares = (downside[downside > 0] ** 2).sum()
    downside_deviation = npSqrt(downside_sum_of_squares / (returns.shape[0] - 1))
    return downside_deviation * npSqrt(days_per_year)


def jensens_alpha(returns: Series, benchmark_returns: Series) -> float:
    """一系列與基準的 Jensen's 'Alpha'。

    Args:
        returns (pd.Series): 'returns' 的序列
        benchmark_returns (pd.Series): 'benchmark_returns' 的序列

    >>> result = ta.jensens_alpha(returns, benchmark_returns)
    """
    # 確保 returns 是有效的 Series
    returns = verify_series(returns)
    # 確保 benchmark_returns 是一個 Series 物件,並返回一個驗證過的 Series 物件
    benchmark_returns = verify_series(benchmark_returns)
    
    # 對 benchmark_returns 進行插值處理,使得其中的缺失值被填充,原地修改(不建立新物件)
    benchmark_returns.interpolate(inplace=True)
    
    # 對 benchmark_returns 和 returns 進行線性迴歸分析,並返回其中的斜率引數(截距引數不返回)
    return linear_regression(benchmark_returns, returns)["a"]
def log_max_drawdown(close: Series) -> float:
    """Calculate the logarithmic maximum drawdown of a series.

    Args:
        close (pd.Series): Series of 'close' prices.

    >>> result = ta.log_max_drawdown(close)
    """
    # Ensure 'close' series is valid
    close = verify_series(close)
    # Calculate the log return from the beginning to the end of the series
    log_return = npLog(close.iloc[-1]) - npLog(close.iloc[0])
    # Return the log return minus the maximum drawdown of the series
    return log_return - max_drawdown(close, method="log")


def max_drawdown(close: Series, method:str = None, all:bool = False) -> float:
    """Calculate the maximum drawdown from a series of closing prices.

    Args:
        close (pd.Series): Series of 'close' prices.
        method (str): Options for calculating max drawdown: 'dollar', 'percent', 'log'.
            Default: 'dollar'.
        all (bool): If True, return all three methods as a dictionary.
            Default: False.

    >>> result = ta.max_drawdown(close, method="dollar", all=False)
    """
    # Ensure 'close' series is valid
    close = verify_series(close)
    # Calculate the maximum drawdown using the drawdown function
    max_dd = drawdown(close).max()

    # Dictionary containing maximum drawdown values for different methods
    max_dd_ = {
        "dollar": max_dd.iloc[0],
        "percent": max_dd.iloc[1],
        "log": max_dd.iloc[2]
    }
    # If 'all' is True, return all methods as a dictionary
    if all: return max_dd_

    # If 'method' is specified and valid, return the corresponding value
    if isinstance(method, str) and method in max_dd_.keys():
        return max_dd_[method]
    # Default to dollar method if 'method' is not specified or invalid
    return max_dd_["dollar"]


def optimal_leverage(
        close: Series, benchmark_rate: float = 0.0,
        period: Tuple[float, int] = RATE["TRADING_DAYS_PER_YEAR"],
        log: bool = False, capital: float = 1., **kwargs
    ) -> float:
    """Calculate the optimal leverage of a series. WARNING: Incomplete. Do NOT use.

    Args:
        close (pd.Series): Series of 'close' prices.
        benchmark_rate (float): Benchmark Rate to use. Default: 0.0.
        period (int, float): Period to use to calculate Mean Annual Return and
            Annual Standard Deviation.
            Default: None or the default sharpe_ratio.period().
        log (bool): If True, calculates log_return. Otherwise, it returns
            percent_return. Default: False.

    >>> result = ta.optimal_leverage(close, benchmark_rate=0.0, log=False)
    """
    # Ensure 'close' series is valid
    close = verify_series(close)

    # Check if use_cagr is specified
    use_cagr = kwargs.pop("use_cagr", False)
    # Calculate returns based on whether log or percent return is specified
    returns = percent_return(close=close) if not log else log_return(close=close)

    # Calculate period mean and standard deviation
    period_mu = period * returns.mean()
    period_std = npSqrt(period) * returns.std()

    # Calculate mean excess return and optimal leverage
    mean_excess_return = period_mu - benchmark_rate
    opt_leverage = (period_std ** -2) * mean_excess_return

    # Calculate the amount based on capital and optimal leverage
    amount = int(capital * opt_leverage)
    return amount


def pure_profit_score(close: Series) -> Tuple[float, int]:
    """Calculate the pure profit score of a series.

    Args:
        close (pd.Series): Series of 'close' prices.

    >>> result = ta.pure_profit_score(df.close)
    """
    # Ensure 'close' series is valid
    close = verify_series(close)
    # Create a series of zeros with the same index as 'close'
    close_index = Series(0, index=close.reset_index().index)

    # Calculate the linear regression 'r' value
    r = linear_regression(close_index, close)["r"]
    # If 'r' value is not NaN, return 'r' multiplied by CAGR of 'close'
    if r is not npNaN:
        return r * cagr(close)
    # Otherwise, return 0
    return 0
# 計算夏普比率的函式
def sharpe_ratio(close: Series, benchmark_rate: float = 0.0, log: bool = False, use_cagr: bool = False, period: int = RATE["TRADING_DAYS_PER_YEAR"]) -> float:
    """Sharpe Ratio of a series.

    Args:
        close (pd.Series): Series of 'close's
        benchmark_rate (float): Benchmark Rate to use. Default: 0.0
        log (bool): If True, calculates log_return. Otherwise it returns
            percent_return. Default: False
        use_cagr (bool): Use cagr - benchmark_rate instead. Default: False
        period (int, float): Period to use to calculate Mean Annual Return and
            Annual Standard Deviation.
            Default: RATE["TRADING_DAYS_PER_YEAR"] (currently 252)

    >>> result = ta.sharpe_ratio(close, benchmark_rate=0.0, log=False)
    """
    # 驗證輸入的資料是否為Series型別
    close = verify_series(close)
    # 根據log引數選擇計算百分比收益率或對數收益率
    returns = percent_return(close=close) if not log else log_return(close=close)

    # 如果使用cagr引數,則返回年複合增長率與波動率的比率
    if use_cagr:
        return cagr(close) / volatility(close, returns, log=log)
    else:
        # 計算期望收益率和標準差
        period_mu = period * returns.mean()
        period_std = npSqrt(period) * returns.std()
        return (period_mu - benchmark_rate) / period_std


# 計算Sortino比率的函式
def sortino_ratio(close: Series, benchmark_rate: float = 0.0, log: bool = False) -> float:
    """Sortino Ratio of a series.

    Args:
        close (pd.Series): Series of 'close's
        benchmark_rate (float): Benchmark Rate to use. Default: 0.0
        log (bool): If True, calculates log_return. Otherwise it returns
            percent_return. Default: False

    >>> result = ta.sortino_ratio(close, benchmark_rate=0.0, log=False)
    """
    # 驗證輸入的資料是否為Series型別
    close = verify_series(close)
    # 根據log引數選擇計算百分比收益率或對數收益率
    returns = percent_return(close=close) if not log else log_return(close=close)

    # 計算Sortino比率
    result  = cagr(close) - benchmark_rate
    result /= downside_deviation(returns)
    return result


# 計算波動率的函式
def volatility(close: Series, tf: str = "years", returns: bool = False, log: bool = False, **kwargs) -> float:
    """Volatility of a series. Default: 'years'

    Args:
        close (pd.Series): Series of 'close's
        tf (str): Time Frame options: 'days', 'weeks', 'months', and 'years'.
            Default: 'years'
        returns (bool): If True, then it replace the close Series with the user
            defined Series; typically user generated returns or percent returns
            or log returns. Default: False
        log (bool): If True, calculates log_return. Otherwise it calculates
            percent_return. Default: False

    >>> result = ta.volatility(close, tf="years", returns=False, log=False, **kwargs)
    """
    # 驗證輸入的資料是否為Series型別
    close = verify_series(close)

    # 如果returns引數為False,則計算百分比收益率或對數收益率
    if not returns:
        returns = percent_return(close=close) if not log else log_return(close=close)
    else:
        returns = close

    # 計算對數幾何平均值的標準差作為波動率
    returns = log_geometric_mean(returns).std()
    return returns

相關文章