Relative: Lead-Lag and Phase

Relative: Lead-Lag and Phase

All functions · nseries package

CrossCorrelation

func (s Series) CrossCorrelation(s2 Series, n, lag int) Series

CrossCorrelation returns the rolling Pearson correlation of s with s2 displaced by lag bars, where a positive lag pairs each bar of s with the value of s2 lag bars earlier.

At bar j it is the Pearson correlation, with the sums centred within the window, of the n bars s[j-n+1..j] against the n bars s2[j-n+1-lag..j-lag] when lag >= 0, and of the n bars s2[j-n+1..j] against s[j-n+1+lag..j+lag] when lag < 0, so the series displaced into the past is s2 for a positive lag and s for a negative one. The sign of lag is the sign of Shift: a positive lag pairs the current values of s with the earlier values of s2 and so measures how far s2 leads, a negative lag measures how far s leads, and both directions are causal, since every paired value is at or before bar j. Each pair of windows goes through the kernel of Correlation with the window of s2 as the regressor and the window of s as the response, so lag 0 is Correlation bit for bit, and with s2 = s the result is the rolling autocorrelation of s at lag bars. It is the displaced correlation of Katsanos (Intermarket Trading Strategies, Wiley 2008, section 7.2, pages 100 to 102), whose Table 7.3 on page 101 correlates weekly gold returns with the XAU, the dollar index, silver, the Venture index and the CRB at shifts of -10 to +10 days; his positive shift is a negative lag here when gold, the traded market, is s, so his shift of +k days with another market as s2 is s.CrossCorrelation(s2, n, -k). MetaStock’s Correl(DATA ARRAY, DATA ARRAY, PERIODS, SHIFT) shifts the second array to the right, so Correl(s, s2, n, k) pairs the same bars as s.CrossCorrelation(s2, n, k).

s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.

The first valid index, counted from the start of the overlap, is n+|lag|-1, and the minimum overlap is n+|lag|. An empty overlap, n < 2, n > min(len(s), len(s2)) or |lag| > min(len(s), len(s2))-n gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or lag; the lag is compared with the room left after n before its magnitude is taken, so every int value of n and lag, including math.MinInt and math.MaxInt, is accepted without overflow. Otherwise one kernel of capacity n (none at n = 2) serves every bar of the call.

Every output depends only on its two paired windows of n bars, so the value at a bar is the same whatever precedes those windows (start-invariant): dropping leading bars of both inputs leaves the later outputs unchanged bit for bit, and lengthening only the longer input adds leading zeros and changes nothing else. An output whose paired windows are finite is in [-1, 1], the centred co-moment ratio clamped to that range.

A flat paired window of either input gives exactly 0, and at n = 2 the result is the product of the signs of the two changes, -1, 0 or 1. An output whose paired windows hold a NaN or an infinity is NaN, and the outputs whose windows have moved past it recover: for lag >= 0 a non-finite value at overlap index p of s gives NaN on [p, p+n-1] and one in s2 gives NaN on [p+lag, p+lag+n-1]; for lag < 0 the roles swap, s on [p+|lag|, p+|lag|+n-1] and s2 on [p, p+n-1]; each range is clipped to the valid bars.

Use returns, such as RateOfChange, rather than prices, and prefer 1-bar or non-overlapping returns: overlapping k-bar returns smear a peak at one lag into a triangle of half-width k, as the XAU column of Katsanos’s Table 7.3 shows. For markets that close at different times, lag the later-closing market, or use synchronous proxies as Katsanos does with US-listed funds (Stocks & Commodities 35:8). His chapter 6 shifts the US indices by one day to predict the DAX (Table 6.1), and his section 8.4, pages 116 to 118, compares synchronous, leading and lagging 30-minute correlations of the Euro Stoxx against the S&P e-mini, 0.788 synchronous against 0.652 lagging; he reads the leadership as switching around the US open, from leading and lagging values that differ by 0.011 or less. Published leading indicators in Stocks & Commodities include Merrill’s Fed funds trend and free-reserves oscillator (7:7), Snead’s 26-week yield change as a regime filter (9:7), breadth momentum leading index momentum (Oliver and Esayian, 11:1) and the advance-decline line during Fed tightening (McClellan, 22:12). For k >= 1 the composition a.Window(-k).Correlation(b.Shift(k).Window(-k), n) of the overlap views a and b of Overlap is an alternative, and a negative lag -d composes causally as a.Shift(d).Window(-d).Correlation(b.Window(-d), n), which shifts s rather than s2; the method is justified by handling both signs in one call and by its warm-up, which stays aligned with s.

