Examples

Each example can be run directly from the repository root.

Basic candlestick chart

A minimal chart with volume and two moving averages:

python examples/basic_candle.py
examples/basic_candle.py
"""
Example: basic candlestick chart with wickly.

Run:
    python examples/basic_candle.py
"""

import numpy as np
import pandas as pd

import wickly


def _generate_sample_data(n: int = 120) -> pd.DataFrame:
    """Generate synthetic OHLCV data for demonstration."""
    rng = np.random.default_rng(42)
    dates = pd.bdate_range("2025-01-02", periods=n)
    close = 100.0 + np.cumsum(rng.normal(0, 1.2, n))
    open_ = close + rng.normal(0, 0.5, n)
    high  = np.maximum(open_, close) + rng.uniform(0.2, 1.5, n)
    low   = np.minimum(open_, close) - rng.uniform(0.2, 1.5, n)
    volume = rng.integers(500_000, 5_000_000, n).astype(float)

    return pd.DataFrame(
        {"Open": open_, "High": high, "Low": low, "Close": close, "Volume": volume},
        index=dates,
    )


def main() -> None:
    df = _generate_sample_data()

    # --- identical call style to mplfinance ---
    wickly.plot(
        df,
        type="candle",
        volume=True,
        mav=(10, 20),
        style="yahoo",
        title="wickly — Interactive Candlestick Chart",
    )


if __name__ == "__main__":
    main()

Chart types

Cycle through all four chart types — candle, ohlc, line, hollow:

python examples/chart_types.py
examples/chart_types.py
"""
Example: all chart types side-by-side (run one at a time).

Run:
    python examples/chart_types.py
"""

import numpy as np
import pandas as pd

import wickly


def _data(n: int = 80) -> pd.DataFrame:
    rng = np.random.default_rng(99)
    dates = pd.bdate_range("2025-03-01", periods=n)
    close = 200 + np.cumsum(rng.normal(0, 2, n))
    open_ = close + rng.normal(0, 0.8, n)
    high  = np.maximum(open_, close) + rng.uniform(0.3, 2, n)
    low   = np.minimum(open_, close) - rng.uniform(0.3, 2, n)
    vol   = rng.integers(1_000_000, 8_000_000, n).astype(float)
    return pd.DataFrame(
        {"Open": open_, "High": high, "Low": low, "Close": close, "Volume": vol},
        index=dates,
    )


def main() -> None:
    df = _data()

    for chart_type in ("candle", "ohlc", "line", "hollow"):
        wickly.plot(
            df,
            type=chart_type,
            volume=True,
            mav=(5, 20),
            style="charles",
            title=f"Chart type: {chart_type}",
        )


if __name__ == "__main__":
    main()

Addplot overlays (Bollinger Bands + Knoxville Divergence)

Overlay Bollinger Bands, scatter-plot buy signals, and Knoxville Divergence broken-line segments on top of a candlestick chart:

python examples/addplot_overlay.py
examples/addplot_overlay.py
"""
Example: addplot overlays — Bollinger Bands, buy signals, and overlapping segments.

Demonstrates three addplot types:

- ``type='line'``     — Bollinger Band envelopes (continuous, NaN-gapped for warm-up).
- ``type='scatter'``  — buy-signal markers where price crosses below the lower band.
- ``type='segments'`` — Knoxville-style divergence windows rendered as independent,
                        possibly overlapping broken-line segments via ``make_segments``.

Run:
    python examples/addplot_overlay.py
"""

import numpy as np
import pandas as pd

import wickly


def _generate_sample_data(n: int = 120) -> pd.DataFrame:
    rng = np.random.default_rng(7)
    dates = pd.bdate_range("2025-06-01", periods=n)
    close = 50.0 + np.cumsum(rng.normal(0.05, 0.8, n))
    open_ = close + rng.normal(0, 0.3, n)
    high  = np.maximum(open_, close) + rng.uniform(0.1, 1.0, n)
    low   = np.minimum(open_, close) - rng.uniform(0.1, 1.0, n)
    volume = rng.integers(100_000, 2_000_000, n).astype(float)
    return pd.DataFrame(
        {"Open": open_, "High": high, "Low": low, "Close": close, "Volume": volume},
        index=dates,
    )


def _find_divergence_segments(
    close: pd.Series,
    low: pd.Series,
    mom_period: int = 20,
    window: int = 6,
) -> list[tuple[int, np.ndarray]]:
    """Return a list of (start_index, y_values) tuples marking bullish divergence windows.

    Bullish divergence: price makes a new rolling low but momentum does not —
    suggesting selling pressure is weakening.  Each detection spawns an
    independent segment of ``window`` bars anchored to the low at that bar.
    Because a new signal can fire before the previous window ends, segments
    may overlap — which is exactly what ``make_segments`` is designed for.
    """
    momentum = close - close.shift(mom_period)
    price_at_low = close == close.rolling(mom_period).min()
    # Bullish: price at new low but momentum higher than it was mom_period ago
    bullish = price_at_low & (momentum > momentum.shift(mom_period))

    segments: list[tuple[int, np.ndarray]] = []
    close_arr = close.values
    low_arr = low.values
    n = len(close_arr)

    for i in bullish[bullish].index:
        bar = close.index.get_loc(i)
        end = min(bar + window, n)
        # Anchor the segment to the low at the signal bar, then follow close
        y = np.empty(end - bar)
        y[0] = low_arr[bar] - 0.3          # start slightly below the wick
        y[1:] = close_arr[bar + 1 : end]   # continue along close for visibility
        segments.append((bar, y))

    return segments


def main() -> None:
    df = _generate_sample_data()

    # ------------------------------------------------------------------ #
    # 1. Bollinger Bands  (type='line', NaN during warm-up period)        #
    # ------------------------------------------------------------------ #
    sma   = df["Close"].rolling(20).mean()
    std   = df["Close"].rolling(20).std()
    upper = sma + 2 * std
    lower = sma - 2 * std

    # ------------------------------------------------------------------ #
    # 2. Buy-signal scatter  (type='scatter')                             #
    #    Mark bars where close crosses below the lower band.              #
    # ------------------------------------------------------------------ #
    signal = np.where(
        df["Close"].values < lower.values,
        df["Low"].values - 0.5,
        np.nan,
    )

    # ------------------------------------------------------------------ #
    # 3. Divergence segments  (type='segments' via make_segments)         #
    #    Each divergence window is an independent segment so overlapping  #
    #    signals render as separate lines rather than one merged blob.    #
    # ------------------------------------------------------------------ #
    div_segs = _find_divergence_segments(df["Close"], df["Low"])
    divergence_apd = wickly.make_segments(
        div_segs,
        color="#00bcd4",
        width=2.0,
        linestyle="-",
        ylabel="Divergence",
    )

    apds = [
        # Bollinger Band envelopes
        wickly.make_addplot(upper, type="line", color="#9c27b0", width=1.2, linestyle="--",
                            ylabel="Upper BB"),
        wickly.make_addplot(lower, type="line", color="#9c27b0", width=1.2, linestyle="--",
                            ylabel="Lower BB"),
        # Scatter buy signals
        wickly.make_addplot(signal, type="scatter", color="#ff9800", markersize=90, marker="^",
                            ylabel="Buy Signal"),
        # Overlapping divergence segments
        divergence_apd,
    ]

    wickly.plot(
        df,
        type="candle",
        volume=True,
        mav=(10,),
        style="nightclouds",
        title="Bollinger Bands + Buy Signals + Divergence Segments",
        addplot=apds,
    )


if __name__ == "__main__":
    main()

Live / animated chart

Open a live chart and feed it new bars in real time using QTimer:

python examples/live_chart.py
examples/live_chart.py
"""
Example: live / animated candlestick chart with a live addplot overlay.

A new bar is appended every 2 seconds. The most recent bar is updated
with simulated live ticks between bar boundaries.  A rolling SMA overlay
is kept in sync with the price data in real time.

Run:
    python examples/live_chart.py
"""

import sys

import numpy as np
import pandas as pd
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication

import wickly


SMA_PERIOD = 10


def _generate_initial_data(n: int = 60) -> pd.DataFrame:
    """Generate synthetic OHLCV data for the initial chart."""
    rng = np.random.default_rng(42)
    dates = pd.bdate_range("2025-01-02", periods=n, freq="min")
    close = 100.0 + np.cumsum(rng.normal(0, 0.5, n))
    open_ = close + rng.normal(0, 0.2, n)
    high = np.maximum(open_, close) + rng.uniform(0.1, 0.8, n)
    low = np.minimum(open_, close) - rng.uniform(0.1, 0.8, n)
    volume = rng.integers(100_000, 1_000_000, n).astype(float)

    return pd.DataFrame(
        {"Open": open_, "High": high, "Low": low, "Close": close, "Volume": volume},
        index=dates,
    )


def main() -> None:
    # Create the QApplication *before* live_plot so we hold a strong
    # reference throughout the lifetime of the script.
    app = QApplication.instance() or QApplication(sys.argv)

    df = _generate_initial_data()

    # Compute initial SMA overlay
    sma = df["Close"].rolling(SMA_PERIOD).mean()
    ap = wickly.make_addplot(sma, color="#e91e63", width=2, ylabel=f"SMA {SMA_PERIOD}")

    # Open a live chart with the SMA overlay
    widget, axes = wickly.live_plot(
        df, type="candle", volume=True, mav=(20,),
        addplot=ap,
        title="Live Chart with Addplot Overlay", style="yahoo",
    )

    rng = np.random.default_rng(99)
    last_close = df["Close"].iloc[-1]
    last_date = df.index[-1]

    def _current_sma() -> float:
        """Compute the SMA over the last SMA_PERIOD closes."""
        closes = widget._closes
        if len(closes) < SMA_PERIOD:
            return float("nan")
        return float(np.mean(closes[-SMA_PERIOD:]))

    def on_tick():
        """Called every 200ms — simulates a live tick on the current bar."""
        nonlocal last_close
        last_close += rng.normal(0, 0.3)
        widget.update_last(
            close=last_close,
            high=max(widget._highs[-1], last_close),
            low=min(widget._lows[-1], last_close),
        )
        # Keep the SMA overlay in sync with the latest close
        widget.update_addplot_last(0, _current_sma())

    def on_new_bar():
        """Called every 2s — appends a complete new bar."""
        nonlocal last_close, last_date
        last_date += pd.Timedelta(minutes=1)
        open_ = last_close + rng.normal(0, 0.1)
        close = open_ + rng.normal(0, 0.5)
        high = max(open_, close) + rng.uniform(0.05, 0.4)
        low = min(open_, close) - rng.uniform(0.05, 0.4)
        vol = float(rng.integers(100_000, 1_000_000))

        widget.append_data(
            dates=pd.DatetimeIndex([last_date]),
            opens=np.array([open_]),
            highs=np.array([high]),
            lows=np.array([low]),
            closes=np.array([close]),
            volumes=np.array([vol]),
        )
        # Append the new SMA point for the new bar
        widget.append_addplot_data(0, _current_sma())
        last_close = close

    # Tick timer — fast updates within the current candle
    tick_timer = QTimer()
    tick_timer.timeout.connect(on_tick)
    tick_timer.start(200)

    # Bar timer — new candle every 2 seconds
    bar_timer = QTimer()
    bar_timer.timeout.connect(on_new_bar)
    bar_timer.start(2000)

    app.exec()


if __name__ == "__main__":
    main()