Part IV: Quantitative modeling with Fibonacci
From exact computation to trading pipelines in R and Python
Fibonacci isn’t just a set of pretty ratios on a chart—it’s a modeling primitive. In quantitative finance and data science, Fibonacci concepts inform how we compute features, reason about cyclical structure, set dynamic thresholds, and engineer rule-based strategies that are testable, reproducible, and portable across R and Python stacks. This chapter dives far deeper than a naive sequence generator: we’ll cover exact and efficient computation (memoization, matrix exponentiation, fast doubling), numerical stability (floating-point vs. arbitrary precision), vectorization, feature engineering for time series, factor design for retracements and extensions, backtesting, and integration into modern ML pipelines.
By the end, you’ll have ready-to-run code, a design blueprint for robust experimentation, and patterns to productionize Fibonacci-based analytics.
Why Fibonacci is useful in quantitative workflows
Expressive ratios: The core retracement and extension levels provide interpretable thresholds for price action regimes (pullbacks, continuations, exhaustion).
Recursive structure: Recurrence relations give natural handles for building lag-based features and for modeling processes with feedback.
Self-similarity: Multi-timeframe confluence and fractal market hypotheses map well to Fibonacci-derived scaffolding.
Operational simplicity: Even when not “predictive,” Fibonacci levels are a clean way to systematize entries, exits, and risk brackets—ideal for rule-based backtests.
Mathematical foundations for efficient computation
Before we code, let’s frame the computational problem. A naive recursive Fibonacci is exponential-time. In production analytics, that’s unacceptable. We’ll rely on several techniques:
Iterative linear-time: Compute from the bottom up in and memory.
Memoization/dynamic programming: Cache results to avoid recomputing subproblems.
Matrix exponentiation: Use
which yields time via fast exponentiation.
Fast doubling: Compute from in .
Closed form (Binet):
Great for proofs, but numerically unstable for large in floating-point arithmetic.
For financial analytics we rarely need huge , but understanding complexity and stability lets you scale when you’re vectorizing features across millions of bars or running cross-sectional models.
R implementation
Baseline iterative function
fibonacci <- function(n) {
stopifnot(n >= 1)
fib <- numeric(n)
fib[1] <- 0
if (n >= 2) fib[2] <- 1
if (n >= 3) {
for (i in 3:n) {
fib[i] <- fib[i-1] + fib[i-2]
}
}
fib
}
# Example
fibonacci(15)
Complexity: time, space (sequence returned).
Use cases: Quick feature generation for moderate .
Memoized version (for repeated queries)
fib_memo <- local({
cache <- c(0, 1)
function(n) {
stopifnot(n >= 0)
if (n < length(cache)) return(cache[n + 1])
for (i in length(cache):n) {
cache <<- c(cache, cache[i] + cache[i - 1])
}
cache[n + 1]
}
})
# Example
sapply(0:10, fib_memo)
Label: When to use: Repeated scalar queries scattered throughout code (e.g., parameter sweeps).
Fast doubling in R (log-time)
fib_fast_doubling <- function(n) {
stopifnot(n >= 0)
fd <- function(k) {
if (k == 0) return(c(0, 1))
ab <- fd(k %/% 2)
a <- ab[1] # F(k)
b <- ab[2] # F(k+1)
c <- a * (2*b - a) # F(2k)
d <- a*a + b*b # F(2k+1)
if (k %% 2 == 0) return(c(c, d))
c(d, c + d)
}
fd(n)[1]
}
# Example
sapply(0:10, fib_fast_doubling)
Label: Scaling: Use when computing large indices or in tight loops.
Arbitrary precision in R
Base R’s integers can overflow. For very large , use Rmpfr (arbitrary precision):
# install.packages("Rmpfr")
library(Rmpfr)
fib_binet_mpfr <- function(n, precBits = 256) {
n <- mpfr(n, precBits)
sqrt5 <- sqrt(mpfr(5, precBits))
phi <- (1 + sqrt5) / 2
psi <- (1 - sqrt5) / 2
round((phi^n - psi^n) / sqrt5)
}
fib_binet_mpfr(100) # Large n safely
Label: Stability: Binet + mpfr is accurate but slower; fast doubling with big integers is often preferable when available.
Python implementation
Baseline iterative version
def fibonacci(n):
assert n >= 1
fib = [0, 1]
for i in range(2, n):
fib.append(fib[i-1] + fib[i-2])
return fib
# Example
fibonacci(15)
Label: Simplicity: Great for feature arrays in NumPy/Pandas workflows.
Fast doubling (log-time) in Python
def fib_fast_doubling(n):
# Returns F(n)
def _fd(k):
if k == 0:
return (0, 1)
a, b = _fd(k >> 1)
c = a * ((b << 1) - a)
d = a*a + b*b
if k & 1 == 0:
return (c, d)
else:
return (d, c + d)
return _fd(n)[0]
[fib_fast_doubling(i) for i in range(11)]
Label: Performance: Ideal for high-index computations and is numerically exact with Python’s big ints.
Binet with Decimal for controlled precision
from decimal import Decimal, getcontext
def fib_binet(n, prec=100):
getcontext().prec = prec
sqrt5 = Decimal(5).sqrt()
phi = (Decimal(1) + sqrt5) / Decimal(2)
psi = (Decimal(1) - sqrt5) / Decimal(2)
return int(((phi**n) - (psi**n)) / sqrt5 + Decimal('0.5'))
[fib_binet(i) for i in range(11)]
Label: Note: Still slower and sensitive to precision settings; prefer fast doubling for production.
Engineering Fibonacci features for time series
Fibonacci logic can enrich your time series features in ways that are interpretable and robust.
Core retracement levels from a swing
Given swing high and swing low with range , retracements for an up-move are:
, , , ,
For a down-move, mirror the logic from .
Feature ideas
Distance-to-level features: For each bar, compute signed distance to nearest Fibonacci level.
Level touches and bounces: Binary or count features for touches followed by intrabar rejection.
Confluence indicators: Overlaps of levels from multiple recent swings or multiple timeframes.
Fibonacci time pivots: Mark bars at Fibonacci spacings from a key event : .
Regime features: Flags for being between certain bands (e.g., between 0.5 and 0.618 retracements).
R: Building Fibonacci retracement features from OHLC
library(data.table)
fibo_levels_dt <- function(dt, swing_window = 50) {
# dt: data.table with columns: time, open, high, low, close
setorder(dt, time)
dt[, swing_high := frollapply(high, swing_window, max, align = "right")]
dt[, swing_low := frollapply(low , swing_window, min, align = "right")]
dt[, range := swing_high - swing_low]
ratios <- c(0.236, 0.382, 0.5, 0.618, 0.786)
for (r in ratios) {
lvlname <- paste0("fib_", gsub("\\.", "", as.character(r)))
dt[, (lvlname) := swing_high - r * range]
}
# Distances to the nearest Fibonacci level
lvlcols <- paste0("fib_", c("0236","0382","05","0618","0786"))
dt[, dist_to_nearest := do.call(pmin, lapply(.SD, function(x) abs(close - x))), .SDcols = lvlcols]
# Signed distance to golden level
dt[, dist_to_0618 := close - fib_0618]
dt[]
}
# Example usage
# dt <- data.table(time=..., open=..., high=..., low=..., close=...)
# features <- fibo_levels_dt(dt, swing_window = 50)
Label: Interpretation: Smaller
dist_to_nearestsuggests we are at/near a reaction zone; sign ondist_to_0618indicates position relative to the “golden” level.
Python: Retracements and extensions with Pandas
import pandas as pd
import numpy as np
def fibonacci_features(df, swing_window=50):
# df: DataFrame with columns ['open','high','low','close']; index as datetime
rolling_high = df['high'].rolling(swing_window).max()
rolling_low = df['low'].rolling(swing_window).min()
rng = rolling_high - rolling_low
ratios = [0.236, 0.382, 0.5, 0.618, 0.786]
for r in ratios:
col = f'fib_{str(r).replace("0.", "")}'
df[col] = rolling_high - r * rng
# Distance to nearest level
lvlcols = [f'fib_{k}' for k in ["0236","0382","05","0618","0786"]]
df['dist_to_nearest'] = np.min([np.abs(df['close'] - df[c]) for c in lvlcols], axis=0)
# Signed distance to 0.618
df['dist_to_0618'] = df['close'] - df['fib_0618']
# Simple extension targets from latest swing
df['ext_1618'] = rolling_high + 1.618 * rng
df['ext_2618'] = rolling_high + 2.618 * rng
df['ext_4236'] = rolling_high + 4.236 * rng
return df
# Example
# df = pd.read_csv('ohlc.csv', parse_dates=['time']).set_index('time')
# df = fibonacci_features(df, 50)
Label: Extensions: Useful as dynamic profit target zones. Consider combining with volatility bands for adaptive exits.
Designing rule-based strategies with Fibonacci
Here’s a minimal, testable pattern that you can evolve:
Setup condition: Uptrend confirmed by higher highs/lows or a moving average slope.
Entry: Buy when price pulls back into [0.5, 0.618] retracement band and prints a rejection candle (e.g., hammer) or momentum divergence.
Stop: Below the 0.786 level or below the swing low.
Targets: Scale out at 1.000 (prior high), 1.618, 2.618 extensions; trail remaining with ATR or structure-based stops.
Why this works operationally
Liquidity clustering: Many orders sit around these levels, increasing the odds of reaction.
Behavioral symmetry: “Half-back” and “golden” pullbacks attract re-entries from sidelined participants.
Risk clarity: Natural invalidation points (e.g., below 0.786) simplify position sizing.
Backtesting Fibonacci strategies
To validate, you need vectorized signals, non-peeking logic, and robust metrics.
Python example: vectorized signals
import numpy as np
import pandas as pd
def fib_strategy_signals(df, swing_window=50):
df = fibonacci_features(df.copy(), swing_window)
# Trend filter: 50 > 200 SMA
df['sma50'] = df['close'].rolling(50).mean()
df['sma200'] = df['close'].rolling(200).mean()
df['uptrend'] = df['sma50'] > df['sma200']
# Entry zone: between 0.5 and 0.618; rejection approximated by close > open after touching band
in_band = (df['low'] <= df['fib_0618']) & (df['high'] >= df['fib_05'])
bullish_bar = df['close'] > df['open']
df['long_signal'] = df['uptrend'] & in_band & bullish_bar
return df
def simulate_long_only(df, risk_per_trade=0.01, initial_capital=100000.0):
df = df.copy()
capital = initial_capital
position = 0
entry_price = np.nan
stop = np.nan
eq_curve = []
for i in range(len(df)):
price_o, price_h, price_l, price_c = df['open'].iloc[i], df['high'].iloc[i], df['low'].iloc[i], df['close'].iloc[i]
fib0618 = df['fib_0618'].iloc[i] if not np.isnan(df['fib_0618'].iloc[i]) else None
fib0786 = df['fib_0786'].iloc[i] if not np.isnan(df['fib_0786'].iloc[i]) else None
ext1618 = df['ext_1618'].iloc[i] if not np.isnan(df['ext_1618'].iloc[i]) else None
# Entry
if position == 0 and bool(df['long_signal'].iloc[i]) and fib0618 and fib0786:
# Risk = entry - stop
entry_price = price_c
stop = min(fib0786, price_l) # conservative
risk_per_share = max(entry_price - stop, 1e-6)
shares = int((risk_per_trade * capital) / risk_per_share)
if shares > 0:
position = shares
# Exit logic
if position > 0:
# Stop-loss
if price_l <= stop:
capital += position * (stop - entry_price)
position = 0
entry_price = np.nan
stop = np.nan
# Target (partial or full); here use full exit at 1.618
elif ext1618 and price_h >= ext1618:
capital += position * (ext1618 - entry_price)
position = 0
entry_price = np.nan
stop = np.nan
# Mark to market
if position > 0:
pnl = position * (price_c - entry_price)
eq_curve.append(capital + pnl)
else:
eq_curve.append(capital)
df['equity'] = eq_curve
return df
Labels:
Risk model: 1% of capital at risk per trade with level-based stops.
Targets: Exit at 1.618 extension (simple baseline; consider scaling).
Improvements: Add slippage/fees, ATR-based trailing, multi-timeframe confluence.
Evaluation metrics
Return and drawdown: CAGR, max drawdown, Calmar.
Stability: Sharpe, Sortino, rolling Sharpe.
Hit ratio and expectation: Probability of win, average win/loss, expectancy per trade.
Turnover and capacity: Trades per period, average holding time.
Integrating TA-Lib, Pandas, and scikit-learn
TA-Lib for context indicators
Momentum: RSI, MACD to confirm reactions at Fibonacci levels.
Trend: ADX, DMI, moving averages for trend filters.
Volatility: ATR to scale stops/targets relative to noise.
Pseudocode pattern:
import talib as ta
df['rsi'] = ta.RSI(df['close'].values, timeperiod=14)
df['atr'] = ta.ATR(df['high'].values, df['low'].values, df['close'].values, timeperiod=14)
# Confirmation: long when pullback to 0.618 and RSI rising from <30 to >30
df['rsi_turn_up'] = (df['rsi'].shift(1) < 30) & (df['rsi'] >= 30)
df['confirmed'] = df['long_signal'] & df['rsi_turn_up']
Label: Synergy: Indicators do not replace levels; they contextualize them.
Custom scikit-learn transformers for Fibonacci features
Turn Fibonacci logic into reusable components that slot into pipelines.
from sklearn.base import BaseEstimator, TransformerMixin
import numpy as np
import pandas as pd
class FibonacciTransformer(BaseEstimator, TransformerMixin):
def __init__(self, swing_window=50, include_extensions=True):
self.swing_window = swing_window
self.include_extensions = include_extensions
self.columns_ = None
def fit(self, X, y=None):
self.columns_ = list(X.columns)
return self
def transform(self, X):
df = X.copy()
df = fibonacci_features(df, self.swing_window)
feats = ['dist_to_nearest', 'dist_to_0618']
if self.include_extensions:
feats += ['ext_1618', 'ext_2618', 'ext_4236']
return df[feats].values
Label: Leakage control: Fit your scaler/selector on the training split only; generate Fibonacci features with rolling windows that do not peek into the future.
Pipeline example with cross-validation
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
pipe = Pipeline(steps=[
('fib', FibonacciTransformer(swing_window=50)),
('scaler', StandardScaler()),
('clf', LogisticRegression(max_iter=1000))
])
# y: binary label for “bounce after touch of 0.618 within 5 bars”
# X_raw: DataFrame with OHLC and maybe baseline indicators
tscv = TimeSeriesSplit(n_splits=5)
scores = cross_val_score(pipe, X_raw, y, cv=tscv, scoring='roc_auc')
scores.mean(), scores.std()
Label: Target engineering: Define labels consistent with the trading logic you actually intend to deploy (e.g., bounce window, min return threshold, stop logic).
Advanced modeling concepts with Fibonacci
Multi-timeframe confluence
Definition: Require confirmation when a lower-timeframe retracement aligns within a tolerance band with a higher-timeframe level.
Implementation: Compute features on, e.g., 1H and 4H aggregates; trigger signals only if |LTF_level − HTF_level| ≤ k·ATR.
Probabilistic zones from historical reactions
Idea: Instead of treating 0.618 as a line, model a band with empirical hit probabilities based on prior touches.
Method: Maintain rolling statistics for post-touch outcomes (e.g., 1-day forward return distributions) and condition entries on favorable skew.
Volatility-aware bands
Approach: Expand/narrow the zone around each level by a volatility measure (ATR or realized vol), adapting sensitivity to market regime.
Regime detection
Approach: Use HMMs or clustering on volatility/trend features to segment regimes (trend, chop, high vol). Enable Fibonacci strategies only in regimes where they historically show edge.
Position sizing with level geometry
Tiered sizing: Increase size when confluence increases (e.g., 0.618 + higher-timeframe 0.382 + daily VWAP band).
Risk brackets: Map stops to 0.786, targets to 1.618/2.618, and size by fixed-fraction or ATR-normalized risk.
Numerical stability, complexity, and performance
Closed form caution: Binet’s formula accumulates floating-point error for large . Prefer fast doubling for exactness with big integers.
Vectorization: In Pandas/R, broadcast distance-to-level computations across columns and avoid Python loops.
Rolling windows: Use efficient rolling ops (R: frollapply; Python: numba-accelerated operations when necessary).
Backtest integrity: Avoid look-ahead bias; compute levels using only past bars at each timestamp.
Reproducible research and productionization
Configuration-first: Externalize parameters (swing windows, tolerance bands, regime filters) into YAML/TOML.
Experiment tracking: Log metrics, parameters, and artifacts (MLflow, Weights & Biases).
Data versioning: Pin data snapshots for identical re-runs (DVC/LakeFS).
Monitoring: In production, watch drift in the distribution of “distance-to-level” and reaction outcomes; alert when edge decays.
Putting it together: an end-to-end example in Python
The following sketch ties features, signals, and evaluation coherently:
import pandas as pd
import numpy as np
def prepare(df, swing_window=50):
df = fibonacci_features(df.copy(), swing_window)
df['atr'] = df['high'].rolling(14).max() - df['low'].rolling(14).min() # simple proxy
df['sma50'] = df['close'].rolling(50).mean()
df['sma200'] = df['close'].rolling(200).mean()
df['uptrend'] = df['sma50'] > df['sma200']
return df
def signals(df, tol=0.2):
# tol multiplier on ATR to make a band around 0.618
band_low = df['fib_0618'] - tol * df['atr']
band_high = df['fib_0618'] + tol * df['atr']
touch_band = (df['low'] <= band_high) & (df['high'] >= band_low)
reversal_bar = df['close'] > df['open']
df['enter_long'] = df['uptrend'] & touch_band & reversal_bar
return df
def backtest(df, risk_frac=0.01):
capital = 100000.0
pos = 0
entry = stop = np.nan
eq = []
for i, row in df.iterrows():
o, h, l, c = row['open'], row['high'], row['low'], row['close']
fib0786, ext1618 = row['fib_0786'], row['ext_1618']
if pos == 0 and row.get('enter_long', False) and not np.isnan(fib0786):
entry = c
stop = min(fib0786, l)
risk = max(entry - stop, 1e-6)
size = int((risk_frac * capital) / risk)
if size > 0:
pos = size
if pos > 0:
# stop-loss
if l <= stop:
capital += pos * (stop - entry)
pos = 0
entry = stop = np.nan
# take-profit
elif not np.isnan(ext1618) and h >= ext1618:
capital += pos * (ext1618 - entry)
pos = 0
entry = stop = np.nan
eq.append(capital if pos == 0 else capital + pos * (c - entry))
df['equity'] = eq
return df
# Usage
# df = load_ohlc(...) # DataFrame with open, high, low, close
# df = prepare(df)
# df = signals(df)
# df = backtest(df)
# performance = df['equity'].iloc[-1] / df['equity'].iloc[0] - 1
Extensions: Introduce multi-timeframe confluence, partial profit-taking, ATR trailing stops, and walk-forward validation.
R end-to-end sketch
library(data.table)
library(TTR)
prepare_r <- function(dt, swing_window = 50) {
dt <- copy(dt)[order(time)]
dt <- fibo_levels_dt(dt, swing_window)
dt[, atr := ATR(HLC = cbind(high, low, close), n = 14)[, "atr"]]
dt[, sma50 := SMA(close, n = 50)]
dt[, sma200 := SMA(close, n = 200)]
dt[, uptrend := sma50 > sma200]
dt
}
signals_r <- function(dt, tol = 0.2) {
dt[, band_low := fib_0618 - tol * atr]
dt[, band_high := fib_0618 + tol * atr]
dt[, touch_band := (low <= band_high) & (high >= band_low)]
dt[, reversal_bar := close > open]
dt[, enter_long := uptrend & touch_band & reversal_bar]
dt
}
backtest_r <- function(dt, risk_frac = 0.01, initial_capital = 1e5) {
capital <- initial_capital
pos <- 0L
entry <- NA_real_
stop <- NA_real_
eq <- numeric(nrow(dt))
for (i in seq_len(nrow(dt))) {
o <- dt$open[i]; h <- dt$high[i]; l <- dt$low[i]; c <- dt$close[i]
fib0786 <- dt$fib_0786[i]; ext1618 <- dt$ext_1618[i]
if (pos == 0L && isTRUE(dt$enter_long[i]) && !is.na(fib0786)) {
entry <- c
stop <- min(fib0786, l)
risk <- max(entry - stop, 1e-6)
size <- as.integer((risk_frac * capital) / risk)
if (size > 0) pos <- size
}
if (pos > 0L) {
if (l <= stop) {
capital <- capital + pos * (stop - entry)
pos <- 0L; entry <- NA_real_; stop <- NA_real_
} else if (!is.na(ext1618) && h >= ext1618) {
capital <- capital + pos * (ext1618 - entry)
pos <- 0L; entry <- NA_real_; stop <- NA_real_
}
}
eq[i] <- if (pos > 0L) capital + pos * (c - entry) else capital
}
dt[, equity := eq]
dt[]
}
Label: Production hint: Wrap parameters in a config file and log every run (params, seed, metrics) for reproducibility.
Practical cautions and best practices
No silver bullets: Fibonacci levels are powerful structure—but not a guarantee. Treat them as priors that need confirmation.
Data quality: Wicks and gaps affect level interactions. Clean data and consider session filters for intraday.
Transaction costs: Always include slippage/fees in backtests; Fibonacci strategies can be active around levels.
Robustness checks: Vary swing windows, tolerance bands, and timeframes. Seek stability across neighbors, not peak performance at a single point.
Out-of-sample validation: Use rolling/expanding windows and walk-forward to avoid hindsight bias.
Risk-first design: Stops should be mechanical; sizing tied to defined risk per trade and portfolio-level limits.
Where to take this next
Harmonic scanners: Generalize from plain retracements to patterns (Gartley, Bat, Butterfly) with template-matching across windows.
Bayesian level confidence: Maintain posterior probabilities for reaction strength given recent hit/miss history.
Reinforcement learning overlays: Use RL to manage exits around extension ladders while entries remain rule-based at retracements.
Cross-asset confluence: Combine equity pullbacks with FX or rates levels to catch macro-synchronized moves.
Conclusion
Fibonacci offers a complete stack for quantitative work: mathematically grounded computation, interpretable features, strategy scaffolding, and straightforward integration into R/Python ecosystems. Whether you’re hand-crafting a swing model or embedding level geometry into a machine learning pipeline, the techniques above let you move from charts to code—and from code to credible, testable edge.

Join the conversation