Python  

How to Find Function Values in Python

Table of Contents

  1. Introduction

  2. What Does “Find Function Values” Really Mean?

  3. Real-World Scenario: The Volatility-Adjusted Momentum Strategy

  4. Methods to Compute Function Values Efficiently

  5. Complete Implementation with Test Cases

  6. Best Practices and Performance Tips

  7. 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.