Back to Documentations

Signature Description
enum class  asof_policy : unsigned char  {

    // Default:
    // For each lhs value, find the greatest rhs value that is <= lhs value.
    // This is the classic "last known value" lookup used in time series work
    // (align trade prices to the most recent quote available at that time).
    //
    backward = 1,

    // For each lhs value, find the smallest rhs value that is >= lhs value.
    // Useful for "next event" lookups (e.g. next scheduled earnings date).
    //
    forward  = 2,

    // For each lhs value, find the rhs value whose absolute distance to the
    // lhs value is smallest. Ties are broken in favour of backward (i.e. the
    // earlier rhs row).
    //
    nearest  = 3,
};
Policy for asof_join(): controls how each lhs value is matched to the nearest rhs value when an exact match does not exist.


Signature Description Parameters
template<typename RHS_T, typename ... Ts>
DataFrame
asof_join(const RHS_T &rhs,
          asof_policy ap = asof_policy::backward,
          IndexType tolerance = IndexType { }) const;
For every row in self (lhs), find the nearest row in rhs by index and combine their columns. The result carries lhs's index as its own index.

Unlike join_by_index, which requires exact index matches, asof_join performs a nearest-neighbour lookup so it is well-suited for irregularly-sampled time series (tick data vs quotes, signals vs bars, etc.). Only lhs rows are kept (this is a left-join by design: every lhs row appears exactly once in the result).

Both DataFrames must be sorted ascending by their index before calling this method. The implementation uses binary search (O(N log M)) and never modifies either input.
Columns present in both frames are disambiguated with "lhs."/"rhs." prefixes, following the same convention used by join_by_index.

