s-leroux / fin

Set of tools for personal investment
MIT License
1 stars 0 forks source link

Implement the `drawdown` function #5

Open s-leroux opened 1 year ago

s-leroux commented 1 year ago

We should implement the drawdown function.

A strategy suffers a drawdown whenever it has lost money recently. A drawdown at a given time t is defined as the difference between the current equity value (assuming no redemption or cash infusion) of the portfolio and the global maximum of the equity curve occur- ring on or before time t. The maximum drawdown is the difference between the global maximum of the equity curve with the global minimum of the curve after the occurrence of the global maximum (time order matters here: The global minimum must occur later than the global maximum). — Quantitative Trading (Chan) ,p21

s-leroux commented 5 months ago

The plan is to implement the drawdown function as a CAggregateFunction instance. This would validate the work done as part of #50/#51.

The implementation will follow Chan's definition as given in the OP.

s-leroux commented 5 months ago

The plan is to implement the drawdown function as a CAggregateFunction instance.

We will not seek the "global maximum of the equity curve occurring on or before time t". We will only consider the local maximum over the time window. This might be optimized for rolling windows (like the Sliding Window Maximum).

s-leroux commented 5 months ago

After a second thought, I am not sure this was such a good candidate to be a CAggregateFunction instance: the maximum drawdown is calculated from the highest and lowest-after-that price of an asset. For low-volatility assets, the close price is sufficient. However, for more volatile assets, or assets trading 24/7 where the "close" point in time is arbitrary. In that case, it makes more sense to use both the "high" and "low" prices to extract the extremes.

I suggest rewriting drawdown and maximum_drawdown as a function taking two-column arguments:

def __call__(self, unsigned begin, unsigned end, Column low, Column high=None):
   if high is None:
      high = low
   self.call(....)

This may imply reordering the arguments of the __call__ function.

s-leroux commented 5 months ago

The core difference between ag ("aggregate") and fc ("column"?) functions is the former returns a scalar, whereas the latter returns a new column. The additional begin and end parameters found on ag functions could be added to fc function without altering their functionality. I don't see an urgent need to add them, though.

On the other hand, I don't see the point of having "multi-column" aggregate functions.

I wonder if we could have some kind of generalization of the "function" concept that could work as well with one or several column parameters and that would not break higher-order functions like naive_window, cumulate, or spread, and all that with avoiding a Cypthon-Python round trip esp. costly in window functions.

s-leroux commented 5 months ago

The drawdown function should have a two-column version:

drawdown(0, -1, highs, lows)