Verification: two independent references, a Python reference in exact rational arithmetic with high-precision square roots and a Julia reference, agree with it; and there are exact identities with existing methods: at lag 0 it equals s.Correlation(s2, n) bit for bit; for k >= 0 and inputs of equal length it equals s.Correlation(s2.Shift(k), n) from bar n-1+k on, before which the zero padding of Shift pollutes the windows of the composition (for a negative lag -d the matching identity shifts s instead, s.Shift(d).Correlation(s2, n) from bar n-1+d on, since a negative Shift of s2 would be look-ahead); s.CrossCorrelation(s2, n, k) equals s2.CrossCorrelation(s, n, -k) on inputs of equal length, the same pairs through a symmetric kernel; and a shifted copy s2 = s.Shift(D) with D >= 0 gives exactly 1 at lag -D on non-flat windows from bar n-1+D on.

LeadLag

func (s Series) LeadLag(s2 Series, n, maxLag int) Series

LeadLag returns the lag, in bars between -maxLag and maxLag, at which the rolling n-bar cross-correlation of s with s2 is strongest in absolute value.

At bar j it takes the value r(k) that s.CrossCorrelation(s2, n, k) gives at that bar for every lag k from -maxLag to maxLag, the same paired windows through the same kernel with the window of s2 as the regressor, and scans them in a fixed order: the incumbent starts at lag 0 with r(0); then for k = 1, 2, …, maxLag, first +k and then -k, the incumbent is replaced by lag k only when math.Abs(r(k)) exceeds math.Abs of the incumbent’s correlation by more than 1e-9; the lag of the final incumbent is the result, as a float64. The tolerance decides ties for the incumbent, so among equally strong lags the shorter wins and a positive lag beats the negative lag of the same length. The objective is the absolute correlation, so inverse relationships, gold against the dollar, are found, and LeadLagCorrelation carries the sign. The sign of the result is the sign of Shift and of CrossCorrelation: a positive lag means that the earlier values of s2 match the current values of s best, so s2 leads by that many bars, and a negative lag means that s leads. Katsanos (Intermarket Trading Strategies, Wiley 2008, section 7.2, Tables 7.3 to 7.5, pages 101 to 107) tabulates displaced correlations at shifts of -10 to +10 days and judges the lead from their lagging and leading averages (LeadLagBias) rather than from the strongest shift, which is the synchronous one in every column of those tables; the rolling scan is original to this library.

s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.

The first valid index, counted from the start of the overlap, is n-1+maxLag, and the minimum overlap is n+maxLag. A warm-up zero before the first valid index is indistinguishable from a chosen lag of 0, so make sure the overlap reaches the minimum before reading the result. An empty overlap, n < 2, maxLag < 0, maxLag > 64 or n+maxLag > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or maxLag; the span test subtracts each span from the room rather than adding the spans, so every int value of n and maxLag, including math.MinInt and math.MaxInt, is accepted without overflow. The cap of 64 on maxLag is a resource bound, at most 129 correlations per bar, where Katsanos scanned 10 each way; the cost is O((2*maxLag+1)*n) per bar with one kernel of capacity n (none at n = 2) per call.

Every output depends only on the 2*maxLag+1 pairs of n-bar windows ending at its bar or up to maxLag bars earlier, which together span the n+maxLag most recent bars of each input, so the value at a bar is the same whatever precedes them (start-invariant): dropping leading bars of both inputs leaves the later outputs unchanged bit for bit, and lengthening only the longer input adds leading zeros and changes nothing else. An output whose lag windows are all finite is an integer-valued float64 in [-maxLag, maxLag].

If the correlation at any scanned lag is NaN, because a window at that lag holds a NaN or an infinity, the output is NaN: the lag windows overlap, so the NaN is the window-local contract of the whole scan rather than of one lag. A non-finite value at overlap index p of either input therefore gives NaN on [p, p+n+maxLag-1], clipped to the valid bars, and the later outputs recover. Two ramps of small integers correlate exactly 1 at every lag (-1 if they run in opposite directions), and an input flat over the n+maxLag bars gives exactly 0 at every lag, so both keep lag 0; ramps with inexact steps come within a few ulps of 1, and the scan still keeps lag 0. Nearly linear windows keep lag 0 as well whenever |r(0)| is within 1e-9 of 1, even when a true displacement exists: s = t + 1e-6*t^2 against its own copy shifted by 3 bars, with n = 30 and maxLag = 5, returns 0. The scan is meant for white-noise-like returns, where a shifted copy is found at its true lag: s2 = s.Shift(D) with 0 < D <= maxLag gives -D, with LeadLagCorrelation exactly 1, wherever the window of s at lag -D is not flat and every other lag correlates below 1-1e-9 in absolute value. The result jumps between integer lags from bar to bar, so persistence rules belong downstream.

Katsanos scans shifts in Stocks & Commodities 38:9, where weekly percent changes computed daily and shifted by -7 to +7 days over two years find that bitcoin leads gold by one day and the S&P 500 by three; in 35:8 one-day leading and lagging correlations of one- and two-week changes ask whether the yen leads Japanese equities, on synchronous US-listed proxies that avoid non-synchronous closes. Saitta (17:5) lags gold by 0 to 20 months against bond yields, takes the signed maximum on one side, and trusts the lead only if it recurs in every 10-year and 5-year sub-period: a one-sided scan of the signed correlation over a lag range, the objective r rather than |r|, is a documented variant rather than a method of its own, and the sub-period check is the block practice described under CorrelationBreak, applied at the chosen lag.

Verification: two independent references, a Python reference and a Julia reference, both working from exact rational sums with high-precision square roots and deciding every comparison of the scan far above binary64 precision with the same tie rule, agree with it; at maxLag = 0 it is 0 wherever the window is finite and NaN elsewhere, and LeadLagCorrelation is Correlation; it equals the scan applied to explicitly computed CrossCorrelation values; and since no identity with an existing method exercises the scan itself, the alternative oracle is those references, which are partial entries on the alternative-oracle list, for the scan.

LeadLagBias

func (s Series) LeadLagBias(s2 Series, n, maxLag int) Series

LeadLagBias returns Katsanos’s time-weighted average of the correlations at which s2 leads minus those at which s leads, over lags 1 to maxLag.

At bar j, with r(k) the value that s.CrossCorrelation(s2, n, k) gives at that bar (the same paired windows through the same kernel with the window of s2 as the regressor), it is the sum, started at 0 and taken in ascending order of k = 1, 2, …, maxLag, of the terms w(k)(r(k) - r(-k)), where w(k) = (maxLag+1-k)/(maxLag(maxLag+1)/2) is Katsanos’s linear time weight: lag 1 has the largest weight, lag maxLag the smallest, and the weights sum to 1 in exact arithmetic. Each term is rounded to a float64 before it is added, an explicit conversion that stops the compiler fusing the multiplication into the addition, so the bias adds no architecture-dependent rounding of its own to the correlations it sums, whose last bits can still differ between architectures. With maxLag = 0 there are no terms and the bias is 0, subject to the NaN rule below. A positive lag pairs the current values of s with the earlier values of s2, the sign of Shift, so a positive bias means that the past moves of s2 correlate more with the present moves of s than the reverse, that is, s2 tends to lead, and a negative bias means that s tends to lead. Katsanos (Intermarket Trading Strategies, Wiley 2008, Tables 7.3 to 7.5, pages 101 to 107) prints the two weighted averages as the “lag avg” and “lead avg” rows of his displaced-correlation tables: with gold, the traded market, as s, the weighted average of the correlations at positive lags, where gold lags the other market, reproduces his lag average, and the weighted average at negative lags, where gold leads, reproduces his lead average: in Table 7.3 they are 0.291 and 0.150 for the XAU against gold and -0.098 and -0.113 for the dollar index, whereas unweighted means give 0.189 and 0.065 for the XAU and do not match his rows. LeadLagBias is his lag average minus his lead average.

s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.

The first valid index, counted from the start of the overlap, is n-1+maxLag, and the minimum overlap is n+maxLag; a warm-up zero before the first valid index is indistinguishable from a bias of 0, so make sure the overlap reaches the minimum before reading the result. An empty overlap, n < 2, maxLag < 0, maxLag > 64 or n+maxLag > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or maxLag; the span test subtracts each span from the room rather than adding the spans, so every int value of n and maxLag, including math.MinInt and math.MaxInt, is accepted without overflow. The cap of 64 on maxLag bounds the work at 129 correlations per bar; the cost is O((2*maxLag+1)*n) per bar with one kernel of capacity n (none at n = 2) per call.

Every output depends only on the pairs of n-bar windows at the scanned lags, ending at its bar or up to maxLag bars earlier, which together span the n+maxLag most recent bars of each input, so the value at a bar is the same whatever precedes them (start-invariant): dropping leading bars of both inputs leaves the later outputs unchanged bit for bit, and lengthening only the longer input adds leading zeros and changes nothing else. An output whose lag windows are all finite lies in [-2, 2] up to one ulp of rounding (2.0000000000000004 is reachable at maxLag = 14), since every correlation is in [-1, 1] and the weights sum to one in exact arithmetic.

If the correlation at any scanned lag, lag 0 included, is NaN, because a window at that lag holds a NaN or an infinity, the output is NaN, the window-local contract shared with LeadLag and LeadLagCorrelation; a non-finite value at overlap index p of either input therefore gives NaN on [p, p+n+maxLag-1], clipped to the valid bars, and the later outputs recover. An input flat over the n+maxLag bars gives 0 at every lag and two ramps of small integers give 1 (or -1) at every lag, so both give a bias of 0; ramps with inexact steps give a bias of order 1e-16. With s2 = s the two lags of each term are the same pairs through a symmetric kernel, so every difference is +0 and the bias is +0 bit for bit; swapping s and s2 swaps r(k) and r(-k) bit for bit, so every term changes sign exactly and the bias is antisymmetric bit for bit apart from the sign of a zero result, which is +0 in both orders. Unlike the integer jumps of LeadLag the bias moves continuously with the correlations, and a value near 0 means no clear leader rather than a leader at lag 0.

Use returns rather than prices, as for CrossCorrelation. Katsanos’s leading against lagging comparisons ask the same question: whether the yen leads Japanese equities, from one-day leading and lagging correlations of one- and two-week changes on synchronous US-listed proxies (Stocks & Commodities 35:8), and whether bitcoin leads gold and the S&P 500, from weekly percent changes computed daily and shifted by -7 to +7 days over two years (38:9); the bias sums that evidence over every lag, weighting the nearest lags most, and changes sign when leadership switches, as Katsanos reads it doing around the US open for the Euro Stoxx against the S&P e-mini in his section 8.4, from differences of 0.011 or less.

Verification: two independent references, a Python reference and a Julia reference, both working from exact rational sums with high-precision square roots, agree with it; it is 0 for s2 = s and antisymmetric, so on inputs of equal length s.LeadLagBias(s2, n, maxLag) equals -s2.LeadLagBias(s, n, maxLag), a zero result aside; Katsanos’s weights reproduce the printed averages of Tables 7.3 to 7.5; and since no identity reaches an existing method, the alternative oracle is those references and the printed tables.

LeadLagCorrelation

func (s Series) LeadLagCorrelation(s2 Series, n, maxLag int) Series

LeadLagCorrelation returns the signed rolling n-bar cross-correlation of s with s2 at the lag chosen by LeadLag.

At bar j it is r(best), the value that s.CrossCorrelation(s2, n, best) gives at that bar, where best is the lag LeadLag returns there: the same scan of r(k) for k from -maxLag to maxLag, the same paired windows through the same kernel with the window of s2 as the regressor, and the same incumbent rule, lag 0 first, then +k and -k for k = 1, 2, …, maxLag, replaced only when math.Abs(r(k)) exceeds math.Abs of the incumbent’s correlation by more than 1e-9, keeping the correlation of the final incumbent rather than its lag. It is signed, so an inverse relationship found by the scan shows as a negative value, and its magnitude is the strongest absolute correlation over the scanned lags, up to the tie tolerance. With maxLag = 0 the scan holds lag 0 alone and the result is Correlation bit for bit. Katsanos prints the displaced correlations that it scans (Intermarket Trading Strategies, Wiley 2008, Tables 7.3 to 7.5, pages 101 to 107) and, in his section 8.4, pages 116 to 118, one leading and one lagging correlation beside the synchronous one, 0.788 synchronous against 0.652 lagging for the Euro Stoxx against the S&P e-mini; there the synchronous correlation is the strongest, and it is what this method would report.

s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.

The first valid index, counted from the start of the overlap, is n-1+maxLag, and the minimum overlap is n+maxLag; a warm-up zero before the first valid index is indistinguishable from a correlation of 0, so make sure the overlap reaches the minimum before reading the result. An empty overlap, n < 2, maxLag < 0, maxLag > 64 or n+maxLag > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or maxLag; the span test subtracts each span from the room rather than adding the spans, so every int value of n and maxLag, including math.MinInt and math.MaxInt, is accepted without overflow. The cap of 64 on maxLag bounds the work at 129 correlations per bar; the cost is O((2*maxLag+1)*n) per bar with one kernel of capacity n (none at n = 2) per call.

Every output depends only on the 2*maxLag+1 pairs of n-bar windows ending at its bar or up to maxLag bars earlier, which together span the n+maxLag most recent bars of each input, so the value at a bar is the same whatever precedes them (start-invariant): dropping leading bars of both inputs leaves the later outputs unchanged bit for bit, and lengthening only the longer input adds leading zeros and changes nothing else. An output whose lag windows are all finite is in [-1, 1].

If the correlation at any scanned lag is NaN, because a window at that lag holds a NaN or an infinity, the output is NaN, the window-local contract of the whole scan, since the lag windows overlap; a non-finite value at overlap index p of either input therefore gives NaN on [p, p+n+maxLag-1], clipped to the valid bars, and the later outputs recover. An input flat over the n+maxLag bars gives exactly 0 at every lag, and the result is the +0 of lag 0; two ramps of small integers correlate exactly 1 at every lag (-1 in opposite directions), and the result is that value, where ramps with inexact steps give a value within a few ulps of it; at n = 2 every r(k) is the product of the signs of two changes, so the result is -1, 0 or 1. A shifted copy s2 = s.Shift(D) with 0 < D <= maxLag gives exactly 1, at lag -D, wherever the window of s at lag -D is not flat and every other lag correlates below 1-1e-9 in absolute value; where |r(0)| is within 1e-9 of 1, as on nearly linear windows such as s = t + 1e-6*t^2, lag 0 is kept and the result is r(0). Ties between equally strong lags go to the incumbent, so the value reported is that of the shorter lag, or of the positive lag at equal length.

Read it with LeadLag: the lag says which input leads and by how many bars, this method says how strongly, and its sign says whether the two move together or against each other. Katsanos sets one-shift leading and lagging correlations beside the synchronous one in his section 8.4, and his scan in Stocks & Commodities 38:9, weekly percent changes computed daily and shifted by -7 to +7 days over two years, reports the strength at which bitcoin leads gold by one day and the S&P 500 by three; 35:8 sets one-day leading and lagging correlations of one- and two-week changes of the yen and Japanese equities beside the synchronous one, on synchronous US-listed proxies. Use returns rather than prices, as for CrossCorrelation.

Verification: two independent references, a Python reference and a Julia reference, both working from exact rational sums with high-precision square roots and deciding every comparison of the scan far above binary64 precision with the same tie rule, agree with it; at maxLag = 0 it is Correlation bit for bit and LeadLag is 0 wherever the window is finite; it equals the scan applied to explicitly computed CrossCorrelation values; and since no identity with an existing method exercises the scan itself, the alternative oracle is those references, which are partial entries on the alternative-oracle list, for the scan.