Add strategy evaluation
This commit is contained in:
parent
f36b7a8e30
commit
24f7cd60c2
64174
notebooks/evaluate.ipynb
Normal file
64174
notebooks/evaluate.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,8 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = [
|
packages = [
|
||||||
"src/ml"
|
"src/ml",
|
||||||
|
"src/strategy"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
|
|||||||
0
src/strategy/__init__.py
Normal file
0
src/strategy/__init__.py
Normal file
67
src/strategy/evaluation.py
Normal file
67
src/strategy/evaluation.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from strategy import metrics
|
||||||
|
from strategy.strategy import LONG_POSITION, SHORT_POSITION, EXIT_POSITION
|
||||||
|
from strategy.strategy import StrategyBase
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_strategy(
|
||||||
|
data: pd.DataFrame,
|
||||||
|
strategy: StrategyBase,
|
||||||
|
exchange_fee: float = 0.001,
|
||||||
|
interval: str = "5min"):
|
||||||
|
"""Evaluates a trading strategy."""
|
||||||
|
|
||||||
|
# Get strategy positions,
|
||||||
|
# position at time t is used at t+1.
|
||||||
|
# Skip last position as it cannot be evaluated.
|
||||||
|
positions = strategy.run(data)[:-1]
|
||||||
|
|
||||||
|
# Compute returns for long and short positions.
|
||||||
|
close_price = data['close_price'].to_numpy()
|
||||||
|
long_returns = (
|
||||||
|
(close_price[1:] - close_price[:-1]) / close_price[:-1])
|
||||||
|
short_returns = (
|
||||||
|
(close_price[:-1] - close_price[1:]) / close_price[1:])
|
||||||
|
assert positions.shape == long_returns.shape
|
||||||
|
assert positions.shape == short_returns.shape
|
||||||
|
|
||||||
|
# timestamps = data['close_time'].astype('datetime64[s]').to_numpy()
|
||||||
|
timestamps = data['close_time'].to_numpy()
|
||||||
|
assert positions.shape[0] == timestamps.shape[0] - 1
|
||||||
|
|
||||||
|
# Compute returns of the strategy.
|
||||||
|
strategy_returns = np.zeros_like(positions, dtype=np.float64)
|
||||||
|
strategy_returns[positions == LONG_POSITION] = \
|
||||||
|
long_returns[positions == LONG_POSITION]
|
||||||
|
strategy_returns[positions == SHORT_POSITION] = \
|
||||||
|
short_returns[positions == SHORT_POSITION]
|
||||||
|
|
||||||
|
# Include exchange fees
|
||||||
|
positions_changed = np.append([EXIT_POSITION], positions[:-1]) != positions
|
||||||
|
strategy_returns[positions_changed] = (
|
||||||
|
strategy_returns[positions_changed] + 1.0) * (1.0 - exchange_fee) - 1.0
|
||||||
|
|
||||||
|
strategy_returns = np.append([0.], strategy_returns)
|
||||||
|
portfolio_value = np.cumprod(strategy_returns + 1)
|
||||||
|
|
||||||
|
# Compute all the metrics
|
||||||
|
result = {
|
||||||
|
'value': portfolio_value[-1],
|
||||||
|
'total_return': portfolio_value[-1] - 1,
|
||||||
|
'arc': metrics.arc(portfolio_value, interval=interval),
|
||||||
|
'asd': metrics.asd(portfolio_value, interval=interval),
|
||||||
|
'ir': metrics.ir(portfolio_value, interval=interval),
|
||||||
|
'md': metrics.max_drawdown(portfolio_value),
|
||||||
|
'n_trades': np.sum(np.append([EXIT_POSITION], positions[:-1]) !=
|
||||||
|
np.append(positions[1:], [EXIT_POSITION])),
|
||||||
|
'long_pos': np.sum(positions == LONG_POSITION) / positions.size,
|
||||||
|
'short_pos': np.sum(positions == SHORT_POSITION) / positions.size,
|
||||||
|
# Arrays
|
||||||
|
'portfolio_value': portfolio_value,
|
||||||
|
'strategy_returns': strategy_returns,
|
||||||
|
'strategy_positions': np.append([EXIT_POSITION], positions),
|
||||||
|
'time': timestamps
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
54
src/strategy/metrics.py
Normal file
54
src/strategy/metrics.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
|
||||||
|
NUM_INTERVALS = {
|
||||||
|
'min': 365 * 24 * 60,
|
||||||
|
'5min': 365 * 24 * 12,
|
||||||
|
'hour': 365 * 24,
|
||||||
|
'day': 365
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def investment_return(array: NDArray[Any]):
|
||||||
|
"""Return at the end of the investment period."""
|
||||||
|
return (array[-1] - array[0]) / array[0]
|
||||||
|
|
||||||
|
|
||||||
|
def arc(array: NDArray[Any], interval: str = '5min'):
|
||||||
|
"""Annualised Return Compounded for the investment period."""
|
||||||
|
return np.power(array[-1] / array[0],
|
||||||
|
NUM_INTERVALS[interval] / array.size) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def asd(array: NDArray[Any], interval: str = '5min'):
|
||||||
|
"""Annualised Standard Deviation for the investment period."""
|
||||||
|
simple_returns = (array[1:] - array[:-1]) / array[:-1]
|
||||||
|
avg_simple_return = np.mean(simple_returns)
|
||||||
|
return np.sqrt(
|
||||||
|
(NUM_INTERVALS[interval] /
|
||||||
|
array.size) *
|
||||||
|
np.sum(
|
||||||
|
np.power(
|
||||||
|
simple_returns -
|
||||||
|
avg_simple_return,
|
||||||
|
2)))
|
||||||
|
|
||||||
|
|
||||||
|
def ir(array: NDArray[Any], interval: str = '5min'):
|
||||||
|
"""Information Ratio, the amount of return for a given unit of risk."""
|
||||||
|
std = asd(array, interval=interval)
|
||||||
|
return arc(array, interval=interval) / std if std else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def max_drawdown(array: NDArray[Any]):
|
||||||
|
"""The maximum percentage drawdown during the investment period."""
|
||||||
|
cummax = np.maximum.accumulate(array)
|
||||||
|
return np.max((cummax - array) / cummax)
|
||||||
|
|
||||||
|
|
||||||
|
# def modified_ir(array: NDArray[Any]):
|
||||||
|
# """Information Ratio adjusted by drawdown and ARC."""
|
||||||
|
# return ir(array) * arc(array) * (np.sign(arc(array)) /
|
||||||
|
# max_drawdown(array))
|
||||||
72
src/strategy/strategy.py
Normal file
72
src/strategy/strategy.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
EXIT_POSITION = 0
|
||||||
|
LONG_POSITION = 1
|
||||||
|
SHORT_POSITION = 2
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyBase:
|
||||||
|
"""Base class for investment strategies."""
|
||||||
|
|
||||||
|
def info(self) -> Dict[str, Any]:
|
||||||
|
"""Returns general informaiton about the strategy."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def run(self, data: pd.DataFrame):
|
||||||
|
"""Run strategy on data."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class BuyAndHoldStrategy(StrategyBase):
|
||||||
|
"""Simple benchmark strategy, always long position"""
|
||||||
|
|
||||||
|
NAME = "BUY_AND_HOLD"
|
||||||
|
|
||||||
|
def info(self) -> Dict[str, Any]:
|
||||||
|
return {'strategy_name': BuyAndHoldStrategy.NAME}
|
||||||
|
|
||||||
|
def run(self, data: pd.DataFrame):
|
||||||
|
return np.full_like(
|
||||||
|
data['close_price'].to_numpy(),
|
||||||
|
LONG_POSITION,
|
||||||
|
dtype=np.int32)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelReturnsPredictionStrategy(StrategyBase):
|
||||||
|
"""Strategy that selects position based on returns predictions."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
predictions,
|
||||||
|
threshold=0.001,
|
||||||
|
name=None):
|
||||||
|
self.predictions = predictions
|
||||||
|
assert 'time_index' in self.predictions.columns
|
||||||
|
assert 'group_id' in self.predictions.columns
|
||||||
|
assert 'prediction' in self.predictions.columns
|
||||||
|
|
||||||
|
self.name = name or "ML Returns prediction"
|
||||||
|
self.threshold = threshold
|
||||||
|
|
||||||
|
def info(self) -> Dict[str, Any]:
|
||||||
|
return {'strategy_name': self.name}
|
||||||
|
|
||||||
|
def run(self, data):
|
||||||
|
arr = pd.merge(
|
||||||
|
data, self.predictions, on=['time_index', 'group_id'],
|
||||||
|
how='left')['prediction'].to_numpy()
|
||||||
|
|
||||||
|
positions = []
|
||||||
|
for i in range(len(arr)):
|
||||||
|
if arr[i] > self.threshold:
|
||||||
|
positions.append(LONG_POSITION)
|
||||||
|
elif arr[i] < -self.threshold:
|
||||||
|
positions.append(EXIT_POSITION)
|
||||||
|
elif not len(positions):
|
||||||
|
positions.append(EXIT_POSITION)
|
||||||
|
else:
|
||||||
|
positions.append(positions[-1])
|
||||||
|
|
||||||
|
return np.array(positions, dtype=np.int32)
|
||||||
Loading…
x
Reference in New Issue
Block a user