How Certain Is the Result? Build Bootstrap Confidence Intervals with Python

Phase 4-04 showed something easy to miss: many five-bar forward-return rows can share the same future market movement.

many rows
≠
many independent observations

That changes the next question.

We are no longer asking only:

Was the Signal Mean
higher than the Baseline Mean?

We now ask:

How uncertain is
that difference?

1. Keep the Research Design Frozen

Nothing about the trading idea changes.

Signal
SMA20 Cross Above

Signal Time
after Close(t)

Outcome
5-bar next-Open forward return

Entry
Open(t+1)

Exit
Open(t+6)

Baseline
All Eligible Bars

Phase 4-05 adds only one new layer:

uncertainty

2. Start with One Real Forward-Return Observation

Before thinking about bootstrap statistics, first remember what one row of the research table actually means.

The Python program selects a real AAPL SMA20 Cross Above signal and draws the candles around it.

Signal
known after Close(t)

Entry
Open(t+1)

Exit
Open(t+6)

One Observation
=
Open(t+6)
---------
Open(t+1)
- 1

Green candles are bullish. Red candles are bearish.

The shaded region is the exact five-bar future window that becomes one forward-return observation.

AAPL candlestick chart showing one SMA20 Cross Above signal and the five-bar forward-return window used as one bootstrap observation
Figure 1. One observation begins with a signal known after Close(t), then measures the forward return from Open(t+1) to Open(t+6). Green candles are bullish and red candles are bearish.

3. Many Observations Become One Research Number

Repeat the same forward-return calculation across all eligible rows.

Some rows are SMA20 Cross Above events. Most rows are not.

Eligible Row 1
Signal? False
Forward Return +0.8%

Eligible Row 2
Signal? True
Forward Return +2.1%

Eligible Row 3
Signal? False
Forward Return -0.4%

...

From these rows we calculate:

Signal Mean
-
Baseline Mean

That difference is the single statistic studied in this lesson.

4. Work Through a Tiny Example by Hand

Row 1
Signal = False
Return = +1%

Row 2
Signal = True
Return = +3%

Row 3
Signal = False
Return = -1%

Row 4
Signal = True
Return = +2%

Signal mean:

(3% + 2%) / 2
= 2.5%

Baseline mean:

(1% + 3% - 1% + 2%) / 4
= 1.25%

Therefore:

Signal Mean
-
Baseline Mean

=
2.5% - 1.25%

=
+1.25 percentage points

5. A Point Estimate Is Only One Answer

Suppose the real AAPL data gives:

Signal - Baseline
=
+0.20 percentage points

That is the answer from the historical sample we happened to observe.

But how stable is it?

Could a slightly different
sample of market history give:

-0.30?

+0.05?

+0.60?

A point estimate alone cannot answer that.

6. Bootstrap Means “Rebuild the Sample and Recalculate”

A bootstrap creates many alternative samples from the observations we already have.

Sampling is done with replacement, so an observation may appear more than once.

Original

A B C D


Bootstrap Sample 1

B B D A


Bootstrap Sample 2

D A D C

For every bootstrap sample, calculate the same research statistic again:

Signal Mean
-
Baseline Mean

7. The Bootstrap Distribution Is Not a Return Distribution

This distinction is important.

Phase 4-03

distribution of
individual forward returns


Phase 4-05

distribution of
recalculated
Signal - Baseline
mean differences

Phase 4-05 is studying uncertainty in the estimate, not the shape of individual market returns.

8. Why Not Randomly Shuffle Individual Rows?

A basic introductory bootstrap often treats rows as if they were independent.

pick row
pick row
pick row
pick row

But Phase 4-04 showed that adjacent five-bar forward returns can overlap.

They also belong to a time series with local market regimes.

Randomly picking isolated rows destroys that local order.

9. Keep the IID Row Bootstrap Only as a Comparison

The program still calculates an ordinary row bootstrap.

IID Row Bootstrap
— Illustrative

IID means independent and identically distributed.

We do not assume that condition is true here. The IID result is included so we can see what changes when local time order is ignored.

10. Moving-Block Bootstrap Keeps Nearby Rows Together

Instead of choosing isolated rows, choose a consecutive block of rows.

Original Time Order

0 1 2 3 4 5 6 7 8 9 ...


Sample One Block

21 22 23 ... 40


Sample Another Block

301 302 303 ... 320

Join enough blocks to rebuild a sample approximately the same size as the original sequence.

11. See the Block Sampling Directly

The program creates:

moving_block_bootstrap_mechanics.png

The first horizontal line represents the original eligible observations in time order.

Each line below it shows one contiguous block selected from that sequence.

The important visual idea is:

inside one sampled block

nearby observations
stay nearby
Diagram showing moving-block bootstrap sampling of contiguous blocks from the original time-ordered eligible observations
Figure 2. A moving-block bootstrap samples contiguous groups of observations instead of isolated rows, preserving local time order inside each selected block.

