The 3-Day Echo: How Foreign Money Really Moves TLKM

Ask any retail trader on Indonesian StockTwits what happens when foreign institutions dump massive amounts of TLKM, and the answer comes back almost unanimously:

The price will drop. Immediately.

It’s the conventional wisdom. Foreign sells on Monday morning, red candle by Monday close. Cause, then effect. Clean and simple.

But what if the market’s memory doesn’t work that way?

What if the real damage — or the real alpha — doesn’t show up until days after the trigger is pulled?

In this post, I’ll walk through a microstructure analysis of TLKM (PT Telkom Indonesia) to test whether foreign net flow actually leads price action. Using a Granger-Causality test on six years of daily data, I found something the spot-trading crowd is missing: a hidden echo effect in the order flow. The true impact of foreign buying or selling doesn’t fire on D-Day. It lies dormant, then detonates with peak statistical significance exactly three days later.

Here’s how I got there.


The Setup

Hypothesis

Data

tlkm_df = df.query('StockCode == "TLKM"').copy()
tlkm_df['Date'] = pd.to_datetime(tlkm_df['Date'])
tlkm_df = tlkm_df.sort_values('Date').set_index('Date')

tlkm_df.info()
<class 'pandas.DataFrame'>
DatetimeIndex: 1517 entries, 2020-01-02 to 2026-04-21
Data columns (total 47 columns):
 #   Column               Non-Null Count  Dtype
---  ------               --------------  -----
 ...
 22  ForeignSell          1517 non-null   float64
 23  ForeignBuy           1517 non-null   float64
 ...

That’s 1,517 trading days — a healthy sample spanning the COVID crash, the post-pandemic recovery, the 2022 rate-hike cycle, and the consolidation regime that followed.


A Quick Detour: What Granger-Causality Actually Tests

Before running the test, it’s worth being honest about what it doesn’t prove. Granger-Causality, in plain English, asks:

Does knowing the past values of variable X help me predict the future values of variable Y better than if I only used Y’s own past?

If the answer is yes, we say X “Granger-causes” Y. But there are three traps that catch most analysts who treat this test casually.

Trap 1: The Rooster and the Sun

A rooster crows every morning right before sunrise. Run a Granger-Causality test on that data and the math will gleefully conclude that the rooster’s crowing causes the sun to rise. The past values of the crow perfectly predict the future values of the sunrise.

The test measures sequence and correlation, not mechanism. We are not proving the foreign desk causes the move — only that it systematically precedes it. That’s still tradeable. Just don’t oversell it at the next quant meetup.

Trap 2: Stationarity Is Non-Negotiable

You cannot run Granger-Causality on raw, trending data. The math assumes stationarity — a stable mean and variance over time.

Skip this step and the test will hallucinate relationships out of pure trend.

Trap 3: Lag Selection

The test takes a parameter p — how many past time steps to look back. Test only lag 1 and you might miss the signal entirely. Test lag 5 and a strong relationship suddenly appears. The honest move is to scan a range of lags and let the statistics speak, which is exactly what we’ll do.


Feature Engineering

The closing price is non-stationary, so we transform it into daily returns. The dataset gives us ForeignBuy and ForeignSell separately, so we derive net foreign flow and normalize it by total volume to get a unitless ratio that’s comparable across days of wildly different liquidity.

In our test:

tlkm_df['net_foreign'] = tlkm_df['ForeignBuy'] - tlkm_df['ForeignSell']
tlkm_df['net_foreign_ratio'] = tlkm_df['net_foreign'] / tlkm_df['Volume']
tlkm_df['returns'] = tlkm_df['Close'].pct_change()

Running the Test

granger_data = tlkm_df[['returns', 'net_foreign_ratio']].dropna()
grangercausalitytests(granger_data, maxlag=10, verbose=True)

Note: The dropna() is not cosmetic. Granger-Causality fits a regression under the hood, and a single NaN propagates through the entire lag matrix, silently corrupting your test statistics. Always clean before testing.

The output (trimmed to the SSR-based F-test for clarity):

Granger Causality
number of lags (no zero) 1
ssr based F test:    F=2.1804, p=0.1400, df_denom=1512, df_num=1

Granger Causality
number of lags (no zero) 2
ssr based F test:    F=3.7939, p=0.0227, df_denom=1509, df_num=2

Granger Causality
number of lags (no zero) 3
ssr based F test:    F=3.8428, p=0.0094, df_denom=1506, df_num=3

Granger Causality
number of lags (no zero) 4
ssr based F test:    F=2.8930, p=0.0212, df_denom=1503, df_num=4

Granger Causality
number of lags (no zero) 5
ssr based F test:    F=2.4810, p=0.0301, df_denom=1500, df_num=5

Granger Causality
number of lags (no zero) 6
ssr based F test:    F=2.2222, p=0.0386, df_denom=1497, df_num=6

Granger Causality
number of lags (no zero) 7
ssr based F test:    F=1.9579, p=0.0575, df_denom=1494, df_num=7

Granger Causality
number of lags (no zero) 8
ssr based F test:    F=1.8728, p=0.0604, df_denom=1491, df_num=8

Granger Causality
number of lags (no zero) 9
ssr based F test:    F=1.6209, p=0.1040, df_denom=1488, df_num=9