RHS_T: Type of the rhs DataFrame
Ts: List all the types of all data columns. A type should be specified in the list only once.
rhs: rhs DataFrame
ap: As-of join policy. See above
tolerance: Optional upper bound on |lhs_idx - rhs_idx|. When the nearest rhs index is further away than tolerance, the rhs columns for that lhs row are filled with NaN. A tolerance of 0 (the default) disables the check and always propagates the nearest value regardless of distance.
static void test_asof_join()  {

    std::cout << "\nTesting asof_join( ) ..." << std::endl;

    using MyDataFrame = StdDataFrame<unsigned long>;

    MyDataFrame lhs, rhs;

    lhs.load_index(std::vector<unsigned long>{ 1, 3, 5, 7, 9 });
    lhs.load_column<double>("price", std::vector<double>{ 1.1, 3.3, 5.5, 7.7, 9.9 });

    rhs.load_index(std::vector<unsigned long>{ 2, 4, 6 });
    rhs.load_column<double>("quote", std::vector<double>{ 20.0, 40.0, 60.0 });

    // Test backward
    //
    {
        const auto  result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) };
        const auto  &ridx { result.get_index() };

        assert(ridx.size() == 5);
        assert(ridx[0] == 1 && ridx[1] == 3 && ridx[2] == 5 && ridx[3] == 7 && ridx[4] == 9);

        // lhs price column should be present unmodified
        //
        const auto  &price { result.get_column<double>("price") };

        assert(price[0] == 1.1 && price[4] == 9.9);

        // rhs quote column: lhs=1 has no rhs before it -> NaN
        //
        const auto  &quote { result.get_column<double>("quote") };

        assert(std::isnan(quote[0]));  // lhs=1: no rhs <= 1
        assert(quote[1] == 20.0);  // lhs=3: rhs=2 -> 20
        assert(quote[2] == 40.0);  // lhs=5: rhs=4 -> 40
        assert(quote[3] == 60.0);  // lhs=7: rhs=6 -> 60
        assert(quote[4] == 60.0);  // lhs=9: rhs=6 -> 60 (last carry)
    }

    // Test foreward
    //
    {
        const auto  result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::forward) };
        const auto  &ridx { result.get_index() };

        assert(ridx.size() == 5);
        assert(ridx[0] == 1 && ridx[1] == 3 && ridx[2] == 5 && ridx[3] == 7 && ridx[4] == 9);

        const auto  &quote { result.get_column<double>("quote") };

        // lhs price column should be present unmodified
        //
        const auto  &price { result.get_column<double>("price") };

        assert(price[0] == 1.1 && price[4] == 9.9);

        assert(quote[0] == 20.0);      // lhs=1: rhs=2 -> 20
        assert(quote[1] == 40.0);      // lhs=3: rhs=4 -> 40
        assert(quote[2] == 60.0);      // lhs=5: rhs=6 -> 60
        assert(std::isnan(quote[3]));  // lhs=7: no rhs >= 7
        assert(std::isnan(quote[4]));  // lhs=9: no rhs >= 9
    }

    // Test nearest
    //
    {
        const auto  result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::nearest) };
        const auto  &ridx { result.get_index() };

        assert(ridx.size() == 5);
        assert(ridx[0] == 1 && ridx[1] == 3 && ridx[2] == 5 && ridx[3] == 7 && ridx[4] == 9);

        const auto  &quote { result.get_column<double>("quote") };

        // lhs price column should be present unmodified
        //
        const auto  &price { result.get_column<double>("price") };

        assert(price[0] == 1.1 && price[4] == 9.9);

        assert(quote[0] == 20.0);  // lhs=1: only forward rhs=2
        assert(quote[1] == 20.0);  // lhs=3: tie → backward -> rhs=2
        assert(quote[2] == 40.0);  // lhs=5: tie → backward -> rhs=4
        assert(quote[3] == 60.0);  // lhs=7: only backward -> rhs=6
        assert(quote[4] == 60.0);  // lhs=9: only backward -> rhs=6
    }

    // Test exact match
    //
    {
        MyDataFrame lhs, rhs;

        lhs.load_index(std::vector<unsigned long>{ 3, 5, 8 });
        lhs.load_column<double>("x", std::vector<double>{ 3.0, 5.0, 8.0 });

        rhs.load_index(std::vector<unsigned long>{ 2, 5, 9 });
        rhs.load_column<double>("y", std::vector<double>{ 200.0, 500.0, 900.0 });

        // Backward
        //
        auto        rb { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) };
        const auto  &yb { rb.get_column<double>("y") };

        assert(yb[0] == 200.0);  // lhs=3: rhs=2 (back)
        assert(yb[1] == 500.0);  // lhs=5: exact rhs=5
        assert(yb[2] == 500.0);  // lhs=8: rhs=5 (back, next is 9 which is >8)

        // forward
        //
        auto        rf { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::forward) };
        const auto  &yf { rf.get_column<double>("y") };

        assert(yf[0] == 500.0);   // lhs=3: rhs=5 (fwd)
        assert(yf[1] == 500.0);   // lhs=5: exact rhs=5
        assert(yf[2] == 900.0);   // lhs=8: rhs=9 (fwd)
    }

    // Test tolerance
    //
    {
        const unsigned long tol { 1UL };
        const auto          result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward, tol) };
        const auto          &ridx { result.get_index() };

        assert(ridx.size() == 5);
        assert(ridx[0] == 1 && ridx[1] == 3 && ridx[2] == 5 && ridx[3] == 7 && ridx[4] == 9);

        // lhs price column should be present unmodified
        //
        const auto  &price { result.get_column<double>("price") };

        assert(price[0] == 1.1 && price[4] == 9.9);

        const auto  &quote { result.get_column<double>("quote") };

        assert(std::isnan(quote[0]));  // lhs=1: no rhs <= 1 -> NaN
        assert(quote[1] == 20.0);      // lhs=3: dist 1 <= tol
        assert(quote[2] == 40.0);      // lhs=5: dist 1 <= tol
        assert(quote[3] == 60.0);      // lhs=7: dist 1 <= tol
        assert(std::isnan(quote[4]));  // lhs=9: dist 3 > tol -> NaN
    }

    // Empty rhs
    //
    {
        MyDataFrame lhs, rhs;

        lhs.load_index(std::vector<unsigned long>{ 1, 2, 3 });
        lhs.load_column<double>("price", std::vector<double>{ 1.0, 2.0, 3.0 });

        rhs.load_index(std::vector<unsigned long>{ });
        rhs.load_column<double>("quote", std::vector<double>{ });

        const auto  result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) };
        const auto  &ridx { result.get_index() };

        assert(ridx.size() == 3);
        assert(ridx[0] == 1 && ridx[1] == 2 && ridx[2] == 3);

        // lhs price column should be present unmodified
        //
        const auto  &price { result.get_column<double>("price") };

        assert(price[0] == 1.0 && price[1] == 2.0 && price[2] == 3.0);

        const auto  &quote { result.get_column<double>("quote") };

        for (const auto &v : quote)
            assert(std::isnan(v));
    }

    // Same column names
    //
    {
        MyDataFrame lhs, rhs;

        lhs.load_index(std::vector<unsigned long>{ 1, 3, 5 });
        lhs.load_column<double>("value", std::vector<double>{ 10.0, 30.0, 50.0 });

        rhs.load_index(std::vector<unsigned long>{ 2, 4 });
        rhs.load_column<double>("value", std::vector<double>{ 20.0, 40.0 });

        const auto  result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) };

        assert(result.has_column("lhs.value"));
        assert(result.has_column("rhs.value"));

        const auto  &lv { result.get_column<double>("lhs.value") };
        const auto  &rv { result.get_column<double>("rhs.value") };

        assert(lv[0] == 10.0 && lv[1] == 30.0 && lv[2] == 50.0);
        assert(std::isnan(rv[0]));  // lhs=1: no rhs <= 1
        assert(rv[1] == 20.0);      // lhs=3: rhs=2
    }

    // Single elements
    //
    {
        {   // Single lhs, single rhs, exact
            MyDataFrame lhs, rhs;

            lhs.load_index(std::vector<unsigned long>{ 5 });
            lhs.load_column<double>("x", std::vector<double>{ 1.0 });
            rhs.load_index(std::vector<unsigned long>{ 5 });
            rhs.load_column<double>("y", std::vector<double>{ 99.0 });

            const auto  r { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) };

            assert(r.get_column<double>("y")[0] == 99.0);
        }

        {   // Single lhs, single rhs, lhs before rhs (backward → NaN)
            MyDataFrame lhs, rhs;

            lhs.load_index(std::vector<unsigned long>{ 3 });
            lhs.load_column<double>("x", std::vector<double>{ 1.0 });
            rhs.load_index(std::vector<unsigned long>{ 5 });
            rhs.load_column<double>("y", std::vector<double>{ 99.0 });

            const auto  r { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) };

            assert(std::isnan(r.get_column<double>("y")[0]));
        }

        {   // Single lhs, single rhs, lhs before rhs (forward → value)
            MyDataFrame lhs, rhs;

            lhs.load_index(std::vector<unsigned long>{ 3 });
            lhs.load_column<double>("x", std::vector<double>{ 1.0 });
            rhs.load_index(std::vector<unsigned long>{ 5 });
            rhs.load_column<double>("y", std::vector<double>{ 99.0 });

            const auto  r { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::forward) };

            assert(r.get_column<double>("y")[0] == 99.0);
        }
    }
}

C++ DataFrame