← Back to Documentations

Signature Description Parameters
template<arithmetic T, typename I = unsigned long,
         std::size_t A = 0>
struct  CalmarRatioVisitor;

// ---------------------------------

template<typename T, typename I = unsigned long,
         std::size_t A = 0>
using calmarr_v = CalmarRatioVisitor<T, I, A>;
This is a "single action visitor", meaning it is passed the whole data vector in one call and you must use the single_act_visit() interface.

The Calmar ratio is the annualized compound return of a strategy divided by its maximum drawdown:
Calmar = (final_equity ^ (periods_per_year / n) - 1) / max_drawdown
        
The visitor builds an equity curve that starts at 1 and compounds the returns. The maximum drawdown is the largest peak-to-trough decline of that curve, as a fraction of the peak. It is always greater than or equal to 0. The starting capital counts as the first peak, so a loss in the very first period is a drawdown. By default the input values are simple (percentage) returns. If log_returns is true, they are treated as log returns instead. Percentage and log returns computed from the same prices describe the same equity curve and give the same result.
Edge cases:
A simple return less than or equal to -1 wipes out the equity curve. The final equity is 0, the annualized return is -1 and the maximum drawdown is 1.
If there is no drawdown at all, the ratio is +infinity when the annualized return is positive, and NaN when it is zero.
If there is no valid return, all results are NaN.

get_result(): returns the Calmar ratio.
get_annualized_return(): returns the annualized compound return component.
get_max_drawdown(): returns the maximum drawdown component, as a fraction (0.25 means a 25% decline).

explicit
CalmarRatioVisitor(size_type periods_per_year = 252,
                   bool log_returns = false);

periods_per_year: Number of return periods in a year. Use 252 for daily,
                  52 for weekly and 12 for monthly returns. It must be greater than 0.
log_returns: If true, the input is treated as log returns. The default is simple
        