12. Why Does a Block Help?

A moving block preserves local ordering inside the block.

row 101
row 102
row 103
...
row 120

Those observations remain together when that block is selected.

This is more appropriate for our time-series problem than pretending each row is completely unrelated to its neighbors.

13. But a Block Bootstrap Is Not Perfect

When one sampled block ends and another begins, the original time relationship between those two blocks is broken.

Block A
...
end

↓ artificial join

Block B
start
...

Therefore:

Moving-Block Bootstrap
≠
perfect reconstruction
of the real market

14. Block Length Is Part of the Model

This lesson uses:

block_length = 20

Twenty bars are used as a clear teaching choice, roughly comparable to one trading month.

We are not claiming that 20 is statistically optimal.

Short block
→ preserves less dependence

Long block
→ preserves longer local structure
but gives fewer distinct blocks

15. Preserve the Signal Flag and Return Together

Each eligible observation is stored as:

(
    is_signal,
    forward_return
)

If a row is resampled, its signal label and historical return move together.

16. One Bootstrap Repetition, Step by Step

1.
Start with the
time-ordered eligible rows

2.
Choose contiguous blocks

3.
Join blocks until
sample size ≈ original size

4.
Find all signal rows
inside that new sample

5.
Calculate Signal Mean

6.
Calculate Baseline Mean

7.
Store:

Signal Mean
-
Baseline Mean

That produces one bootstrap estimate.

17. Repeat 5,000 Times

bootstrap_reps = 5000

After 5,000 repetitions we no longer have one estimate.

We have a distribution of estimates:

estimate 1
estimate 2
estimate 3
...
estimate 5000

18. See the Bootstrap Distribution

The program creates:

moving_block_bootstrap_distribution.png

The histogram shows how frequently different Signal-minus-Baseline estimates appeared across the 5,000 resamples.

Reference lines mark:

zero difference

observed difference

95% lower boundary

95% upper boundary
Moving-block bootstrap distribution of the signal minus baseline mean forward return with zero, observed estimate, and 95 percent interval boundaries
Figure 3. The moving-block bootstrap distribution shows how the Signal − Baseline mean difference changes across repeated resamples. The reference lines mark zero, the observed estimate, and the 95% percentile interval.

19. Build a 95% Percentile Interval

Sort the 5,000 bootstrap estimates.

A simple 95% percentile interval keeps the middle 95%.

lower boundary
=
2.5th percentile

upper boundary
=
97.5th percentile

The program implements the percentile calculation directly, so no new statistical package is required.

20. Read the Interval Visually

Suppose:

Observed Difference
= +0.20 percentage points

95% Bootstrap Interval
= -0.30 to +0.75

Zero is inside the interval.

Under this resampling design, plausible estimates include:

negative difference
zero difference
positive difference

The positive point estimate is therefore still uncertain.

21. What If the Interval Does Not Cross Zero?

95% Bootstrap Interval

+0.10
to
+0.80 percentage points

Under that resampling design, the result is stronger than a positive point estimate alone.

But it still does not mean:

future profit guaranteed

trading strategy proven

out-of-sample robust

profitable after costs

22. Compare IID and Moving-Block Intervals

The program creates:

bootstrap_interval_comparison.png

Both methods start from the same observed point estimate.

What changes is the resampling assumption.

IID Row Bootstrap

individual rows
picked separately


Moving-Block Bootstrap

local sequences
picked together
Comparison of 95 percent bootstrap intervals from IID row resampling and moving-block resampling
Figure 4. The IID-row and moving-block methods start from the same observed point estimate, but their uncertainty intervals can differ because they make different assumptions about dependence.

23. A Narrower Interval Is Not Automatically Better

A narrow interval may look attractive.

But:

narrow interval
from an unrealistic
independence assumption

≠

better evidence

The uncertainty model should respect the data structure as much as the current research stage allows.

24. What This Lesson Still Does Not Solve

optimal block length

all forms of serial dependence

market regime changes

multiple testing

parameter selection bias

out-of-sample validation

transaction costs

position accounting

A bootstrap interval is one research layer, not the final strategy verdict.

25. The Complete Python Program

This lesson introduces no new external package.

It reuses:

FinanceDataReader
pandas
matplotlib

and Python's built-in:

random
statistics

Save as:

phase4_05_bootstrap_confidence_interval_v1_1.py
from pathlib import Path
from datetime import date, timedelta
from statistics import mean, stdev
import os
import random

import FinanceDataReader as fdr
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import pandas as pd


# ============================================================
# Phase 4-05 — Bootstrap Confidence Intervals
#
# Learning flow:
#
# Candles
#   ↓
# One forward return
#   ↓
# Many eligible observations
#   ↓
# Signal Mean - Baseline Mean
#   ↓
# Resample
#   ↓
# Recalculate many times
#   ↓
# Bootstrap distribution
#   ↓
# 95% bootstrap interval
#
# Phase 4-04 taught us that adjacent forward returns can
# overlap. Therefore this lesson compares:
#
# 1. IID row bootstrap — illustrative only
# 2. Moving-block bootstrap — time-series diagnostic
# ============================================================


