finance-data-mcp / tests /test_technical_analysis.py
dlrklc's picture
Initial commit: Gradio MCP app for real-time financial data
7169bc5
"""
Test cases for technical analysis module.
Tests edge cases for technical indicators.
"""
import unittest
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from src.technical_analysis import (
calculate_sma,
calculate_ema,
calculate_rsi,
calculate_macd,
calculate_bollinger_bands
)
class TestTechnicalAnalysis(unittest.TestCase):
"""Test cases for technical analysis functions."""
def setUp(self):
"""Set up test data."""
# Create sample price data
dates = pd.date_range(start='2024-01-01', periods=100, freq='D')
# Generate realistic price data with trend
base_price = 100
trend = np.linspace(0, 20, 100)
noise = np.random.normal(0, 2, 100)
prices = base_price + trend + noise
self.prices = pd.Series(prices, index=dates)
# ========================================================================
# SMA TESTS
# ========================================================================
def test_calculate_sma_normal(self):
"""Test SMA calculation with normal data."""
sma = calculate_sma(self.prices, 20)
self.assertEqual(len(sma), len(self.prices))
self.assertFalse(sma.iloc[:19].notna().any()) # First 19 should be NaN
self.assertTrue(sma.iloc[19:].notna().all()) # Rest should have values
def test_calculate_sma_short_data(self):
"""Test SMA with insufficient data."""
short_prices = self.prices[:10]
sma = calculate_sma(short_prices, 20)
self.assertTrue(sma.isna().all()) # All should be NaN
def test_calculate_sma_single_value(self):
"""Test SMA with single price point."""
single_price = pd.Series([100])
sma = calculate_sma(single_price, 1)
self.assertEqual(sma.iloc[0], 100)
def test_calculate_sma_empty(self):
"""Test SMA with empty series."""
empty_prices = pd.Series([], dtype=float)
sma = calculate_sma(empty_prices, 20)
self.assertEqual(len(sma), 0)
# ========================================================================
# EMA TESTS
# ========================================================================
def test_calculate_ema_normal(self):
"""Test EMA calculation with normal data."""
ema = calculate_ema(self.prices, 12)
self.assertEqual(len(ema), len(self.prices))
self.assertTrue(ema.notna().any()) # Should have some values
def test_calculate_ema_empty(self):
"""Test EMA with empty series."""
empty_prices = pd.Series([], dtype=float)
ema = calculate_ema(empty_prices, 12)
self.assertEqual(len(ema), 0)
# ========================================================================
# RSI TESTS
# ========================================================================
def test_calculate_rsi_normal(self):
"""Test RSI calculation with normal data."""
rsi = calculate_rsi(self.prices, 14)
self.assertIsInstance(rsi, float)
self.assertGreaterEqual(rsi, 0)
self.assertLessEqual(rsi, 100)
def test_calculate_rsi_insufficient_data(self):
"""Test RSI with insufficient data."""
short_prices = self.prices[:10]
rsi = calculate_rsi(short_prices, 14)
self.assertEqual(rsi, 50.0) # Should return neutral
def test_calculate_rsi_all_gains(self):
"""Test RSI with all positive changes (should be high)."""
increasing_prices = pd.Series([100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115])
rsi = calculate_rsi(increasing_prices, 14)
self.assertGreater(rsi, 50) # Should be above neutral
def test_calculate_rsi_all_losses(self):
"""Test RSI with all negative changes (should be low)."""
decreasing_prices = pd.Series([115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100])
rsi = calculate_rsi(decreasing_prices, 14)
self.assertLess(rsi, 50) # Should be below neutral
def test_calculate_rsi_empty(self):
"""Test RSI with empty series."""
empty_prices = pd.Series([], dtype=float)
rsi = calculate_rsi(empty_prices, 14)
self.assertEqual(rsi, 50.0)
# ========================================================================
# MACD TESTS
# ========================================================================
def test_calculate_macd_normal(self):
"""Test MACD calculation with normal data."""
macd = calculate_macd(self.prices)
self.assertIn("macd", macd)
self.assertIn("signal", macd)
self.assertIn("histogram", macd)
self.assertIn("trend", macd)
self.assertIn(macd["trend"], ["bullish", "bearish"])
def test_calculate_macd_insufficient_data(self):
"""Test MACD with insufficient data."""
short_prices = self.prices[:20] # Need at least 26+9=35 for full MACD
macd = calculate_macd(short_prices)
self.assertEqual(macd["trend"], "neutral")
self.assertEqual(macd["macd"], 0.0)
def test_calculate_macd_empty(self):
"""Test MACD with empty series."""
empty_prices = pd.Series([], dtype=float)
macd = calculate_macd(empty_prices)
self.assertEqual(macd["trend"], "neutral")
# ========================================================================
# BOLLINGER BANDS TESTS
# ========================================================================
def test_calculate_bollinger_bands_normal(self):
"""Test Bollinger Bands calculation with normal data."""
bands = calculate_bollinger_bands(self.prices, 20)
self.assertIn("upper", bands)
self.assertIn("middle", bands)
self.assertIn("lower", bands)
self.assertIn("current_price", bands)
self.assertIn("position", bands)
self.assertGreater(bands["upper"], bands["middle"])
self.assertLess(bands["lower"], bands["middle"])
def test_calculate_bollinger_bands_insufficient_data(self):
"""Test Bollinger Bands with insufficient data."""
short_prices = self.prices[:10]
bands = calculate_bollinger_bands(short_prices, 20)
self.assertEqual(bands["upper"], bands["middle"])
self.assertEqual(bands["lower"], bands["middle"])
self.assertEqual(bands["position"], "neutral")
def test_calculate_bollinger_bands_empty(self):
"""Test Bollinger Bands with empty series."""
empty_prices = pd.Series([], dtype=float)
bands = calculate_bollinger_bands(empty_prices, 20)
self.assertEqual(bands["position"], "neutral")
self.assertEqual(bands["current_price"], 0)
def test_calculate_bollinger_bands_overbought(self):
"""Test Bollinger Bands when price is above upper band."""
# Create prices that trend upward significantly
high_prices = pd.Series([100 + i*2 for i in range(30)])
bands = calculate_bollinger_bands(high_prices, 20)
# Price should be above upper band (overbought)
if bands["current_price"] > bands["upper"]:
self.assertEqual(bands["position"], "overbought")
def test_calculate_bollinger_bands_oversold(self):
"""Test Bollinger Bands when price is below lower band."""
# Create prices that trend downward significantly
low_prices = pd.Series([100 - i*2 for i in range(30)])
bands = calculate_bollinger_bands(low_prices, 20)
# Price should be below lower band (oversold)
if bands["current_price"] < bands["lower"]:
self.assertEqual(bands["position"], "oversold")
if __name__ == '__main__':
unittest.main()