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()