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.

ParameterTypeDescriptionExample
treated_tickerstrThe asset believed to be affected by an event/ intervention starting at treatment_period."AAPL"
treatment_periodstrThe 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_tickersstr | list[str] | NoneThe 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"]
periodstrThe data frequency (daily, weekly, monthly, quarterly, or yearly). Defaults to “daily”."weekly"
columnstrThe historical data column to use. Defaults to “Return”."Return"
include_benchmarkboolWhether to include “Benchmark” in the default donor pool (has no effect when donor_tickers is given explicitly). Defaults to False.True
roundingint | NoneThe number of decimals to round the results to. Defaults to None.2
Share