• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 9394559170

26 Apr 2024 06:36AM UTC coverage: 94.656% (-0.02%) from 94.674%
9394559170

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.28
/freqtrade/data/metrics.py
1
import logging
1✔
2
import math
1✔
3
from datetime import datetime
1✔
4
from typing import Dict, Tuple
1✔
5

6
import numpy as np
1✔
7
import pandas as pd
1✔
8

9

10
logger = logging.getLogger(__name__)
1✔
11

12

13
def calculate_market_change(data: Dict[str, pd.DataFrame], column: str = "close") -> float:
1✔
14
    """
15
    Calculate market change based on "column".
16
    Calculation is done by taking the first non-null and the last non-null element of each column
17
    and calculating the pctchange as "(last - first) / first".
18
    Then the results per pair are combined as mean.
19

20
    :param data: Dict of Dataframes, dict key should be pair.
21
    :param column: Column in the original dataframes to use
22
    :return:
23
    """
24
    tmp_means = []
1✔
25
    for pair, df in data.items():
1✔
26
        start = df[column].dropna().iloc[0]
1✔
27
        end = df[column].dropna().iloc[-1]
1✔
28
        tmp_means.append((end - start) / start)
1✔
29

30
    return float(np.mean(tmp_means))
1✔
31

32

33
def combine_dataframes_by_column(
1✔
34
        data: Dict[str, pd.DataFrame], column: str = "close") -> pd.DataFrame:
35
    """
36
    Combine multiple dataframes "column"
37
    :param data: Dict of Dataframes, dict key should be pair.
38
    :param column: Column in the original dataframes to use
39
    :return: DataFrame with the column renamed to the dict key.
40
    :raise: ValueError if no data is provided.
41
    """
42
    if not data:
1✔
43
        raise ValueError("No data provided.")
1✔
44
    df_comb = pd.concat([data[pair].set_index('date').rename(
1✔
45
        {column: pair}, axis=1)[pair] for pair in data], axis=1)
46
    return df_comb
1✔
47

48

49
def combined_dataframes_with_rel_mean(
1✔
50
        data: Dict[str, pd.DataFrame], fromdt: datetime, todt: datetime,
51
        column: str = "close") -> pd.DataFrame:
52
    """
53
    Combine multiple dataframes "column"
54
    :param data: Dict of Dataframes, dict key should be pair.
55
    :param column: Column in the original dataframes to use
56
    :return: DataFrame with the column renamed to the dict key, and a column
57
        named mean, containing the mean of all pairs.
58
    :raise: ValueError if no data is provided.
59
    """
60
    df_comb = combine_dataframes_by_column(data, column)
1✔
61
    # Trim dataframes to the given timeframe
62
    df_comb = df_comb.iloc[(df_comb.index >= fromdt) & (df_comb.index < todt)]
1✔
63
    df_comb['count'] = df_comb.count(axis=1)
1✔
64
    df_comb['mean'] = df_comb.mean(axis=1)
1✔
65
    df_comb['rel_mean'] = df_comb['mean'].pct_change().fillna(0).cumsum()
1✔
66
    return df_comb[['mean', 'rel_mean', 'count']]
1✔
67

68

69
def combine_dataframes_with_mean(
1✔
70
        data: Dict[str, pd.DataFrame], column: str = "close") -> pd.DataFrame:
71
    """
72
    Combine multiple dataframes "column"
73
    :param data: Dict of Dataframes, dict key should be pair.
74
    :param column: Column in the original dataframes to use
75
    :return: DataFrame with the column renamed to the dict key, and a column
76
        named mean, containing the mean of all pairs.
77
    :raise: ValueError if no data is provided.
78
    """
79
    df_comb = combine_dataframes_by_column(data, column)
1✔
80

81
    df_comb['mean'] = df_comb.mean(axis=1)
1✔
82

83
    return df_comb
1✔
84

85

86
def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
1✔
87
                      timeframe: str) -> pd.DataFrame:
88
    """
89
    Adds a column `col_name` with the cumulative profit for the given trades array.
90
    :param df: DataFrame with date index
91
    :param trades: DataFrame containing trades (requires columns close_date and profit_abs)
92
    :param col_name: Column name that will be assigned the results
93
    :param timeframe: Timeframe used during the operations
94
    :return: Returns df with one additional column, col_name, containing the cumulative profit.
95
    :raise: ValueError if trade-dataframe was found empty.
96
    """
97
    if len(trades) == 0:
1✔
98
        raise ValueError("Trade dataframe empty.")
1✔
99
    from freqtrade.exchange import timeframe_to_resample_freq
1✔
100
    timeframe_freq = timeframe_to_resample_freq(timeframe)
1✔
101
    # Resample to timeframe to make sure trades match candles
102
    _trades_sum = trades.resample(timeframe_freq, on='close_date'
1✔
103
                                  )[['profit_abs']].sum()
104
    df.loc[:, col_name] = _trades_sum['profit_abs'].cumsum()
1✔
105
    # Set first value to 0
106
    df.loc[df.iloc[0].name, col_name] = 0
