Propagator#

Description#

Propagator computes the predicted price impact at each time step by convolving past signed order flow with a power-law decaying kernel. The model was introduced by Bouchaud, Gefen, Potters, and Wyart (2004) to capture the empirical observation that the price impact of a trade does not disappear instantly but decays slowly over many subsequent periods.

The predicted impact at time t is:

impact_t = sum_{k=0}^{window-1} G(k) * flow_{t-k}

where the propagator kernel is:

G(k) = g0 * (k + 1)^(-gamma)

Here flow_{t-k} is the signed order flow k periods in the past. The kernel G assigns full weight g0 to the current period (k = 0) and gradually decreasing weight to older flow, following a power law with exponent gamma. When gamma = 0.5 (the default), the memory decays slowly enough to be called long-memory: past flow continues to affect the predicted price even after many periods.

The operator requires window samples before producing its first output. The first window - 1 outputs are NaN (warmup period).

Because this is a positional (FIR) filter, it follows the propagate NaN policy, like Lag and Diff: a NaN flow value is kept in the window and flows through the convolution, so the output is NaN while the NaN is inside the window and recovers once it drops out. Dropping the NaN instead (as an ignore-policy statistic would) would misalign the kernel with the wrong lags.

The operator processes one sample per step.

References:

  • Bouchaud, J.-P., Gefen, Y., Potters, M., and Wyart, M. (2004). "Fluctuations and response in financial markets: the subtle nature of 'random' price changes." Quantitative Finance, 4(2), 176-190.

Examples#

Basic usage#

import numpy as np
from screamer import Propagator

# Isolated unit of buy flow at t=0 and t=3; window=3 reveals the kernel shape
flow = np.array([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
impact = Propagator(window=3, g0=1.0, gamma=0.5)(flow)
# G = [1.0, 2^-0.5, 3^-0.5] = [1.0, 0.70711, 0.57735]
# t=0, t=1: NaN (warmup, fewer than window samples seen)
# t=2: G[0]*flow[2] + G[1]*flow[1] + G[2]*flow[0] = 0 + 0 + 0.57735 = 0.57735
# t=3: G[0]*flow[3] + G[1]*flow[2] + G[2]*flow[1] = 1.0 + 0 + 0 = 1.0
# t=4: G[0]*flow[4] + G[1]*flow[3] + G[2]*flow[2] = 0 + 0.70711 + 0 = 0.70711
# t=5: G[0]*flow[5] + G[1]*flow[4] + G[2]*flow[3] = 0 + 0 + 0.57735 = 0.57735

Streaming one sample at a time#

op = Propagator(window=20, g0=1.0, gamma=0.5)
for signed_flow in flow_stream:
    predicted_impact = op(float(signed_flow))

Reset clears accumulated history#

op = Propagator(window=10)
impact_first_pass = op(flow)
op.reset()
impact_second_pass = op(flow)   # identical to first pass

Usage plot#

import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from screamer import Propagator

# One unit of buy flow, fired after the warmup window so the output traces the
# whole impact kernel: impact peaks with the trade, then relaxes.
window = 30
n = window + 45
flow = np.zeros(n)
trade = window + 5
flow[trade] = 1.0
impact = Propagator(window=window, g0=1.0, gamma=0.9)(flow)

lo = trade - 2
t = np.arange(lo, n) - trade                 # time since the trade (0 = the trade)
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.35, 0.65],
                    vertical_spacing=0.08)
fig.add_trace(go.Bar(x=t, y=flow[lo:], name='signed flow', marker_color='seagreen'),
              row=1, col=1)
fig.add_trace(go.Scatter(x=t, y=impact[lo:], name='price impact',
                         line=dict(color='teal')), row=2, col=1)
fig.update_layout(title='Propagator: impact of one trade peaks, then decays',
                  yaxis=dict(title='flow'), yaxis2=dict(title='impact'),
                  xaxis2=dict(title='time since the trade'),
                  margin=dict(l=20, r=20, t=60, b=40),
                  legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1))
fig.show()