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()