1✔
107
    # FFill to get continuous
108
    df[col_name] = df[col_name].ffill()
1✔
109
    return df
1✔
110

111

112
def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str,
1✔
113
                          starting_balance: float) -> pd.DataFrame:
114
    max_drawdown_df = pd.DataFrame()
1✔
115
    max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
1✔
116
    max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax()
1✔
117
    max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value']
1✔
118
    max_drawdown_df['date'] = profit_results.loc[:, date_col]
1✔
119
    if starting_balance:
1✔
120
        cumulative_balance = starting_balance + max_drawdown_df['cumulative']
1✔
121
        max_balance = starting_balance + max_drawdown_df['high_value']
1✔
122
        max_drawdown_df['drawdown_relative'] = ((max_balance - cumulative_balance) / max_balance)
1✔
123
    else:
124
        # NOTE: This is not completely accurate,
125
        # but might good enough if starting_balance is not available
126
        max_drawdown_df['drawdown_relative'] = (
1✔
127
            (max_drawdown_df['high_value'] - max_drawdown_df['cumulative'])
128
            / max_drawdown_df['high_value'])
129
    return max_drawdown_df
1✔
130

131

132
def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date',
1✔
133
                         value_col: str = 'profit_ratio', starting_balance: float = 0.0
134
                         ):
135
    """
136
    Calculate max drawdown and the corresponding close dates
137
    :param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
138
    :param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
139
    :param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio')
140
    :return: Tuple (float, highdate, lowdate, highvalue, lowvalue) with absolute max drawdown,
141
             high and low time and high and low value.
142
    :raise: ValueError if trade-dataframe was found empty.
143
    """
144
    if len(trades) == 0:
1✔
145
        raise ValueError("Trade dataframe empty.")
1✔
146
    profit_results = trades.sort_values(date_col).reset_index(drop=True)
1✔
147
    max_drawdown_df = _calc_drawdown_series(
1✔
148
        profit_results,
149
        date_col=date_col,
150
        value_col=value_col,
151
        starting_balance=starting_balance)
152

153
    return max_drawdown_df
1✔
154

155

156
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
1✔
157
                           value_col: str = 'profit_abs', starting_balance: float = 0,
158
                           relative: bool = False
159
                           ) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]:
160
    """
161
    Calculate max drawdown and the corresponding close dates
162
    :param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
163
    :param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
164
    :param value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
165
    :param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
166
    :return: Tuple (float, highdate, lowdate, highvalue, lowvalue, relative_drawdown)
167
             with absolute max drawdown, high and low time and high and low value,
168
             and the relative account drawdown
169
    :raise: ValueError if trade-dataframe was found empty.
170
    """
171
    if len(trades) == 0:
1✔
172
        raise ValueError("Trade dataframe empty.")
1✔
173
    profit_results = trades.sort_values(date_col).reset_index(drop=True)
1✔
174
    max_drawdown_df = _calc_drawdown_series(
1✔
175
        profit_results,
176
        date_col=date_col,
177
        value_col=value_col,
178
        starting_balance=starting_balance
179
    )
180

181
    idxmin = (
1✔
182
        max_drawdown_df['drawdown_relative'].idxmax()
183
        if relative else max_drawdown_df['drawdown'].idxmin()
184
    )
185
    if idxmin == 0:
1✔
186
        raise ValueError("No losing trade, therefore no drawdown.")
1✔
187
    high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
1✔
188
    low_date = profit_results.loc[idxmin, date_col]
1✔
189
    high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
1✔
190
                                   ['high_value'].idxmax(), 'cumulative']
191
    low_val = max_drawdown_df.loc[idxmin, 'cumulative']
1✔
192
    max_drawdown_rel = max_drawdown_df.loc[idxmin, 'drawdown_relative']
1✔
193

194
    return (
1✔
195
        abs(max_drawdown_df.loc[idxmin, 'drawdown']),
196
        high_date,
197
        low_date,
198
        high_val,
199
        low_val,
200
        max_drawdown_rel
201
    )
202

203

204
def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]:
1✔
205
    """
206
    Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane
207
    :param trades: DataFrame containing trades (requires columns close_date and profit_percent)
208
    :param starting_balance: Add starting balance to results, to show the wallets high / low points
209
    :return: Tuple (float, float) with cumsum of profit_abs
210
    :raise: ValueError if trade-dataframe was found empty.
211
    """
212
    if len(trades) == 0:
1✔
213
        raise ValueError("Trade dataframe empty.")
1✔
214

215
    csum_df = pd.DataFrame()
1✔
216
    csum_df['sum'] = trades['profit_abs'].cumsum()
1✔
217
    csum_min = csum_df['sum'].min() + starting_balance
1✔
218
    csum_max = csum_df['sum'].max() + starting_balance
1✔
219

220
    return csum_min, csum_max
1✔
221

222

223
def calculate_cagr(days_passed: int, starting_balance: float, final_balance: float) -> float:
1✔
224
    """
225
    Calculate CAGR
226
    :param days_passed: Days passed between start and ending balance
227
    :param starting_balance: Starting balance
228
    :param final_balance: Final balance to calculate CAGR against
229
    :return: CAGR
230
    """