# ------------------------------------------------------------
# 1. Settings
# ------------------------------------------------------------

symbol = "AAPL"

recent_trading_days = 800

sma_period = 20

forward_horizon = 5

bootstrap_reps = 5000

confidence_level = 0.95

# Teaching choice only.
# This is NOT claimed to be the statistically optimal
# block length.
block_length = 20

random_seed = 20260905

SCRIPT_DIR = Path(__file__).resolve().parent

os.chdir(SCRIPT_DIR)


# ------------------------------------------------------------
# 2. SMA and event functions
# ------------------------------------------------------------

def simple_moving_average(values, period):
    result = [None] * len(values)

    if period <= 0:
        raise ValueError(
            "period must be positive"
        )

    for i in range(
        period - 1,
        len(values),
    ):
        window = values[
            i - period + 1 : i + 1
        ]

        result[i] = (
            sum(float(v) for v in window)
            / period
        )

    return result


def above_sma_state(
    close_values,
    sma_values,
):
    result = [None] * len(close_values)

    for i in range(len(close_values)):

        if sma_values[i] is None:
            continue

        result[i] = (
            float(close_values[i])
            > float(sma_values[i])
        )

    return result


def crossover_events(above_state):
    cross_above = [
        False
    ] * len(above_state)

    cross_below = [
        False
    ] * len(above_state)

    for i in range(
        1,
        len(above_state),
    ):
        previous_state = (
            above_state[i - 1]
        )

        current_state = (
            above_state[i]
        )

        if (
            previous_state is None
            or current_state is None
        ):
            continue

        cross_above[i] = (
            current_state is True
            and previous_state is False
        )

        cross_below[i] = (
            current_state is False
            and previous_state is True
        )

    return (
        cross_above,
        cross_below,
    )


# ------------------------------------------------------------
# 3. Forward return
# ------------------------------------------------------------

def next_open_forward_return(
    open_values,
    horizon,
):
    """
    Signal is known after Close(t).

    Entry:
        Open(t+1)

    Exit:
        Open(t+1+horizon)
    """

    result = [
        None
    ] * len(open_values)

    if horizon <= 0:
        raise ValueError(
            "horizon must be positive"
        )

    last_signal_index = (
        len(open_values)
        - horizon
        - 2
    )

    for i in range(
        last_signal_index + 1
    ):
        entry_index = i + 1

        exit_index = (
            entry_index
            + horizon
        )

        entry_open = float(
            open_values[entry_index]
        )

        exit_open = float(
            open_values[exit_index]
        )

        result[i] = (
            exit_open
            / entry_open
            - 1.0
        )

    return result


# ------------------------------------------------------------
# 4. Research statistic
# ------------------------------------------------------------

def signal_minus_baseline_mean(
    records,
):
    """
    Each record is:

        (
            is_signal,
            forward_return,
        )

    Baseline:
        all eligible rows

    Signal:
        rows where is_signal is True

    Statistic:
        Signal Mean - Baseline Mean
    """

    if not records:
        return None

    baseline_values = [
        float(return_value)
        for _, return_value
        in records
    ]

    signal_values = [
        float(return_value)
        for is_signal, return_value
        in records
        if bool(is_signal)
    ]

    if not signal_values:
        return None

    return (
        mean(signal_values)
        - mean(baseline_values)
    )


# ------------------------------------------------------------
# 5. Percentiles
# ------------------------------------------------------------

def percentile(
    values,
    probability,
):
    if not (
        0.0
        <= probability
        <= 1.0
    ):
        raise ValueError(
            "probability must be "
            "between 0 and 1"
        )

    clean_values = sorted(
        float(value)
        for value in values
    )

    if not clean_values:
        raise ValueError(
            "values cannot be empty"
        )

    if len(clean_values) == 1:
        return clean_values[0]

    position = (
        probability
        * (len(clean_values) - 1)
    )

    lower_index = int(position)

    upper_index = min(
        lower_index + 1,
        len(clean_values) - 1,
    )

    weight = (
        position
        - lower_index
    )

    return (
        clean_values[lower_index]
        * (1.0 - weight)
        +
        clean_values[upper_index]
        * weight
    )


def percentile_interval(
    values,
    confidence_level,
):
    alpha = (
        1.0
        - confidence_level
    )

    lower_probability = (
        alpha / 2.0
    )

    upper_probability = (
        1.0
        - alpha / 2.0
    )

    return (
        percentile(
            values,
            lower_probability,
        ),
        percentile(
            values,
            upper_probability,
        ),
    )


# ------------------------------------------------------------
# 6. IID row bootstrap
# ------------------------------------------------------------

def iid_bootstrap_distribution(
    records,
    reps,
    rng,
):
    """
    Educational comparison only.

    This resamples individual rows.

    Phase 4-04 showed why treating every
    time-series row as independent is
    questionable.
    """

    n = len(records)

    if n == 0:
        raise ValueError(
            "records cannot be empty"
        )

    results = []

    while len(results) < reps:

        sample = [
            records[
                rng.randrange(n)
            ]
            for _ in range(n)
        ]

        statistic = (
            signal_minus_baseline_mean(
                sample
            )
        )

        if statistic is not None:
            results.append(
                statistic
            )

    return results


# ------------------------------------------------------------
# 7. Moving-block bootstrap
# ------------------------------------------------------------

def moving_block_bootstrap_indices(
    n,
    block_length,
    rng,
):
    """
    Return source row indices used to build
    one moving-block bootstrap sample.

    Example:

        original rows:
        0 1 2 3 4 5 6 ...

        choose block:
        21 ... 40

        choose another:
        301 ... 320

        concatenate until sample length = n
    """

    if n <= 0:
        raise ValueError(
            "n must be positive"
        )

    if (
        block_length <= 0
        or block_length > n
    ):
        raise ValueError(
            "invalid block_length"
        )

    max_start = (
        n - block_length
    )

    selected_indices = []

    block_starts = []

    while len(
        selected_indices
    ) < n:

        start = rng.randint(
            0,
            max_start,
        )

        block_starts.append(
            start
        )

        selected_indices.extend(
            range(
                start,
                start + block_length,
            )
        )

    return (
        selected_indices[:n],
        block_starts,
    )


def moving_block_bootstrap_sample(
    records,
    block_length,
    rng,
):
    selected_indices, _ = (
        moving_block_bootstrap_indices(
            len(records),
            block_length,
            rng,
        )
    )

    return [
        records[index_value]
        for index_value
        in selected_indices
    ]


def moving_block_bootstrap_distribution(
    records,
    reps,
    block_length,
    rng,
):
    results = []

    while len(results) < reps:

        sample = (
            moving_block_bootstrap_sample(
                records,
                block_length,
                rng,
            )
        )

        statistic = (
            signal_minus_baseline_mean(
                sample
            )
        )

        if statistic is not None:
            results.append(
                statistic
            )

    return results


# ------------------------------------------------------------
# 8. Bootstrap summary
# ------------------------------------------------------------

def summarize_bootstrap(
    values,
    confidence_level,
):
    lower, upper = (
        percentile_interval(
            values,
            confidence_level,
        )
    )

    return {
        "Replicates": len(values),

        "Bootstrap Mean":
            mean(values),

        "Bootstrap SD":
            stdev(values),

        "CI Lower":
            lower,

        "CI Upper":
            upper,

        "Includes Zero":
            lower <= 0.0 <= upper,
    }


# ------------------------------------------------------------
# 9. Candlestick drawing
# ------------------------------------------------------------

def draw_candlesticks(
    ax,
    market_df,
    body_width=0.62,
):
    """
    Bullish:
        seagreen

    Bearish:
        firebrick
    """

    bullish_color = "seagreen"

    bearish_color = "firebrick"

    for x, (_, row) in enumerate(
        market_df.iterrows()
    ):
        open_price = float(
            row["Open"]
        )

        high_price = float(
            row["High"]
        )

        low_price = float(
            row["Low"]
        )

        close_price = float(
            row["Close"]
        )

        bullish = (
            close_price
            >= open_price
        )

        candle_color = (
            bullish_color
            if bullish
            else bearish_color
        )

        ax.vlines(
            x,
            low_price,
            high_price,
            color=candle_color,
            linewidth=1.0,
        )

        body_bottom = min(
            open_price,
            close_price,
        )

        body_height = abs(
            close_price
            - open_price
        )

        if body_height == 0:
            body_height = max(
                high_price - low_price,
                0.01,
            ) * 0.02

        ax.add_patch(
            Rectangle(
                (
                    x
                    - body_width / 2.0,
                    body_bottom,
                ),
                body_width,
                body_height,
                facecolor=candle_color,
                edgecolor=candle_color,
                linewidth=1.0,
                alpha=0.90,
            )
        )


# ------------------------------------------------------------
# 10. Deterministic self-test
# ------------------------------------------------------------

toy_records = [
    (False, 0.01),
    (True, 0.03),
    (False, -0.01),
    (True, 0.02),
]

toy_statistic = (
    signal_minus_baseline_mean(
        toy_records
    )
)

# Signal mean:
# (0.03 + 0.02) / 2
# = 0.025
#
# Baseline mean:
# (0.01 + 0.03 - 0.01 + 0.02) / 4
# = 0.0125
#
# Difference:
# 0.025 - 0.0125
# = 0.0125

assert abs(
    toy_statistic
    - 0.0125
) < 1e-12

assert abs(
    percentile(
        [0, 1, 2, 3, 4],
        0.50,
    )
    - 2.0
) < 1e-12

toy_rng = random.Random(
    123
)

toy_indices, toy_starts = (
    moving_block_bootstrap_indices(
        n=12,
        block_length=3,
        rng=toy_rng,
    )
)

assert len(
    toy_indices
) == 12

assert len(
    toy_starts
) >= 4

print("Self-test")
print("=========")
print(
    "Toy Signal - Baseline:",
    toy_statistic,
)
print(
    "Toy block starts:",
    toy_starts,
)
print("Self-test: PASS")
print()


# ------------------------------------------------------------
# 11. Download market data
# ------------------------------------------------------------

today = date.today()

start_date = (
    today
    - timedelta(days=1800)
).strftime(
    "%Y-%m-%d"
)

end_date = (
    today.strftime(
        "%Y-%m-%d"
    )
)

df = fdr.DataReader(
    symbol,
    start_date,
    end_date,
)

df = df.tail(
    recent_trading_days
).copy()

minimum_rows = (
    sma_period
    + forward_horizon
    + block_length
    + 20
)

if len(df) < minimum_rows:
    raise ValueError(
        "Not enough market data."
    )


# ------------------------------------------------------------
# 12. Rebuild the frozen signal
# ------------------------------------------------------------

close_values = [
    float(value)
    for value in df["Close"]
]

open_values = [
    float(value)
    for value in df["Open"]
]

sma_values = (
    simple_moving_average(
        close_values,
        sma_period,
    )
)

state = above_sma_state(
    close_values,
    sma_values,
)

cross_above, cross_below = (
    crossover_events(
        state
    )
)

df["SMA"] = sma_values

df["Cross Above"] = (
    cross_above
)

df["Cross Below"] = (
    cross_below
)


# ------------------------------------------------------------
# 13. Rebuild the 5-bar outcome
# ------------------------------------------------------------

return_column = (
    f"Open Fwd {forward_horizon}"
)

df[return_column] = (
    next_open_forward_return(
        open_values,
        forward_horizon,
    )
)


# ------------------------------------------------------------
# 14. Build one time-ordered eligible sequence
# ------------------------------------------------------------

eligible_records = []

eligible_source_indices = []

for i in range(len(df)):

    return_value = (
        df.iloc[i][
            return_column
        ]
    )

    if pd.isna(
        return_value
    ):
        continue

    eligible_records.append(
        (
            bool(
                df.iloc[i][
                    "Cross Above"
                ]
            ),
            float(
                return_value
            ),
        )
    )

    eligible_source_indices.append(
        i
    )

signal_count = sum(
    is_signal
    for is_signal, _
    in eligible_records
)

if signal_count < 2:
    raise ValueError(
        "Not enough signal events "
        "for bootstrap."
    )

point_estimate = (
    signal_minus_baseline_mean(
        eligible_records
    )
)


# ------------------------------------------------------------
# 15. Visual 1 — where one observation comes from
# ------------------------------------------------------------

visual_signal_candidates = [
    i
    for i in range(len(df))
    if (
        bool(
            df.iloc[i][
                "Cross Above"
            ]
        )
        and pd.notna(
            df.iloc[i][
                return_column
            ]
        )
        and (
            i
            + forward_horizon
            + 1
            < len(df)
        )
    )
]

source_candlestick_file = (
    SCRIPT_DIR
    / "bootstrap_source_candlestick.png"
)

source_example_file = (
    SCRIPT_DIR
    / "bootstrap_source_example.csv"
)

