pmorissette / ffn

ffn - a financial function library for Python
pmorissette.github.io/ffn
MIT License
1.87k stars 282 forks source link

TypeError from ufunc `isinf` in `calc_inv_vol_weights` #204

Closed jeremytanjianle closed 8 months ago

jeremytanjianle commented 9 months ago

Describe the bug Tried to replicate inverse vol example from bt package. The backtest could not be completed. Error:

TypeError: ufunc 'isinf' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

Resulting traceback:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
[c:\Users\User\Desktop\vectorbt\notebooks\bt.ipynb](file:///C:/Users/User/Desktop/vectorbt/notebooks/bt.ipynb) Cell 13 line [1](vscode-notebook-cell:/c%3A/Users/User/Desktop/vectorbt/notebooks/bt.ipynb#X31sZmlsZQ%3D%3D?line=0)
      1 # create and run
      [2](vscode-notebook-cell:/c%3A/Users/User/Desktop/vectorbt/notebooks/bt.ipynb#X31sZmlsZQ%3D%3D?line=1) tsmom_invvol_bt = bt.Backtest(
      [3](vscode-notebook-cell:/c%3A/Users/User/Desktop/vectorbt/notebooks/bt.ipynb#X31sZmlsZQ%3D%3D?line=2)     tsmom_invvol_strat,
      [4](vscode-notebook-cell:/c%3A/Users/User/Desktop/vectorbt/notebooks/bt.ipynb#X31sZmlsZQ%3D%3D?line=3)     pdf,
   (...)
      [8](vscode-notebook-cell:/c%3A/Users/User/Desktop/vectorbt/notebooks/bt.ipynb#X31sZmlsZQ%3D%3D?line=7)     progress_bar=True
      [9](vscode-notebook-cell:/c%3A/Users/User/Desktop/vectorbt/notebooks/bt.ipynb#X31sZmlsZQ%3D%3D?line=8) )
---> [10](vscode-notebook-cell:/c%3A/Users/User/Desktop/vectorbt/notebooks/bt.ipynb#X31sZmlsZQ%3D%3D?line=9) tsmom_invvol_res = bt.run(tsmom_invvol_bt)

File [c:\Users\User\anaconda3\envs\vectorbt\lib\site-packages\bt\backtest.py:28](file:///C:/Users/User/anaconda3/envs/vectorbt/lib/site-packages/bt/backtest.py:28), in run(*backtests)
     26 # run each backtest
     27 for bkt in backtests:
---> 28     bkt.run()
     30 return Result(*backtests)

File [c:\Users\User\anaconda3\envs\vectorbt\lib\site-packages\bt\backtest.py:240](file:///C:/Users/User/anaconda3/envs/vectorbt/lib/site-packages/bt/backtest.py:240), in Backtest.run(self)
    237 self.strategy.update(dt)
    239 if not self.strategy.bankrupt:
--> 240     self.strategy.run()
    241     # need update after to save weights, values and such
    242     self.strategy.update(dt)

File [c:\Users\User\anaconda3\envs\vectorbt\lib\site-packages\bt\core.py:2101](file:///C:/Users/User/anaconda3/envs/vectorbt/lib/site-packages/bt/core.py:2101), in bt.core.Strategy.run()

File [c:\Users\User\anaconda3\envs\vectorbt\lib\site-packages\bt\core.py:2040](file:///C:/Users/User/anaconda3/envs/vectorbt/lib/site-packages/bt/core.py:2040), in bt.core.AlgoStack.__call__()

File [c:\Users\User\anaconda3\envs\vectorbt\lib\site-packages\bt\algos.py:1160](file:///C:/Users/User/anaconda3/envs/vectorbt/lib/site-packages/bt/algos.py:1160), in WeighInvVol.__call__(self, target)
   1158 t0 = target.now - self.lag
   1159 prc = target.universe.loc[t0 - self.lookback : t0, selected]
-> 1160 tw = bt.ffn.calc_inv_vol_weights(prc.to_returns().dropna())
   1161 target.temp["weights"] = tw.dropna()
   1162 return True

File [c:\Users\User\anaconda3\envs\vectorbt\lib\site-packages\ffn\core.py:1571](file:///C:/Users/User/anaconda3/envs/vectorbt/lib/site-packages/ffn/core.py:1571), in calc_inv_vol_weights(returns)
   1569 # calc vols
   1570 vol = np.divide(1.0, np.std(returns, ddof=1))
-> 1571 vol[np.isinf(vol)] = np.NaN
   1572 volsum = vol.sum()
   1573 return np.divide(vol, volsum)

As I look into the issue, it seems that np.std(returns, ddof=1) may return a series of object dtype. It seems that np.isinf cannot handle this.

foo        55.35794
bar      115.241039
fake1    148.411672
fake2     71.772825
dtype: object

Additional context Pandas version: 2.0.0 Numpy version: 1.23.5

Proposed fix The solution is a quick simply one, suggest to enforce a float type.

def calc_inv_vol_weights(returns):
    """
    Calculates weights proportional to inverse volatility of each column.

    Returns weights that are inversely proportional to the column's
    volatility resulting in a set of portfolio weights where each position
    has the same level of volatility.

    Note, that assets with returns all equal to NaN or 0 are excluded from
    the portfolio (their weight is set to NaN).

    Returns:
        Series {col_name: weight}
    """
    # calc vols
    vol = np.divide(1.0, np.std(returns, ddof=1))**.astype(np.float64)**
    vol[np.isinf(vol)] = np.NaN
    volsum = vol.sum()
    return np.divide(vol, volsum)