Synthetic Control
Construct a Synthetic Control for treated_ticker from a weighted combination of donor_tickers (Abadie, Diamond & Hainmueller, 2010), and estimate the effect of an event/intervention as the post-treatment_period gap between treated_ticker’s actual and synthetic counterfactual return path.
When a single company or market experiences an event (an acquisition, a regulatory change, an index inclusion) and there is no single obvious comparison, the synthetic control builds one from the data: a weighted average of unaffected “donor” tickers, with weights chosen so it tracks the treated ticker as closely as possible before the event. If that pre-event fit is good, the synthetic control’s path after the event estimates what would have happened without it, and the gap is the estimated effect.
The formula is as follows:
\[\text{Weights:}\;\; w \cdot = \text{argmin}_{w} | | \text{treated\_pre} - \text{donors\_pre} \text{@} w | | ^{2},\;\; \text{subject to}\; w \geq 0 \;\text{and sum} (w) = 1 (\text{SLSQP}),\;\; \text{with pre the dates before treatment\_period}\] \[\text{Synthetic control:}\;\; \text{synthetic}_{t} = \text{donors}_{t} \text{@} w \cdot,\;\; \text{for every period t}\] \[\text{Treatment effect:}\;\; \text{gap}_{t} = \text{treated}_{t} - \text{synthetic}_{t},\;\; \text{for every post-event period t}\] \[\text{Average Treatment Effect} = \text{mean of}\; \text{gap}_{t} \;\text{over the post-event periods}\] \[\text{RMSPE ratio} = \sqrt{\operatorname{mean}(\text{gap\_post} ^{2})} / \sqrt{\operatorname{mean}(\text{gap\_pre} ^{2})}\] \[\text{P-Value} = \text{the share of the treated ticker's and every placebo donor's RMSPE ratios that are at least the treated ticker's ratio}\]- All series are the tickers’
column(Return by default)
Unlike difference-in-differences, this does not require the treated and comparison units to follow parallel trends, as the synthetic control is built to match the treated ticker’s own pre-event path. Because there is only one treated unit, the p-value comes from placebo tests: the procedure is rerun with each donor as if it were treated, and the treated ticker’s post/pre fit ratio is compared with theirs. Matching is done on the pre-event outcome path only, a common simplification of the original method.
For more information about the method, see the following papers:
- Abadie, A., Diamond, A., & Hainmueller, J. (2010). “Synthetic Control Methods for Comparative Case Studies: Estimating the Effect of California’s Tobacco Control Program.” Journal of the American Statistical Association, 105(490), 493-505.
- Abadie, A. (2021). “Using Synthetic Controls: Feasibility, Data Requirements, and Methodological Aspects.” Journal of Economic Literature, 59(2), 391-425.
Also known as: SCM, synthetic control method.
Synthetic Control in Python or via the MCP
Install the Finance Toolkit with:
pip install financetoolkit -U
Then call get_synthetic_control as shown below.
from financetoolkit import Toolkit
toolkit = Toolkit(
["BA", "AIR.PA", "LMT", "GD", "RTX", "NOC"],
api_key="FINANCIAL_MODELING_PREP_KEY",
start_date="2023-01-01",
end_date="2024-12-31",
)
# Boeing after the 737 MAX 9 grounding, with the other aerospace companies as donors
toolkit.econometrics.get_synthetic_control(treated_ticker="BA", treatment_period="2024-01-08")
Or, after setting up the Finance Toolkit MCP, ask:
Did Boeing's daily returns change from 8 January 2024, the 737 MAX 9 grounding, compared with a synthetic Boeing built from Airbus, Lockheed Martin, General Dynamics, RTX and Northrop Grumman (2023 to 2024)?
Which returns:
| Value | |
|---|---|
| Average Treatment Effect | -0.0017 |
| Pre-Treatment RMSPE | 0.0156 |
| Post-Treatment RMSPE | 0.0204 |
| RMSPE Ratio | 1.3094 |
| P-Value | 0.5 |
| N Donors | 5 |
| N Pre-Periods | 260 |
| N Post-Periods | 255 |
Using Your Own Data
get_synthetic_control runs on any data, not only prices: ratios, economic indicators, yield curves or your own dataset. Pass a DataFrame, a Series or a dictionary of them to Econometrics as data, with the dates in the rows or the columns, and set changes to how its changes are calculated: "percentage", "log", "difference" for rates and ratios, or None when the data are changes already. For example, for the monthly unemployment rates of the United States and Germany, where each country is a column:
from financetoolkit import Economics, Econometrics
economics = Economics(start_date="2000-01-01")
unemployment = economics.get_unemployment_rate(
countries=["United States", "Germany"], period="monthly"
)
econometrics = Econometrics(data=unemployment, changes="difference")
Then call econometrics.get_synthetic_control with treated_ticker and treatment_period, using the column names of the data as tickers.
Parameters
get_synthetic_control accepts the following parameters.
| Parameter | Type | Description | Example |
|---|---|---|---|
treated_ticker | str | The asset believed to be affected by an event/ intervention starting at treatment_period. | "AAPL" |
treatment_period | str | The first post-treatment period – periods at or after this value (within period’s index) are treated as post-treatment, everything before as pre-treatment (used to fit the synthetic control’s weights). | "2024-06-10" |
donor_tickers | str | list[str] | None | The ticker(s) forming the donor pool the synthetic control is built from. Defaults to None, meaning every other Toolkit ticker (subject to include_benchmark). | ["MSFT", "GOOGL", "AMZN"] |
period | str | The data frequency (daily, weekly, monthly, quarterly, or yearly). Defaults to “daily”. | "weekly" |
column | str | The historical data column to use. Defaults to “Return”. | "Return" |
include_benchmark | bool | Whether to include “Benchmark” in the default donor pool (has no effect when donor_tickers is given explicitly). Defaults to False. | True |
rounding | int | None | The number of decimals to round the results to. Defaults to None. | 2 |