if visual_signal_candidates:

    example_index = (
        visual_signal_candidates[-1]
    )

    entry_index = (
        example_index + 1
    )

    exit_index = (
        entry_index
        + forward_horizon
    )

    example_return = (
        float(
            df.iloc[
                exit_index
            ]["Open"]
        )
        /
        float(
            df.iloc[
                entry_index
            ]["Open"]
        )
        - 1.0
    )

    pd.DataFrame(
        [
            {
                "Signal Date":
                    df.index[
                        example_index
                    ],

                "Entry Date":
                    df.index[
                        entry_index
                    ],

                "Entry Open":
                    float(
                        df.iloc[
                            entry_index
                        ]["Open"]
                    ),

                "Exit Date":
                    df.index[
                        exit_index
                    ],

                "Exit Open":
                    float(
                        df.iloc[
                            exit_index
                        ]["Open"]
                    ),

                "Forward Return":
                    example_return,
            }
        ]
    ).to_csv(
        source_example_file,
        index=False,
    )

    plot_start = max(
        0,
        example_index - 10,
    )

    plot_end = min(
        len(df),
        exit_index + 8,
    )

    plot_df = df.iloc[
        plot_start:plot_end
    ].copy()

    signal_x = (
        example_index
        - plot_start
    )

    entry_x = (
        entry_index
        - plot_start
    )

    exit_x = (
        exit_index
        - plot_start
    )

    fig, ax = plt.subplots(
        figsize=(12, 7)
    )

    draw_candlesticks(
        ax,
        plot_df,
    )

    ax.plot(
        list(
            range(
                len(plot_df)
            )
        ),
        plot_df["SMA"],
        linewidth=1.4,
        label=f"SMA {sma_period}",
    )

    ax.axvline(
        signal_x,
        linewidth=1.5,
        label=(
            "Signal known "
            "after Close(t)"
        ),
    )

    ax.axvspan(
        entry_x,
        exit_x,
        alpha=0.12,
        hatch="//",
        label=(
            "One observation: "
            "Open(t+1) → Open(t+6)"
        ),
    )

    ax.axvline(
        entry_x,
        linestyle="--",
        linewidth=1.1,
    )

    ax.axvline(
        exit_x,
        linestyle="--",
        linewidth=1.1,
    )

    ax.set_title(
        f"{symbol} — One Forward-Return "
        "Observation Used by the Bootstrap"
    )

    ax.set_ylabel(
        "Price"
    )

    ax.grid(
        axis="y",
        alpha=0.20,
    )

    step = max(
        1,
        len(plot_df) // 8,
    )

    tick_positions = list(
        range(
            0,
            len(plot_df),
            step,
        )
    )

    tick_labels = [
        plot_df.index[i].strftime(
            "%Y-%m-%d"
        )
        for i
        in tick_positions
    ]

    ax.set_xticks(
        tick_positions
    )

    ax.set_xticklabels(
        tick_labels,
        rotation=35,
        ha="right",
    )

    ax.legend()

    fig.subplots_adjust(
        left=0.09,
        right=0.98,
        top=0.90,
        bottom=0.18,
    )

    fig.savefig(
        source_candlestick_file,
        dpi=140,
    )

    plt.close(fig)


# ------------------------------------------------------------
# 16. Visual 2 — what one moving-block sample does
# ------------------------------------------------------------

example_rng = random.Random(
    random_seed
)

example_indices, example_block_starts = (
    moving_block_bootstrap_indices(
        len(
            eligible_records
        ),
        block_length,
        example_rng,
    )
)

block_example_rows = []

for block_number, start in enumerate(
    example_block_starts[:6],
    start=1,
):
    block_example_rows.append(
        {
            "Block":
                block_number,

            "Start Eligible Row":
                start,

            "End Eligible Row":
                start
                + block_length
                - 1,
        }
    )

block_example_df = pd.DataFrame(
    block_example_rows
)

block_example_file = (
    SCRIPT_DIR
    / "moving_block_bootstrap_example.csv"
)

block_example_df.to_csv(
    block_example_file,
    index=False,
)

block_map_file = (
    SCRIPT_DIR
    / "moving_block_bootstrap_mechanics.png"
)

fig, ax = plt.subplots(
    figsize=(11, 6.5)
)

n_eligible = len(
    eligible_records
)

ax.hlines(
    y=0,
    xmin=0,
    xmax=n_eligible - 1,
    linewidth=3.0,
)

ax.text(
    0,
    0.20,
    "Original eligible time order",
    va="bottom",
)

display_blocks = (
    block_example_df.head(5)
)

for row_number, row in enumerate(
    display_blocks.itertuples(
        index=False
    ),
    start=1,
):
    y_value = row_number

    start_value = int(
        getattr(
            row,
            "_1",
        )
    ) if False else int(
        row[1]
    )

    end_value = int(
        row[2]
    )

    ax.hlines(
        y=y_value,
        xmin=start_value,
        xmax=end_value,
        linewidth=8.0,
    )

    ax.text(
        start_value,
        y_value + 0.16,
        f"Block {row_number}: "
        f"{start_value}–{end_value}",
        va="bottom",
    )

ax.set_xlim(
    -5,
    n_eligible + 5,
)

ax.set_ylim(
    -0.8,
    len(display_blocks) + 1.0,
)

ax.set_yticks(
    [0]
    + list(
        range(
            1,
            len(display_blocks) + 1,
        )
    )
)

ax.set_yticklabels(
    ["Original"]
    + [
        f"Sampled Block {i}"
        for i in range(
            1,
            len(display_blocks) + 1,
        )
    ]
)

ax.set_xlabel(
    "Position in the time-ordered eligible record sequence"
)

ax.set_title(
    f"Moving-Block Bootstrap Mechanics "
    f"(Block Length = {block_length})"
)

ax.grid(
    axis="x",
    alpha=0.20,
)

fig.subplots_adjust(
    left=0.21,
    right=0.98,
    top=0.90,
    bottom=0.14,
)

fig.savefig(
    block_map_file,
    dpi=140,
)

plt.close(fig)


# ------------------------------------------------------------
# 17. Bootstrap distributions
# ------------------------------------------------------------

iid_distribution = (
    iid_bootstrap_distribution(
        eligible_records,
        reps=bootstrap_reps,
        rng=random.Random(
            random_seed
        ),
    )
)

block_distribution = (
    moving_block_bootstrap_distribution(
        eligible_records,
        reps=bootstrap_reps,
        block_length=block_length,
        rng=random.Random(
            random_seed
        ),
    )
)

iid_summary = (
    summarize_bootstrap(
        iid_distribution,
        confidence_level,
    )
)

block_summary = (
    summarize_bootstrap(
        block_distribution,
        confidence_level,
    )
)


# ------------------------------------------------------------
# 18. Save numeric results
# ------------------------------------------------------------

summary_rows = [
    {
        "Method":
            "IID Row Bootstrap — Illustrative",

        "Point Estimate":
            point_estimate,

        "Block Length":
            None,

        **iid_summary,
    },
    {
        "Method":
            "Moving-Block Bootstrap — Diagnostic",

        "Point Estimate":
            point_estimate,

        "Block Length":
            block_length,

        **block_summary,
    },
]

summary_df = pd.DataFrame(
    summary_rows
)

summary_file = (
    SCRIPT_DIR
    / "bootstrap_confidence_interval_summary.csv"
)

summary_df.to_csv(
    summary_file,
    index=False,
)

distribution_df = pd.DataFrame(
    {
        "IID Row Bootstrap":
            iid_distribution,

        "Moving-Block Bootstrap":
            block_distribution,
    }
)

distribution_file = (
    SCRIPT_DIR
    / "bootstrap_mean_difference_distribution.csv"
)

distribution_df.to_csv(
    distribution_file,
    index=False,
)


# ------------------------------------------------------------
# 19. Print a human-readable summary
# ------------------------------------------------------------

print(
    "Observed research result"
)

print(
    "========================"
)

print()

print(
    f"Symbol: {symbol}"
)

print(
    f"Signal count: "
    f"{signal_count}"
)

print(
    "Eligible baseline rows:",
    len(
        eligible_records
    ),
)

print()

print(
    "Point estimate:"
)

print(
    "Signal Mean "
    "- Baseline Mean"
)

print(
    f"{100.0 * point_estimate:.4f} "
    "percentage points"
)

print()

display_df = (
    summary_df.copy()
)

for column_name in [
    "Point Estimate",
    "Bootstrap Mean",
    "Bootstrap SD",
    "CI Lower",
    "CI Upper",
]:
    display_df[
        column_name
    ] = (
        100.0
        * display_df[
            column_name
        ]
    )

print(
    display_df.to_string(
        index=False
    )
)

print()

print("Interpretation guardrails")
print("=========================")

print(
    "IID row bootstrap is "
    "illustrative only."
)

print(
    "Moving-block bootstrap "
    "preserves local chunks, "
    "but depends on block length."
)

print(
    "A bootstrap interval is "
    "conditional on this "
    "resampling design."
)

print(
    "It is not proof of a "
    "durable trading edge."
)


# ------------------------------------------------------------
# 20. Visual 3 — bootstrap distribution
# ------------------------------------------------------------

moving_histogram_file = (
    SCRIPT_DIR
    / "moving_block_bootstrap_distribution.png"
)

block_percent = [
    100.0 * value
    for value
    in block_distribution
]

point_percent = (
    100.0
    * point_estimate
)

block_lower = (
    100.0
    * block_summary[
        "CI Lower"
    ]
)

block_upper = (
    100.0
    * block_summary[
        "CI Upper"
    ]
)

fig, ax = plt.subplots(
    figsize=(10, 6)
)

ax.hist(
    block_percent,
    bins=35,
)

ax.axvline(
    0.0,
    linewidth=1.2,
    label="Zero difference",
)

ax.axvline(
    point_percent,
    linestyle="--",
    linewidth=1.4,
    label="Observed difference",
)

ax.axvline(
    block_lower,
    linestyle=":",
    linewidth=1.4,
    label="95% interval lower",
)

ax.axvline(
    block_upper,
    linestyle=":",
    linewidth=1.4,
    label="95% interval upper",
)

ax.set_title(
    f"{symbol} — Moving-Block Bootstrap "
    "of Signal Minus Baseline Mean"
)

ax.set_xlabel(
    "Signal - Baseline Mean Return "
    "(percentage points)"
)

ax.set_ylabel(
    "Bootstrap Replicates"
)

ax.grid(
    axis="y",
    alpha=0.20,
)

ax.legend()

fig.subplots_adjust(
    left=0.11,
    right=0.97,
    top=0.90,
    bottom=0.14,
)

fig.savefig(
    moving_histogram_file,
    dpi=140,
)

plt.close(fig)


# ------------------------------------------------------------
# 21. Visual 4 — interval comparison
# ------------------------------------------------------------

interval_comparison_file = (
    SCRIPT_DIR
    / "bootstrap_interval_comparison.png"
)

methods = [
    "IID Row Bootstrap\nIllustrative",
    "Moving-Block Bootstrap\nDiagnostic",
]

centers = [
    point_percent,
    point_percent,
]

lowers = [
    100.0
    * iid_summary[
        "CI Lower"
    ],

    100.0
    * block_summary[
        "CI Lower"
    ],
]

uppers = [
    100.0
    * iid_summary[
        "CI Upper"
    ],

    100.0
    * block_summary[
        "CI Upper"
    ],
]

lower_errors = [
    centers[i]
    - lowers[i]
    for i in range(2)
]

upper_errors = [
    uppers[i]
    - centers[i]
    for i in range(2)
]

fig, ax = plt.subplots(
    figsize=(10, 5.5)
)

ax.errorbar(
    centers,
    [0, 1],
    xerr=[
        lower_errors,
        upper_errors,
    ],
    fmt="o",
    capsize=6,
)

ax.axvline(
    0.0,
    linewidth=1.2,
)

ax.set_yticks(
    [0, 1]
)

ax.set_yticklabels(
    methods
)

ax.set_xlabel(
    "Signal - Baseline Mean Return "
    "(percentage points)"
)

ax.set_title(
    f"{symbol} — 95% Bootstrap Interval Comparison"
)

ax.grid(
    axis="x",
    alpha=0.20,
)

fig.subplots_adjust(
    left=0.28,
    right=0.97,
    top=0.88,
    bottom=0.16,
)

fig.savefig(
    interval_comparison_file,
    dpi=140,
)

plt.close(fig)


# ------------------------------------------------------------
# 22. Finish
# ------------------------------------------------------------

print()
print("Files saved:")
print(summary_file)
print(distribution_file)

if visual_signal_candidates:
    print(source_example_file)
    print(source_candlestick_file)

print(block_example_file)
print(block_map_file)
print(moving_histogram_file)
print(interval_comparison_file)

26. Run the Program

python phase4_05_bootstrap_confidence_interval_v1_1.py

First confirm:

Self-test: PASS

Then inspect:

bootstrap_source_example.csv
bootstrap_source_candlestick.png

moving_block_bootstrap_example.csv
moving_block_bootstrap_mechanics.png

bootstrap_confidence_interval_summary.csv
bootstrap_mean_difference_distribution.csv

moving_block_bootstrap_distribution.png
bootstrap_interval_comparison.png

27. Research Checkpoint

Signal
SMA20 Cross Above

Outcome
5-bar next-Open
forward return

Baseline
All Eligible Bars

Statistic
Signal Mean
-
Baseline Mean

Problem from Phase 4-04
dependent observations

New Tool
bootstrap

Illustrative Method
IID row bootstrap

Time-Series Diagnostic
moving-block bootstrap

Bootstrap Repetitions
5000

Teaching Block Length
20 bars

Interval
95% percentile interval

Current Status
UNCERTAINTY ESTIMATE

Not Yet
position lifecycle
backtest
costs
out-of-sample validation

28. Check Your Understanding

  • One forward-return observation comes from a specific future candle window.
  • The research statistic is Signal Mean minus Baseline Mean.
  • A point estimate does not show its uncertainty.
  • A bootstrap repeatedly rebuilds a sample and recalculates the same statistic.
  • A bootstrap distribution contains repeated statistic estimates, not individual market returns.
  • IID row resampling destroys local time order.
  • A moving-block bootstrap preserves contiguous observations inside each selected block.
  • A block bootstrap still creates artificial joins between sampled blocks.
  • Block length is part of the resampling model.
  • A 95% percentile interval uses the 2.5th and 97.5th percentiles.
  • An interval that excludes zero is stronger evidence than a point estimate alone, but it is not proof of a durable trading edge.

29. Change One Thing Yourself

Keep the signal, horizon, baseline, and bootstrap repetitions frozen.

Change only:

block_length = 20

Try:

10
20
40

Ask:

Does the interval widen?

Does it narrow?

Does zero move
inside or outside
the interval?

Does the conclusion
depend strongly
on block length?

Do not choose the block length that gives the most attractive result.

This is sensitivity analysis, not optimization.

30. What You Just Learned

Real Candles
     ↓
Forward Return
     ↓
Eligible Observations
     ↓
Signal - Baseline
Point Estimate
     ↓
Resample
     ↓
Recalculate
many times
     ↓
Bootstrap Distribution
     ↓
95% Interval
     ↓
Uncertainty

while remembering:

Time-Series Dependence
     ↓
Resampling Design Matters

Uncertainty is not something added after the result. It is part of the result.

31. Where Do We Go Next?

Phase 4 has now moved through:

Rule
↓
Forward Outcome
↓
Baseline
↓
Distribution
↓
Dependence
↓
Uncertainty

We still do not have an actual trading position.

Phase 4-06 will answer:

How do Entry and Exit events
become a position
that exists through time?
CASH
↓
ENTRY
↓
LONG
↓
EXIT
↓
CASH

Sources and Further Reading