Table of Contents
Introduction
What Does “Find Function Values” Really Mean?
Real-World Scenario: The Volatility-Adjusted Momentum Strategy
Methods to Compute Function Values Efficiently
Complete Implementation with Test Cases
Best Practices and Performance Tips
Conclusion
Introduction
In algorithmic trading, success isn’t about predicting the future — it’s about computing the right value at the right time.
Whether it’s a moving average, volatility index, or risk-adjusted return, every decision your bot makes starts with one question:
“What is the function value at this point?”
This article reveals how top traders use Python to efficiently compute function values over price arrays — not with complex math libraries, but with clean, fast, and battle-tested code.
What Does “Find Function Values” Really Mean?
It’s simple:
You have a function — like f(x) = (x - mean) / std
— and you want to apply it to every element in an array of stock prices.
You’re not just calculating numbers.
You’re turning raw data into actionable signals:
Buy when volatility drops below the threshold
Sell when momentum spikes
Hold when risk exceeds tolerance
The key?
Don’t recalculate the same values over and over.
Real-World Scenario: The Volatility-Adjusted Momentum Strategy
Imagine you’re building a bot that trades based on relative price movement, not absolute price.
Your rule
Buy when today’s return is above the 5-day average return AND volatility is below the 10-day average.
You have
prices
: daily closing prices
A function to compute daily returns: return = (p[i] - p[i-1]) / p[i-1]
A function to compute rolling volatility: std of last N returns
You don’t need AI. You don’t need news feeds.
You just need to find the function values — accurately and fast.
Methods to Compute Function Values Efficiently
1. Direct List Comprehension (Simple & Clean)
def compute_returns(prices):
return [(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices))]
def compute_volatility(returns, window=10):
if len(returns) < window:
return [0] * len(returns)
return [sum(returns[i-window+1:i+1]) / window for i in range(window-1, len(returns))]
Perfect for small datasets. Easy to debug.
2. NumPy Vectorization (Fastest for Large Data)
import numpy as np
def compute_returns_numpy(prices):
prices = np.array(prices)
returns = np.diff(prices) / prices[:-1] # Vectorized
return returns
def compute_volatility_numpy(returns, window=10):
if len(returns) < window:
return np.zeros_like(returns)
# Rolling std using convolution
weights = np.ones(window) / window
volatilities = np.convolve(np.abs(returns), weights, mode='valid')
return np.pad(volatilities, (window-1, 0), constant_values=0)
3. Generator for Memory Efficiency
def returns_generator(prices):
for i in range(1, len(prices)):
yield (prices[i] - prices[i-1]) / prices[i-1]
# Use only when you need one value at a time
def find_signal(prices, threshold=0.02):
returns = list(returns_generator(prices))
avg_return = sum(returns[-5:]) / 5 if len(returns) >= 5 else 0
return "BUY" if returns[-1] > avg_return > threshold else "HOLD"
Use when memory is tight (e.g., streaming data).
Complete Implementation with Test Cases
import numpy as np
import unittest
class VolatilityTrader:
def __init__(self, window_return=5, window_volatility=10):
self.window_r = window_return
self.window_v = window_volatility
def compute_returns(self, prices):
"""Compute daily percentage returns."""
if len(prices) < 2:
return []
return [(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices))]
def compute_volatility(self, returns):
"""Compute rolling average absolute return (volatility proxy)."""
if len(returns) < self.window_v:
# Consistent with numpy version: pad initial values with 0.0
return [0.0] * len(returns)
vols = []
for i in range(self.window_v - 1, len(returns)):
window = returns[i - self.window_v + 1:i + 1]
vol = sum(abs(r) for r in window) / self.window_v
vols.append(vol)
# Pad initial values to match the length of returns
return [0.0] * (self.window_v - 1) + vols
def generate_signals(self, prices):
"""Generate BUY/HOLD signals based on return vs. volatility."""
# Minimum price length required to compute signals: max(R, V) + 1
min_len = max(self.window_r, self.window_v) + 1
if len(prices) < min_len:
return ["HOLD"] * len(prices)
returns = self.compute_returns(prices)
volatilities = self.compute_volatility(returns)
# Signals will have the same length as prices
signals = ["HOLD"] * len(prices)
# Start index for signal generation is min_len - 1, which corresponds to the
# first day where both windows (R and V) are fully formed.
# This is where 'prices[i]' is first considered for a signal.
start_index = min_len - 1
for i in range(start_index, len(prices)):
# Average Return is calculated over the *last* window_r returns (ending at return[i-1])
# The returns array has length len(prices)-1. returns[i-1] is the return from prices[i-1] to prices[i]
# The returns window should cover returns for prices[i-window_r] to prices[i-1]
recent_avg_return = sum(returns[i-self.window_r:i]) / self.window_r
# Volatility is calculated over the last window_v absolute returns (ending at return[i-1])
# volatilities array is padded and has length len(prices)-1.
# volatilities[i-1] is the volatility ending on the return calculated up to price[i]
current_volatility = volatilities[i-1] # volatilities[i-1] corresponds to the volatility up to day i
if recent_avg_return > 0.01 and current_volatility < 0.02:
signals[i] = "BUY"
return signals
def generate_signals_fast(self, prices):
"""High-performance NumPy version."""
if len(prices) < 2:
return ["HOLD"] * len(prices)
prices = np.array(prices)
returns = np.diff(prices) / prices[:-1]
abs_returns = np.abs(returns)
# 1. Rolling average return (window_r)
cumsum_r = np.cumsum(np.insert(returns, 0, 0))
# The rolling average over returns[k:k+R] is computed.
# avg_returns[i] is the average of returns[i:i+window_r].
# Length is len(returns) - window_r + 1
avg_returns_raw = (cumsum_r[self.window_r:] - cumsum_r[:-self.window_r]) / self.window_r
# 2. Rolling volatility (window_v)
cumsum_v = np.cumsum(np.insert(abs_returns, 0, 0))
# avg_volatility_raw[i] is the average of abs_returns[i:i+window_v].
# Length is len(abs_returns) - window_v + 1
avg_volatility_raw = (cumsum_v[self.window_v:] - cumsum_v[:-self.window_v]) / self.window_v
# 3. Alignment and Padding (Crucial Step)
# The decision for prices[i] depends on a window of returns ENDING at returns[i-1].
# In the original implementation:
# - The first decision is at prices[max(R, V)].
# - This decision uses the average return of returns[max(R, V)-R : max(R, V)]
# - This decision uses the volatility ending at returns[max(R, V)]
min_len = max(self.window_r, self.window_v)
# We need to pad the raw arrays to match the length of prices (len(prices)).
# Both arrays represent calculations over the returns array (len(prices)-1).
# Padding for Avg Returns:
# The first valid average return corresponds to the window ending at returns[window_r-1].
# This decision is for prices[window_r].
# We need window_r 'HOLD' signals before the first valid return calculation is available.
# This pad is for the start of the signals.
pad_r = self.window_r
padded_avg_returns = np.pad(avg_returns_raw, (pad_r, 0), constant_values=0)
# Padding for Volatility:
# The first valid volatility corresponds to the window ending at returns[window_v-1].
# This decision is for prices[window_v].
pad_v = self.window_v
padded_avg_volatility = np.pad(avg_volatility_raw, (pad_v, 0), constant_values=0)
# The final arrays should have the length of prices.
# We need to consider the overlap: the final signal array is generated up to the
# length of prices.
# Slice to the correct length and align:
# Since the first signal is at index max(R,V), we slice both to that length
# and then apply the logic.
# Use the maximum required padding for alignment
max_pad = max(self.window_r, self.window_v)
# The array indices for the signal: [0, 1, ..., max_pad-1] will be "HOLD"
# The signal for price[i] is determined by:
# - padded_avg_returns[i] (which is the avg return ending at returns[i-1])
# - padded_avg_volatility[i] (which is the avg volatility ending at returns[i-1])
# Create a combined signal logic based on the longer padded array
combined_signal_logic = np.where(
(padded_avg_returns[max_pad:] > 0.01) & (padded_avg_volatility[max_pad:] < 0.02),
"BUY", "HOLD"
)
# Prepend the initial "HOLD" signals
initial_holds = np.array(["HOLD"] * max_pad)
signals = np.concatenate((initial_holds, combined_signal_logic)).tolist()
# Ensure the final signals list has the same length as the input prices
if len(signals) < len(prices):
# This handles cases where len(prices) is just slightly over max(R,V)
signals += ["HOLD"] * (len(prices) - len(signals))
elif len(signals) > len(prices):
# This prevents over-shooting
signals = signals[:len(prices)]
return signals
class TestVolatilityTrader(unittest.TestCase):
def setUp(self):
# R=5, V=10. Signal starts at index 10.
self.trader = VolatilityTrader(window_return=5, window_volatility=10)
# Simulate rising price with low volatility
self.prices = [100, 101, 102.5, 104, 105.5, 107, 108.5, 109, 110, 111, 112] # len=11
# returns len=10
# returns: ~[0.01, 0.0148, 0.0146, 0.0143, 0.0142, 0.0140, 0.0046, 0.0092, 0.0091, 0.0090]
# avg_returns[5:10]:
# Day 10 (index 10):
# Avg Ret (last 5, returns[5:10]): avg([0.0140, 0.0046, 0.0092, 0.0091, 0.0090]) = 0.00918 (HOLD for 10)
# Let's adjust prices to force a BUY signal on day 10
self.prices_buy = [
100, 101, 102.5, 104, 105.5, # Day 4, P=105.5
107.5, # Day 5, Ret=0.0189
109.5, # Day 6, Ret=0.0186
111.5, # Day 7, Ret=0.0182
113.5, # Day 8, Ret=0.0179
115.5, # Day 9, Ret=0.0176
117.5 # Day 10, Ret=0.0173
] # len=11
# returns len=10: [0.01, 0.0148, 0.0146, 0.0143, 0.0189, 0.0186, 0.0182, 0.0179, 0.0176, 0.0173]
# Avg Ret (last 5, returns[5:10]): avg([0.0186, 0.0182, 0.0179, 0.0176, 0.0173]) = 0.0179 > 0.01 (BUY)
# Avg Vol (last 10, abs(returns)[0:10]): avg(abs(all returns)) = 0.0163 < 0.02 (BUY)
def test_returns_computation(self):
returns = self.trader.compute_returns(self.prices)
self.assertAlmostEqual(returns[0], 0.01, places=4)
self.assertAlmostEqual(returns[-1], (112 - 111) / 111, places=4) # 0.0090
self.assertEqual(len(returns), len(self.prices) - 1)
def test_signal_generation(self):
signals = self.trader.generate_signals(self.prices_buy)
# Should trigger BUY on day 10 (index 10)
self.assertEqual(signals[10], "BUY")
self.assertEqual(signals[5], "HOLD") # Too early (max(R,V)=10, so signal starts at index 10)
# Test the day before the first potential signal (index 9)
self.assertEqual(signals[9], "HOLD")
# Test a case with not enough data
signals_short = self.trader.generate_signals([100] * 10)
self.assertEqual(len(signals_short), 10)
self.assertTrue(all(s == "HOLD" for s in signals_short))
def test_fast_version_matches_slow(self):
slow = self.trader.generate_signals(self.prices_buy)
fast = self.trader.generate_signals_fast(self.prices_buy)
self.assertEqual(slow, fast)
# Test with original prices that did not generate a BUY
slow_no_buy = self.trader.generate_signals(self.prices)
fast_no_buy = self.trader.generate_signals_fast(self.prices)
self.assertEqual(slow_no_buy, fast_no_buy)
def test_edge_cases(self):
# Single price
self.assertEqual(self.trader.generate_signals([100]), ["HOLD"])
self.assertEqual(self.trader.generate_signals_fast([100]), ["HOLD"])
# Two prices
self.assertEqual(self.trader.generate_signals([100, 101]), ["HOLD", "HOLD"])
self.assertEqual(self.trader.generate_signals_fast([100, 101]), ["HOLD", "HOLD"])
if __name__ == "__main__":
# Demo
trader = VolatilityTrader()
# Prices that will generate a BUY signal on day 10 (index 10)
prices = [100, 101, 102.5, 104, 105.5, 107.5, 109.5, 111.5, 113.5, 115.5, 117.5]
signals = trader.generate_signals_fast(prices) # Use the fast version for the demo
print(" VOLATILITY-ADJUSTED TRADING BOT")
print(f"Window Return (R): {trader.window_r}, Window Volatility (V): {trader.window_v}")
print(f"{'Day':<4} {'Price':<8} {'Signal':<8}")
print("-" * 25)
for i, (p, s) in enumerate(zip(prices, signals)):
print(f"{i:<4} {p:<8} {s:<8}")
print("\n Running tests...")
unittest.main(argv=[''], exit=False, verbosity=1)
![q]()
Best Practices and Performance Tips
Always validate input length before computing functions — avoid index errors.
Use NumPy for rolling windows — it’s 5–10x faster than pure Python loops.
Cache function results if inputs don’t change (e.g., static lookback windows).
Never modify original arrays — always return new ones.
Test edge cases: empty arrays, single values, inconsistent lengths.
Document what your function computes — “volatility” could mean std, range, or ATR.
Conclusion
Finding function values isn’t math — it’s decision-making in disguise. The best traders don’t guess trends. They compute them — precisely, repeatedly, and efficiently. Whether you’re using list comprehensions for clarity or NumPy for speed, the goal is the same:
Turn raw data into a signal your bot can trust.
Master this, and you’re no longer coding. You’re building self-executing financial logic. Start small. Test rigorously. Let the numbers speak.