T: Column data type.
I: Index type.
A: Memory alignment boundary for vectors. Default is system default alignment
static void test_CalmarRatioVisitor()  {

    std::cout << "\nTesting CalmarRatioVisitor{ } ..." << std::endl;

    const double                nval = std::numeric_limits<double>::quiet_NaN();
    const double                inf = std::numeric_limits<double>::infinity();
    ULDataFrame                 df;
    StlVecType<unsigned long>   idxvec = { 123450, 123451, 123452, 123453, 123454, 123455, 123456, 123457, 123458, 123459, 123460, 123461, 123462, 123466, 123467, 123468, 123469, 123470, 123471, 123472, 123473 };
    StlVecType<double>          rets = { 0.025, 0.0245, -0.0065, -0.001, -0.011, 0.0187, 0.0098, 0.0034, 0.0156, -0.0034, 0.023, -0.0034, -0.019, 0.00387, 0.00123, 0.0106, -0.0065, 0.0203, 0.004, -0.01, 0.0059 };
    StlVecType<double>          with_nan = { nval, 0.01, -0.02, 0.005, nval, 0.03, -0.015, 0.007, -0.04, 0.02, 0.011, -0.003, 0.0, 0.025, -0.01, nval, nval, nval, nval, nval, nval };
    StlVecType<double>          no_loss = { 0.01, 0.02, 0.0, 0.01, 0.005, 0.0, 0.0, 0.01, 0.0, 0.003, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
    StlVecType<double>          flat(21, 0.0);
    StlVecType<double>          wipeout = { 0.5, -1.5, 0.2, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };

    df.load_data(std::move(idxvec),
                 std::make_pair("rets", rets),
                 std::make_pair("with_nan", with_nan),
                 std::make_pair("no_loss", no_loss),
                 std::make_pair("flat", flat),
                 std::make_pair("wipeout", wipeout));

    calmarr_v<double>   calmar;

    df.single_act_visit<double>("rets", calmar);
    assert(std::abs(calmar.get_result() - 109.786065) < 0.000001);
    assert(std::abs(calmar.get_annualized_return() - 2.452116) < 0.000001);
    assert(std::abs(calmar.get_max_drawdown() - 0.0223354) < 0.0000001);

    // Same numbers, but interpreted as log returns. Compounding is different,
    // so the results are different.
    //
    {
        calmarr_v<double>   calmar_log { 252, true };

        df.single_act_visit<double>("rets", calmar_log);
        assert(std::abs(calmar_log.get_result() - 114.200555) < 0.000001);
        assert(std::abs(calmar_log.get_annualized_return() - 2.529655) < 0.000001);
        assert(std::abs(calmar_log.get_max_drawdown() - 0.0221510) < 0.0000001);
    }

    // NaNs are skipped
    //
    {
        calmarr_v<double>   calmar_nan;

        df.single_act_visit<double>("with_nan", calmar_nan);
        assert(std::abs(calmar_nan.get_result() - 8.574800) < 0.000001);
        assert(std::abs(calmar_nan.get_annualized_return() - 0.409711) < 0.000001);
        assert(std::abs(calmar_nan.get_max_drawdown() - 0.0477808) < 0.0000001);
    }

    // No drawdown: +infinity, if there is a gain. NaN, if there is none.
    //
    {
        calmarr_v<double>   calmar_nl;
        calmarr_v<double>   calmar_flat;

        df.single_act_visit<double>("no_loss", calmar_nl);
        df.single_act_visit<double>("flat", calmar_flat);
        assert(calmar_nl.get_max_drawdown() == 0.0);
        assert(calmar_nl.get_annualized_return() > 0.0);
        assert(calmar_nl.get_result() == inf);
        assert(calmar_flat.get_max_drawdown() == 0.0);
        assert(calmar_flat.get_annualized_return() == 0.0);
        assert(std::isnan(calmar_flat.get_result()));
    }

    // A return of -150% wipes out the equity curve
    //
    {
        calmarr_v<double>   calmar_wo;

        df.single_act_visit<double>("wipeout", calmar_wo);
        assert(calmar_wo.get_max_drawdown() == 1.0);
        assert(calmar_wo.get_annualized_return() == -1.0);
        assert(calmar_wo.get_result() == -1.0);
    }

#ifdef HMDF_SANITY_EXCEPTIONS
    {
        calmarr_v<double>   bad { 0 };
        bool                thrown { false };

        try  {
            df.single_act_visit<double>("rets", bad);
        }
        catch (const DataFrameError &)  { thrown = true; }
        assert(thrown);
    }
#endif // HMDF_SANITY_EXCEPTIONS

    // Real data
    //
    StrDataFrame    ibm_df;

    try  {
        ibm_df.read("IBM.csv", io_format::csv2);
    }
    catch (const DataFrameError &ex)  {
        std::cout << ex.what() << std::endl;
        ::exit(-1);
    }

    ibm_df.load_result_as_column<double>("IBM_Close", ReturnVisitor<double, std::string>(return_policy::percentage), "IBM_Return");
    ibm_df.load_result_as_column<double>("IBM_Close", ReturnVisitor<double, std::string>(return_policy::log), "IBM_Log_Return");

    calmarr_v<double, std::string>  ibm_calmar;
    calmarr_v<double, std::string>  ibm_calmar_log { 252, true };
    calmarr_v<double, std::string>  ibm_calmar_monthly { 12 };

    ibm_df.single_act_visit<double>("IBM_Return", ibm_calmar);
    ibm_df.single_act_visit<double>("IBM_Log_Return", ibm_calmar_log);
    ibm_df.single_act_visit<double>("IBM_Return", ibm_calmar_monthly);

    assert(std::abs(ibm_calmar.get_result() - 0.0111663) < 0.0000001);
    assert(std::abs(ibm_calmar.get_annualized_return() - 0.00627036) < 0.00000001);
    assert(std::abs(ibm_calmar.get_max_drawdown() - 0.561545) < 0.000001);

    // Percentage and log returns of the same prices describe the same equity
    // curve
    //
    assert(std::abs(ibm_calmar_log.get_result() - ibm_calmar.get_result()) < 0.000001);
    assert(std::abs(ibm_calmar_log.get_max_drawdown() - ibm_calmar.get_max_drawdown()) < 0.000001);

    assert(std::abs(ibm_calmar_monthly.get_result() - 0.000530146) < 0.000000001);
    assert(std::abs(ibm_calmar_monthly.get_annualized_return() - 0.000297701) < 0.000000001);
}

C++ DataFrame