Granger Causality
number of lags (no zero) 10
ssr based F test:    F=1.4753, p=0.1426, df_denom=1485, df_num=10

The Story the P-Values Tell

A p-value below 0.05 is our threshold for statistical significance — it means there’s less than a 5% chance the predictive relationship we observed is just luck.

Lag (days)p-valueVerdict
10.140❌ Not significant
20.023✅ Significant
30.009🎯 Peak signal
40.021✅ Significant
50.030✅ Significant
60.039✅ Significant
70.058⚠️ Marginal
80.060⚠️ Marginal
90.104❌ Not significant
100.143❌ Not significant
{
  "data": [
    {
      "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
      "y": [0.14, 0.023, 0.009, 0.021, 0.030, 0.039, 0.058, 0.060, 0.104, 0.143],
      "type": "bar",
      "marker": {
        "color": "steelblue",
        "opacity": 0.7
      }
    }
  ],
  "layout": {
    "title": "Granger Causality p-values: Net Foreign Ratio → Returns",
    "xaxis": { "title": "Lag (Days)" },
    "yaxis": { "title": "p-value" },
    "shapes": [
      {
        "type": "line",
        "x0": 0, "x1": 10,
        "y0": 0.05, "y1": 0.05,
        "line": { "color": "red", "width": 2, "dash": "dashdot" }
      },
      {
        "type": "line",
        "x0": 0, "x1": 10,
        "y0": 0.01, "y1": 0.01,
        "line": { "color": "orange", "width": 2, "dash": "dashdot" }
      }
    ],
    "annotations": [
      { "x": 10, "y": 0.05, "text": "α = 0.05", "showarrow": false, "xanchor": "left" },
      { "x": 10, "y": 0.01, "text": "α = 0.01", "showarrow": false, "xanchor": "left" }
    ]
  }
}

The shape of this curve is the entire story. It’s not flat. It’s not monotonic. It’s a wave — and the wave has a clear arc.

Act I: The Silence (Lag 1)

On the very next trading day after a foreign flow event, there is nothing. The p-value of 0.140 is statistical noise. If your model — or your intraday discretionary trigger — fires on T+1 based on yesterday’s foreign tape, you are trading on a coin flip dressed up as a signal.

This is the most counterintuitive result of the entire study, and it directly contradicts the retail thesis that prices react instantly.

Act II: The Awakening (Lags 2–6)

Then the wave hits.

Starting on Lag 2 (p = 0.023), the relationship snaps into statistical significance. By Lag 3, the signal peaks at p = 0.009 — well below even the strict α = 0.01 threshold. This is the sweet spot. Three days after the foreign desk acts, the market has fully priced in the information, and the directional impact is at its most predictable.

The signal stays significant for three more days, gradually weakening but holding the line through Lag 6.

Act III: The Decay (Lags 7+)

By Lag 7, the p-value crosses back over 0.05 and the edge begins evaporating. By Lag 9 and 10, the signal is gone. The market has fully absorbed the event. Whatever asymmetric information or positioning pressure the foreign flow represented has been arbitraged away.

This decay pattern — silence, climax, dissipation — is exactly what you’d expect from a market that’s neither perfectly efficient nor perfectly inefficient. Information takes time to propagate, and that propagation has a measurable half-life.


Why a 3-Day Lag, Specifically?

The 72-hour delay isn’t arbitrary, and it isn’t magic. A few microstructure mechanisms plausibly drive it:

  1. T+2 settlement. Indonesian equities settle on T+2. Foreign flow reported today represents trades whose cash and custody implications cascade into Day 2 and Day 3, when local desks rebalance against fresh foreign positioning data.
  2. Information diffusion. Foreign institutions often have research-driven views that local participants haven’t yet seen. Domestic funds typically need a day or two to catch up via brokerage notes, news flow, or simply observing the tape.
  3. Momentum confirmation. Local momentum traders rarely act on a single day’s flow. They wait for two-to-three days of confirmation before committing — which is, mechanically, when the cascade kicks in.

The point isn’t to prove which of these dominates. It’s that something structural is happening on a consistent 3-day cycle, and it shows up cleanly in 1,517 days of data.


Conclusion: Trade the Echo, Not the Bang

So — does foreign net flow actually move TLKM?

The answer depends entirely on your stopwatch.

If you measure the impact strictly on T+1, you are forced to accept H₀. There is no immediate, statistically significant relationship. The market doesn’t react the way the conventional retail wisdom claims.

But widen the window, and H₀ shatters. From T+2 through T+6, the data overwhelmingly supports H₁: foreign net flow Granger-causes TLKM’s price returns, with peak significance landing exactly three days after the event.

Trading TLKM on foreign flow isn’t a game of reflexes. It’s a game of patience. The trigger that fires on T+1 is trading noise. The real alpha lives in the echo — and the echo arrives on Day 3.

For practitioners, this reframes the engineering problem entirely. The feature isn’t net_foreign_ratio[t]. It’s net_foreign_ratio[t-3]. And the model that ignores the lag structure is throwing away the only edge the data is actually offering.


Caveats and Honest Limitations

A responsible quant adds the asterisks:

The next post in this series will run the same test across all LQ45 constituents to see whether the 3-day echo is a TLKM quirk or a market-wide signature. Stay tuned.


Author: Andri Syahputra Muda Code and notebook available on request.