231
    if final_balance < 0:
1✔
232
        # With leveraged trades, final_balance can become negative.
233
        return 0
×
234
    return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
1✔
235

236

237
def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]:
1✔
238
    """
239
    Calculate expectancy
240
    :param trades: DataFrame containing trades (requires columns close_date and profit_abs)
241
    :return: expectancy, expectancy_ratio
242
    """
243

244
    expectancy = 0
1✔
245
    expectancy_ratio = 100
1✔
246

247
    if len(trades) > 0:
1✔
248
        winning_trades = trades.loc[trades['profit_abs'] > 0]
1✔
249
        losing_trades = trades.loc[trades['profit_abs'] < 0]
1✔
250
        profit_sum = winning_trades['profit_abs'].sum()
1✔
251
        loss_sum = abs(losing_trades['profit_abs'].sum())
1✔
252
        nb_win_trades = len(winning_trades)
1✔
253
        nb_loss_trades = len(losing_trades)
1✔
254

255
        average_win = (profit_sum / nb_win_trades) if nb_win_trades > 0 else 0
1✔
256
        average_loss = (loss_sum / nb_loss_trades) if nb_loss_trades > 0 else 0
1✔
257
        winrate = (nb_win_trades / len(trades))
1✔
258
        loserate = (nb_loss_trades / len(trades))
1✔
259

260
        expectancy = (winrate * average_win) - (loserate * average_loss)
1✔
261
        if (average_loss > 0):
1✔
262
            risk_reward_ratio = average_win / average_loss
1✔
263
            expectancy_ratio = ((1 + risk_reward_ratio) * winrate) - 1
1✔
264

265
    return expectancy, expectancy_ratio
1✔
266

267

268
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
1✔
269
                      starting_balance: float) -> float:
270
    """
271
    Calculate sortino
272
    :param trades: DataFrame containing trades (requires columns profit_abs)
273
    :return: sortino
274
    """
275
    if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
1✔
276
        return 0
1✔
277

278
    total_profit = trades['profit_abs'] / starting_balance
1✔
279
    days_period = max(1, (max_date - min_date).days)
1✔
280

281
    expected_returns_mean = total_profit.sum() / days_period
1✔
282

283
    down_stdev = np.std(trades.loc[trades['profit_abs'] < 0, 'profit_abs'] / starting_balance)
1✔
284

285
    if down_stdev != 0 and not np.isnan(down_stdev):
1✔
286
        sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
1✔
287
    else:
288
        # Define high (negative) sortino ratio to be clear that this is NOT optimal.
289
        sortino_ratio = -100
1✔
290

291
    # print(expected_returns_mean, down_stdev, sortino_ratio)
292
    return sortino_ratio
1✔
293

294

295
def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
1✔
296
                     starting_balance: float) -> float:
297
    """
298
    Calculate sharpe
299
    :param trades: DataFrame containing trades (requires column profit_abs)
300
    :return: sharpe
301
    """
302
    if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
1✔
303
        return 0
1✔
304

305
    total_profit = trades['profit_abs'] / starting_balance
1✔
306
    days_period = max(1, (max_date - min_date).days)
1✔
307

308
    expected_returns_mean = total_profit.sum() / days_period
1✔
309
    up_stdev = np.std(total_profit)
1✔
310

311
    if up_stdev != 0:
1✔
312
        sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
1✔
313
    else:
314
        # Define high (negative) sharpe ratio to be clear that this is NOT optimal.
315
        sharp_ratio = -100
1✔
316

317
    # print(expected_returns_mean, up_stdev, sharp_ratio)
318
    return sharp_ratio
1✔
319

320

321
def calculate_calmar(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
1✔
322
                     starting_balance: float) -> float:
323
    """
324
    Calculate calmar
325
    :param trades: DataFrame containing trades (requires columns close_date and profit_abs)
326
    :return: calmar
327
    """
328
    if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
1✔
329
        return 0
1✔
330

331
    total_profit = trades['profit_abs'].sum() / starting_balance
1✔
332
    days_period = max(1, (max_date - min_date).days)
1✔
333

334
    # adding slippage of 0.1% per trade
335
    # total_profit = total_profit - 0.0005
336
    expected_returns_mean = total_profit / days_period * 100
1✔
337

338
    # calculate max drawdown
339
    try:
1✔
340
        _, _, _, _, _, max_drawdown = calculate_max_drawdown(
1✔
341
            trades, value_col="profit_abs", starting_balance=starting_balance
342
        )
343
    except ValueError:
1✔
344
        max_drawdown = 0
1✔
345

346
    if max_drawdown != 0:
1✔
347
        calmar_ratio = expected_returns_mean / max_drawdown * math.sqrt(365)
1✔
348
    else:
349
        # Define high (negative) calmar ratio to be clear that this is NOT optimal.
350
        calmar_ratio = -100
1✔
351

352
    # print(expected_returns_mean, max_drawdown, calmar_ratio)
353
    return calmar_ratio
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc