Center of Gravity (John F. Ehlers, Stocks & Commodities, May 2002 —
"The CG Oscillator") — where the window's price mass balances, measured
in bars back from the current one:
The current bar carries weight 1 and the oldest carries period, so
on positive prices the reading is a negative number between
−period and −1: a flat
window balances in the middle at exactly −(period + 1)/2, and it moves
up (toward −1) as recent bars get heavier, i.e. as the price rises.
Ehlers' point was that this is a moment rather than a momentum — it has
essentially no lag, because a change in the newest bar moves the balance
point immediately.
It is not a regression, despite arriving in the same batch: it takes
one position-weighted moment of the price rather than fitting a line, so
nothing here has a slope or a residual.
The sign and the zero line — TradingView's convention, not Ehlers'
Ehlers' published EasyLanguage adds (Length + 1)/2 at the end, which
re-centres a flat window on 0; TradingView's built-in ta.cog leaves
it off, so a flat window reads −(period + 1)/2 (−5.5 at the default
period 10). This ships the uncentred form, the one more consumers
plot. The two differ by a constant that depends only on period, so the
shape is identical and a caller who wants Ehlers' zero line adds
(period + 1)/2. The oracle pins the choice with a separation assert
rather than leaving it to be discovered from a chart, and the sign
convention is pinned the same way (the ascending-weight reading — oldest
bar lightest — is 0.17 away at period 10, a full window's worth of
the reading's own range on the fixture).
TA-Lib has no Center of Gravity, so the oracle is a pandas
replication with the analytic first-valid bar (period − 1) asserted.
No kernel: it is wma and sma, exactly
The weights run down from the oldest bar, which neither
rollingWeightedMeanValues (per-row weights, not positional)
nor symmetricWeightedValues (a fixed 4-bar 1,2,2,1) can
express — but no new kernel is needed either, because the descending
weights are the ascending ones subtracted from a constant. Writing u
for the position in the window (0 oldest, n − 1 newest), the numerator
weight k + 1 is n − u, so
and dividing by Σp = n·SMA collapses the whole study to
${output} = (period +1) · ( WMA/ (2 · SMA) − 1 )
— the K2 engine's wma over rollingMeanValues' sma, both O(N)
and flat in period, both already carrying the package's rebuild-every-
period numerics and its strict-window mask. The identity is exact
algebra rather than an approximation, and it is pinned by a test
against the naive O(N·period) definition (agreement ≤ 1.1e-14 at
period 20 on the oracle input; measured, not pinned: 8.0e-15 over 50k
bars at a price of 1e12 on one series and 2.5e-14 on another — a few
ulps either way), so a future editor can check the shortcut rather than
trust it.
Edges
Bounded −period … −1 on a positive source column, and both ends
are reachable only in the limit (all the weight on the oldest bar, or on
the newest). The bound is a property of positive weights over positive
prices, not of the formula: a column that crosses zero (an oscillator,
a spread) can read outside it — measured −0.667 at period 2 on
[5, 1, −1, 4], the zero-crossing case the bounds test does not cover
because it runs on closes only.
A zero-sum window reads undefined, and the guard is live. The
division is the study's output, and the numerator is not forced
to zero with the denominator — [1, −1] at period 2 sums to 0
with a weighted sum of −1 — so an unguarded x / 0 would reach
withColumn as −Infinity and throw. This is the
choppinessIndex live-guard case, not the ulcerIndex
dead one. Unreachable on prices (they are positive, so a zero sum
means an all-zero window and 0/0 is already NaN); reachable, and
unit-tested, over another study's output that crosses zero.
Scale-invariant, not shift-invariant. Every term of both sums
scales with the price and the ratio divides it out; adding a
constant does not cancel — it adds (n+1)/2·c to the numerator and
c to the mean, dragging the balance point toward the middle. Both
halves are pinned, the second as a direction.
The strict window — a bar is emitted only when all period cells
are finite, because a positional weight cannot skip a cell without
silently re-weighting the rest (wma's rule, and rollingMeanValues
masks the same way, so the two halves agree on where a reading
exists). A leading gap shifts the start; an interior gap
blanks period bars and then recovers.
A misnamed column reads all-missing rather than throwing.
Center of Gravity (John F. Ehlers, Stocks & Commodities, May 2002 — "The CG Oscillator") — where the window's price mass balances, measured in bars back from the current one:
The current bar carries weight
1and the oldest carriesperiod, so on positive prices the reading is a negative number between−periodand−1: a flat window balances in the middle at exactly−(period + 1)/2, and it moves up (toward−1) as recent bars get heavier, i.e. as the price rises. Ehlers' point was that this is a moment rather than a momentum — it has essentially no lag, because a change in the newest bar moves the balance point immediately.It is not a regression, despite arriving in the same batch: it takes one position-weighted moment of the price rather than fitting a line, so nothing here has a slope or a residual.
The sign and the zero line — TradingView's convention, not Ehlers'
Ehlers' published EasyLanguage adds
(Length + 1)/2at the end, which re-centres a flat window on0; TradingView's built-inta.cogleaves it off, so a flat window reads−(period + 1)/2(−5.5at the defaultperiod 10). This ships the uncentred form, the one more consumers plot. The two differ by a constant that depends only onperiod, so the shape is identical and a caller who wants Ehlers' zero line adds(period + 1)/2. The oracle pins the choice with a separation assert rather than leaving it to be discovered from a chart, and the sign convention is pinned the same way (the ascending-weight reading — oldest bar lightest — is 0.17 away atperiod 10, a full window's worth of the reading's own range on the fixture).TA-Lib has no Center of Gravity, so the oracle is a pandas replication with the analytic first-valid bar (
period − 1) asserted.No kernel: it is
wmaandsma, exactlyThe weights run down from the oldest bar, which neither rollingWeightedMeanValues (per-row weights, not positional) nor symmetricWeightedValues (a fixed 4-bar
1,2,2,1) can express — but no new kernel is needed either, because the descending weights are the ascending ones subtracted from a constant. Writingufor the position in the window (0oldest,n − 1newest), the numerator weightk + 1isn − u, soand dividing by
Σp = n·SMAcollapses the whole study to— the K2 engine's
wmaover rollingMeanValues'sma, both O(N) and flat inperiod, both already carrying the package's rebuild-every-periodnumerics and its strict-window mask. The identity is exact algebra rather than an approximation, and it is pinned by a test against the naiveO(N·period)definition (agreement ≤ 1.1e-14 atperiod 20on the oracle input; measured, not pinned: 8.0e-15 over 50k bars at a price of 1e12 on one series and 2.5e-14 on another — a few ulps either way), so a future editor can check the shortcut rather than trust it.Edges
−period … −1on a positive source column, and both ends are reachable only in the limit (all the weight on the oldest bar, or on the newest). The bound is a property of positive weights over positive prices, not of the formula: a column that crosses zero (an oscillator, a spread) can read outside it — measured−0.667atperiod 2on[5, 1, −1, 4], the zero-crossing case the bounds test does not cover because it runs on closes only.undefined, and the guard is live. The division is the study's output, and the numerator is not forced to zero with the denominator —[1, −1]atperiod 2sums to0with a weighted sum of−1— so an unguardedx / 0would reachwithColumnas−Infinityand throw. This is the choppinessIndex live-guard case, not the ulcerIndex dead one. Unreachable on prices (they are positive, so a zero sum means an all-zero window and0/0is alreadyNaN); reachable, and unit-tested, over another study's output that crosses zero.(n+1)/2·cto the numerator andcto the mean, dragging the balance point toward the middle. Both halves are pinned, the second as a direction.periodcells are finite, because a positional weight cannot skip a cell without silently re-weighting the rest (wma's rule, androllingMeanValuesmasks the same way, so the two halves agree on where a reading exists). A leading gap shifts the start; an interior gap blanksperiodbars and then recovers.columnreads all-missing rather than throwing.