diff --git a/.gitignore b/.gitignore index b86dc8e..4ada5eb 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ env/ .vscode/ *.swp *.swo +PAPER_OUTLINE.md # Testing .pytest_cache/ diff --git a/examples/examples-python/09_advanced_models_agent.py b/examples/examples-python/09_advanced_models_agent.py new file mode 100644 index 0000000..7bbe72d --- /dev/null +++ b/examples/examples-python/09_advanced_models_agent.py @@ -0,0 +1,63 @@ +import numpy as np +import pandas as pd +import yfinance as yf +import matplotlib.pyplot as plt +from datetime import datetime, timedelta + +from finlearner.models import TimeSeriesPredictor, TFTPredictor, NBeatsPredictor, GPUConstraintError +from finlearner.agent import Agent + +def create_sample_data(days=500): + """Create a dummy stock price dataframe.""" + dates = pd.date_range(end=datetime.now(), periods=days) + # Generate random walk + prices = [100] + for _ in range(days-1): + change = np.random.normal(0, 1) + prices.append(prices[-1] + change) + + df = pd.DataFrame(data={'Close': prices}, index=dates) + return df + +def run_demo(): + print("=== FinLearner Agent & Advanced Models Demo ===\n") + + # 1. Load Data + print("1. Generating Sample Data...") + df = create_sample_data() + print(f" Data Shape: {df.shape}") + + # 2. Try Loading Advanced Models (Expect Failure on <32GB GPU) + print("\n2. Attempting to load TFT Model (Requires >32GB VRAM)...") + try: + tft_model = TFTPredictor(lookback_days=60) + tft_model.fit(df) # This triggers the check + print(" SUCCESS: TFT Model loaded and trained.") + active_model = tft_model + except GPUConstraintError as e: + print(f" FAILED (Expected): {e}") + print(" -> Fallback to Standard LSTM Model.") + active_model = TimeSeriesPredictor(lookback_days=60) + active_model.fit(df, epochs=5) # Train briefly + + # 3. Initialize Agent + print("\n3. Initializing Trading Agent...") + agent = Agent(model=active_model, initial_balance=10000, strategy='threshold', threshold=0.01) + + # 4. Run Simulation + print(f"4. Running Simulation with {active_model.__class__.__name__}...") + history = agent.simulate(df) + + if history: + print(f"\nSimulation Steps Executed: {len(history)}") + trades = [h for h in history if h.action != 'HOLD'] + print(f"Total Trades: {len(trades)}") + + # Simple plot + final_val = history[-1].portfolio_value + print(f"Final Value: ${final_val:.2f}") + else: + print("No history generated.") + +if __name__ == "__main__": + run_demo() diff --git a/examples/examples-python/10_comprehensive_backtest.py b/examples/examples-python/10_comprehensive_backtest.py new file mode 100644 index 0000000..f6987aa --- /dev/null +++ b/examples/examples-python/10_comprehensive_backtest.py @@ -0,0 +1,231 @@ +""" +Comprehensive Backtest Demo with Real Market Data + +This example demonstrates: +1. Loading REAL stock data from Yahoo Finance (TCS.NS) +2. Testing multiple deep learning models: + - LSTM (TimeSeriesPredictor) + - GRU (GRUPredictor) + - CNN-LSTM Hybrid (CNNLSTMPredictor) + - Transformer with Attention (TransformerPredictor) + - Ensemble Model (EnsemblePredictor) +3. Custom strategy backtesting with the BacktestEngine +4. Model comparison and performance metrics +""" + +import numpy as np +import pandas as pd +import warnings +warnings.filterwarnings('ignore') + +from finlearner import ( + BacktestEngine, + DataLoader, + TimeSeriesPredictor, + GRUPredictor, + CNNLSTMPredictor, + TransformerPredictor, + EnsemblePredictor +) + + +def load_real_data(ticker: str = 'TCS.NS', start: str = '2022-01-01', end: str = '2024-01-01'): + """ + Load real market data from Yahoo Finance. + """ + print(f"๐Ÿ“Š Loading real data for {ticker}...") + try: + df = DataLoader.download_data(ticker, start=start, end=end) + print(f"โœ… Loaded {len(df)} trading days of data") + print(f" Date Range: {df.index[0].strftime('%Y-%m-%d')} to {df.index[-1].strftime('%Y-%m-%d')}") + print(f" Price Range: โ‚น{df['Close'].min():.2f} - โ‚น{df['Close'].max():.2f}") + return df + except Exception as e: + print(f"โŒ Error loading data: {e}") + return None + + +def run_custom_strategy_backtest(df: pd.DataFrame): + """ + SCENARIO A: Backtest a custom Python strategy function. + """ + print("\n" + "="*70) + print("๐Ÿ“ˆ SCENARIO A: Custom Strategy Backtest (Golden Cross)") + print("="*70) + + def golden_cross_strategy(data: pd.DataFrame) -> str: + """ + Simple Moving Average Crossover Strategy. + Buy when SMA_20 > SMA_50 + Sell when SMA_20 < SMA_50 + """ + if len(data) < 50: + return 'HOLD' + + sma_20 = data['Close'].rolling(window=20).mean().iloc[-1] + sma_50 = data['Close'].rolling(window=50).mean().iloc[-1] + + if sma_20 > sma_50: + return 'BUY' + elif sma_20 < sma_50: + return 'SELL' + return 'HOLD' + + engine = BacktestEngine(initial_capital=100000, commission_rate=0.001) + engine.add_strategy(golden_cross_strategy, lookback_days=50) + + result = engine.run(df) + + print(f"\n๐Ÿ“Š Golden Cross Strategy Results:") + print(f" Initial Capital: โ‚น100,000") + print(f" Final Capital: โ‚น{result.equity_curve.iloc[-1]:,.2f}") + print(f" Total Return: {result.total_return*100:.2f}%") + print(f" Sharpe Ratio: {result.sharpe_ratio:.2f}") + print(f" Max Drawdown: {result.max_drawdown*100:.2f}%") + print(f" Trades Executed: {result.trades}") + + return result + + +def train_and_backtest_model(model, model_name: str, df: pd.DataFrame, epochs: int = 3): + """ + Train a model and run backtest. + """ + print(f"\n{'='*70}") + print(f"๐Ÿค– Training {model_name}...") + print(f"{'='*70}") + + try: + # Train the model + model.fit(df, epochs=epochs, batch_size=32) + + # Run backtest + engine = BacktestEngine(initial_capital=100000, commission_rate=0.001) + engine.add_strategy(model) + result = engine.run(df) + + if result: + print(f"\n๐Ÿ“Š {model_name} Backtest Results:") + print(f" Initial Capital: โ‚น100,000") + print(f" Final Capital: โ‚น{result.equity_curve.iloc[-1]:,.2f}") + print(f" Total Return: {result.total_return*100:.2f}%") + print(f" Sharpe Ratio: {result.sharpe_ratio:.2f}") + print(f" Max Drawdown: {result.max_drawdown*100:.2f}%") + print(f" Trades Executed: {result.trades}") + return result + else: + print(f"โŒ Backtest failed for {model_name}") + return None + + except Exception as e: + print(f"โŒ Error with {model_name}: {e}") + return None + + +def run_all_models_comparison(df: pd.DataFrame): + """ + Run all deep learning models and compare their performance. + """ + print("\n" + "="*70) + print("๐Ÿ† MULTI-MODEL COMPARISON") + print("="*70) + print(""" +Testing the following models on TCS.NS: +1. LSTM (TimeSeriesPredictor) +2. GRU (GRUPredictor) +3. CNN-LSTM Hybrid (CNNLSTMPredictor) +4. Transformer with Self-Attention (TransformerPredictor) +""") + + lookback = 60 + epochs = 3 # Reduced for demo speed + + results = {} + + # 1. LSTM Model + lstm = TimeSeriesPredictor(lookback_days=lookback) + lstm_result = train_and_backtest_model(lstm, "LSTM", df, epochs) + if lstm_result: + results['LSTM'] = lstm_result + + # 2. GRU Model + gru = GRUPredictor(lookback_days=lookback) + gru_result = train_and_backtest_model(gru, "GRU", df, epochs) + if gru_result: + results['GRU'] = gru_result + + # 3. CNN-LSTM Hybrid + cnn_lstm = CNNLSTMPredictor(lookback_days=lookback, filters=64, kernel_size=3) + cnn_lstm_result = train_and_backtest_model(cnn_lstm, "CNN-LSTM Hybrid", df, epochs) + if cnn_lstm_result: + results['CNN-LSTM'] = cnn_lstm_result + + # 4. Transformer (Attention Model) + transformer = TransformerPredictor(lookback_days=lookback, d_model=64, num_heads=4, num_blocks=2) + transformer_result = train_and_backtest_model(transformer, "Transformer (Attention)", df, epochs) + if transformer_result: + results['Transformer'] = transformer_result + + return results + + +def print_comparison_table(results: dict, custom_result): + """ + Print a comparison table of all strategies. + """ + print("\n" + "="*70) + print("๐Ÿ“Š FINAL COMPARISON TABLE") + print("="*70) + + print(f"\n{'Strategy':<25} {'Return %':<12} {'Sharpe':<10} {'Max DD %':<12} {'Trades':<10}") + print("-" * 70) + + # Custom strategy + print(f"{'Golden Cross':<25} {custom_result.total_return*100:>8.2f}% {custom_result.sharpe_ratio:>8.2f} {custom_result.max_drawdown*100:>8.2f}% {custom_result.trades:>6}") + + # ML Models + for name, result in results.items(): + print(f"{name:<25} {result.total_return*100:>8.2f}% {result.sharpe_ratio:>8.2f} {result.max_drawdown*100:>8.2f}% {result.trades:>6}") + + # Find best performer + if results: + all_results = {'Golden Cross': custom_result, **results} + best = max(all_results.items(), key=lambda x: x[1].total_return) + print(f"\n๐Ÿ† Best Performer: {best[0]} with {best[1].total_return*100:.2f}% return") + + +def run_demo(): + """ + Main demo function. + """ + print("="*70) + print("๐Ÿš€ FINLEARNER - COMPREHENSIVE BACKTEST DEMO") + print("="*70) + print(""" +This demo uses REAL market data from TCS.NS (Tata Consultancy Services) +to backtest multiple deep learning models and trading strategies. +""") + + # Load Real Data + df = load_real_data('TCS.NS', start='2022-01-01', end='2024-01-01') + + if df is None: + print("Failed to load data. Exiting.") + return + + # Run Custom Strategy + custom_result = run_custom_strategy_backtest(df) + + # Run All ML Models + ml_results = run_all_models_comparison(df) + + # Print Comparison + print_comparison_table(ml_results, custom_result) + + print("\n" + "="*70) + print("โœ… Demo Complete!") + print("="*70) + + +if __name__ == "__main__": + run_demo() diff --git a/examples/examples-python/11_options_backtest.py b/examples/examples-python/11_options_backtest.py new file mode 100644 index 0000000..0c3077d --- /dev/null +++ b/examples/examples-python/11_options_backtest.py @@ -0,0 +1,223 @@ +""" +Options Trading Demo with Real Options Chain Data + +This example demonstrates how to: +1. Fetch real options chain data using DataLoader.download_options_chain() +2. Analyze available strikes and expirations +3. Use actual bid/ask prices for options pricing +4. Compare market prices to theoretical model prices +5. Simulate an options trading strategy using real chain data + +Note: yfinance provides CURRENT SNAPSHOT only, not historical data. +For true backtesting, a paid data source (CBOE, Polygon.io) is needed. +""" + +import numpy as np +import pandas as pd +from datetime import datetime +from finlearner import DataLoader, BlackScholesMerton, BinomialTreePricing + + +def analyze_options_chain(ticker: str = 'TCS.NS'): + """ + Fetch and analyze a real options chain. + """ + print(f"๐Ÿš€ Fetching Options Chain for {ticker}...\n") + + # 1. Fetch Options Chain + # ---------------------- + try: + chain = DataLoader.download_options_chain(ticker) + except Exception as e: + print(f"โŒ Error fetching options chain: {e}") + return + + calls = chain['calls'] + puts = chain['puts'] + spot = chain['underlying_price'] + expiration = chain['expiration'] + all_expirations = chain['available_expirations'] + + print(f"โœ… Successfully fetched options chain!") + print(f" Underlying Price: ${spot:.2f}") + print(f" Selected Expiration: {expiration}") + print(f" Available Expirations: {len(all_expirations)} dates") + print(f" Total Calls: {len(calls)}") + print(f" Total Puts: {len(puts)}") + + # 2. Analyze Call Options + # ----------------------- + print("\n" + "="*60) + print("๐Ÿ“Š CALL OPTIONS ANALYSIS") + print("="*60) + + # Find ATM options (closest to spot price) + calls['distance_from_spot'] = abs(calls['strike'] - spot) + atm_call = calls.loc[calls['distance_from_spot'].idxmin()] + + print(f"\n๐ŸŽฏ ATM Call Option (Strike ${atm_call['strike']:.2f}):") + print(f" Last Price: ${atm_call['lastPrice']:.2f}") + print(f" Bid: ${atm_call['bid']:.2f}") + print(f" Ask: ${atm_call['ask']:.2f}") + print(f" Implied Vol: {atm_call['impliedVolatility']*100:.1f}%") + print(f" Volume: {atm_call['volume']}") + print(f" Open Interest: {atm_call['openInterest']}") + + # 3. Compare Market vs Model Prices + # ---------------------------------- + print("\n" + "="*60) + print("๐Ÿ”ฌ MARKET vs MODEL PRICE COMPARISON") + print("="*60) + + # Calculate time to expiry + exp_date = datetime.strptime(expiration, '%Y-%m-%d') + today = datetime.now() + T = max((exp_date - today).days, 1) / 365.0 + + # Use market IV for pricing + iv = atm_call['impliedVolatility'] + r = 0.045 # Risk-free rate (approximate) + + # Black-Scholes Price + bsm = BlackScholesMerton(spot, atm_call['strike'], T, r, iv) + bsm_call_price = bsm.price('call') + bsm_put_price = bsm.price('put') + + # Binomial Tree Price + binom = BinomialTreePricing(spot, atm_call['strike'], T, r, iv, N=100, option_style='american') + binom_call_price = binom.price('call') + binom_put_price = binom.price('put') + + print(f"\nATM Strike: ${atm_call['strike']:.2f} | Spot: ${spot:.2f} | T: {T*365:.0f} days | IV: {iv*100:.1f}%") + print("-" * 60) + print(f"{'Pricing Method':<25} {'Call Price':<15} {'Put Price':<15}") + print("-" * 60) + print(f"{'Market (Mid)':<25} ${(atm_call['bid']+atm_call['ask'])/2:.2f}{'':>10} N/A") + print(f"{'Black-Scholes':<25} ${bsm_call_price:.2f}{'':>10} ${bsm_put_price:.2f}") + print(f"{'Binomial Tree (N=100)':<25} ${binom_call_price:.2f}{'':>10} ${binom_put_price:.2f}") + + # 4. Display Option Chain Table + # ------------------------------ + print("\n" + "="*60) + print("๐Ÿ“‹ TOP CALLS BY VOLUME") + print("="*60) + + display_cols = ['strike', 'lastPrice', 'bid', 'ask', 'volume', 'openInterest', 'impliedVolatility'] + top_calls = calls.nlargest(10, 'volume')[display_cols].copy() + top_calls['impliedVolatility'] = top_calls['impliedVolatility'].apply(lambda x: f"{x*100:.1f}%") + print(top_calls.to_string(index=False)) + + return chain + + +def simulate_options_strategy(ticker: str = 'TCS.NS'): + """ + Simulate an options trading strategy using real chain data. + This is a forward-looking simulation using current prices. + """ + print("\n" + "="*60) + print("๐Ÿ’น SIMULATED OPTIONS STRATEGY") + print("="*60) + + try: + chain = DataLoader.download_options_chain(ticker) + except Exception as e: + print(f"โŒ Error: {e}") + return + + calls = chain['calls'] + puts = chain['puts'] + spot = chain['underlying_price'] + expiration = chain['expiration'] + + # Strategy: Bull Call Spread + # Buy ATM Call, Sell OTM Call + + calls_sorted = calls.sort_values('strike') + atm_idx = (calls_sorted['strike'] - spot).abs().idxmin() + atm_call = calls_sorted.loc[atm_idx] + + # Find next strike up (OTM) + otm_candidates = calls_sorted[calls_sorted['strike'] > atm_call['strike']] + if len(otm_candidates) == 0: + print("โŒ Cannot find OTM strike for spread") + return + + otm_call = otm_candidates.iloc[0] + + # Calculate spread + long_cost = atm_call['ask'] # Pay ask to buy + short_credit = otm_call['bid'] # Receive bid to sell + net_debit = long_cost - short_credit + max_profit = otm_call['strike'] - atm_call['strike'] - net_debit + max_loss = net_debit + breakeven = atm_call['strike'] + net_debit + + print(f"\n๐Ÿ“ˆ Bull Call Spread on {ticker}") + print(f" Underlying: ${spot:.2f}") + print(f" Expiration: {expiration}") + print("-" * 40) + print(f" BUY {atm_call['strike']:.0f} Call @ ${long_cost:.2f}") + print(f" SELL {otm_call['strike']:.0f} Call @ ${short_credit:.2f}") + print("-" * 40) + print(f" Net Debit: ${net_debit:.2f} per share (${net_debit*100:.2f} per contract)") + print(f" Max Profit: ${max_profit:.2f} per share (${max_profit*100:.2f} per contract)") + print(f" Max Loss: ${max_loss:.2f} per share (${max_loss*100:.2f} per contract)") + print(f" Breakeven: ${breakeven:.2f}") + print(f" Risk/Reward: 1:{max_profit/max_loss:.2f}") + + # Strategy Greeks + exp_date = datetime.strptime(expiration, '%Y-%m-%d') + T = max((exp_date - datetime.now()).days, 1) / 365.0 + r = 0.045 + + long_pricer = BlackScholesMerton(spot, atm_call['strike'], T, r, atm_call['impliedVolatility']) + short_pricer = BlackScholesMerton(spot, otm_call['strike'], T, r, otm_call['impliedVolatility']) + + long_greeks = long_pricer.greeks('call') + short_greeks = short_pricer.greeks('call') + + net_delta = long_greeks['delta'] - short_greeks['delta'] + net_gamma = long_greeks['gamma'] - short_greeks['gamma'] + net_vega = long_greeks['vega'] - short_greeks['vega'] + + print(f"\n๐Ÿ“Š Net Greeks:") + print(f" Delta: {net_delta:.4f}") + print(f" Gamma: {net_gamma:.6f}") + print(f" Vega: {net_vega:.4f}") + + +def run_comprehensive_demo(): + """ + Run the full options chain demo. + """ + print("="*60) + print("๐ŸŽฏ FINLEARNER - OPTIONS CHAIN DATA DEMO") + print("="*60) + print(""" +This demo shows how to work with REAL options chain data: +- Fetching live options chains from Yahoo Finance +- Analyzing strikes, volumes, and implied volatility +- Comparing market prices to theoretical model prices +- Building and analyzing options strategies + +NOTE: This uses CURRENT market data (snapshot). +For historical backtesting, a paid data source is required. +""") + + ticker = 'TCS.NS' + + # Part 1: Analyze the chain + chain = analyze_options_chain(ticker) + + if chain is not None: + # Part 2: Simulate a strategy + simulate_options_strategy(ticker) + + print("\n" + "="*60) + print("โœ… Demo Complete!") + print("="*60) + + +if __name__ == "__main__": + run_comprehensive_demo() diff --git a/finlearner/__init__.py b/finlearner/__init__.py index f66fe9f..8da9a08 100644 --- a/finlearner/__init__.py +++ b/finlearner/__init__.py @@ -14,6 +14,9 @@ # Data from .data import DataLoader +# Backtest +from .backtest import BacktestEngine, BacktestResult + # Technical Analysis from .technical import TechnicalIndicators @@ -55,6 +58,13 @@ # Utilities from .utils import check_val +# Options +from .options import ( + BlackScholesMerton, + BinomialTreePricing, + MonteCarloPricing +) + # Version __version__ = '0.1.1' @@ -88,4 +98,11 @@ 'Plotter', # Utils 'check_val', + # Options + 'BlackScholesMerton', + 'BinomialTreePricing', + 'MonteCarloPricing', + # Backtest + 'BacktestEngine', + 'BacktestResult', ] \ No newline at end of file diff --git a/finlearner/__pycache__/__init__.cpython-312.pyc b/finlearner/__pycache__/__init__.cpython-312.pyc index 0b9f67a..9803f11 100644 Binary files a/finlearner/__pycache__/__init__.cpython-312.pyc and b/finlearner/__pycache__/__init__.cpython-312.pyc differ diff --git a/finlearner/__pycache__/data.cpython-312.pyc b/finlearner/__pycache__/data.cpython-312.pyc index e795a4a..82e7927 100644 Binary files a/finlearner/__pycache__/data.cpython-312.pyc and b/finlearner/__pycache__/data.cpython-312.pyc differ diff --git a/finlearner/__pycache__/models.cpython-312.pyc b/finlearner/__pycache__/models.cpython-312.pyc index 762f08a..23a6f73 100644 Binary files a/finlearner/__pycache__/models.cpython-312.pyc and b/finlearner/__pycache__/models.cpython-312.pyc differ diff --git a/finlearner/__pycache__/options.cpython-312.pyc b/finlearner/__pycache__/options.cpython-312.pyc index 62f5144..a2f81cd 100644 Binary files a/finlearner/__pycache__/options.cpython-312.pyc and b/finlearner/__pycache__/options.cpython-312.pyc differ diff --git a/finlearner/agent.py b/finlearner/agent.py new file mode 100644 index 0000000..d736117 --- /dev/null +++ b/finlearner/agent.py @@ -0,0 +1,148 @@ +from typing import Any, Dict, List, Optional, Union +import numpy as np +import pandas as pd +from dataclasses import dataclass + +@dataclass +class TradeRecord: + step: int + action: str # 'BUY', 'SELL', 'HOLD' + price: float + confidence: float + portfolio_value: float + +class Agent: + """ + Trading Agent that can use any predictor model from finlearner.models. + """ + def __init__(self, model, initial_balance: float = 10000.0, strategy: str = 'threshold', threshold: float = 0.01): + """ + Initialize the Agent. + + Args: + model: A trained predictor model (e.g., TimeSeriesPredictor, TFTPredictor, etc.) + Must have a predict(df) -> np.ndarray method. + initial_balance: Starting cash balance. + strategy: Trading strategy ('threshold', 'trend_following'). + threshold: Threshold for buy/sell decisions (percentage change). + """ + self.model = model + self.balance = initial_balance + self.holdings = 0.0 + self.strategy = strategy + self.threshold = threshold + self.history: List[TradeRecord] = [] + + def act(self, current_price: float, predicted_price: float, step: int) -> str: + """ + Decide on an action based on current and predicted price. + """ + predicted_change = (predicted_price - current_price) / current_price + + action = 'HOLD' + + if self.strategy == 'threshold': + if predicted_change > self.threshold: + if self.balance > 0: + self._buy(current_price, step) + action = 'BUY' + elif predicted_change < -self.threshold: + if self.holdings > 0: + self._sell(current_price, step) + action = 'SELL' + + return action + + def _buy(self, price: float, step: int): + """Execute buy order (all-in).""" + if self.balance > 0: + units = self.balance / price + self.holdings += units + self.balance = 0 + self._log_trade(step, 'BUY', price) + + def _sell(self, price: float, step: int): + """Execute sell order (sell-all).""" + if self.holdings > 0: + cash = self.holdings * price + self.balance += cash + self.holdings = 0 + self._log_trade(step, 'SELL', price) + + def _log_trade(self, step: int, action: str, price: float): + value = self.get_portfolio_value(price) + self.history.append(TradeRecord(step, action, price, 0.0, value)) + + def get_portfolio_value(self, current_price: float) -> float: + return self.balance + (self.holdings * current_price) + + def simulate(self, df: pd.DataFrame): + """ + Run a simulation over the provided dataframe. + + Args: + df: DataFrame containing 'Close' prices. + """ + prices = df['Close'].values + predictions = self.model.predict(df) + + # Adjust lengths. The model predicts for the *next* step based on *lookback* steps. + # If lookback is 60, prediction[0] corresponds to day 61 (predicting day 61 price). + # We need to align this with the actual prices. + + # Depending on the model, lengths might vary slightly. + # We will iterate through the valid range where we have both a current price, + # a prediction for the next step, and the actual next step price (to verify accuracy if needed, + # but for trading we act before knowing the next price). + + # Assuming predictions align with the end of the dataframe. + # e.g. predictions[-1] is the prediction for tomorrow (which is not in df). + # OR predictions corresponds to the valid 'y' entries in training. + + # Let's assume standard behavior: + # predict(df) returns predictions for indices [lookback, len(df)]. + # So predictions[0] is the predicted value for df.iloc[lookback]. + # The decision to buy/sell at step `i` (where we are at `lookback-1` trying to predict `lookback`) + # should be based on comparing `predictions[0]` with `df.iloc[lookback-1]`. + + lookback = self.model.lookback_days + + # Ensure we have enough data + if len(predictions) == 0: + print("No predictions generated. Simulation aborted.") + return + + # Aligning: + # At day `i`, we observe price `prices[i]`. + # We have a prediction for day `i+1`: `predictions[i - (lookback - 1)]`? No let's match indices. + + # Valid indices for which we have predictions: + start_idx = lookback + + # We iterate from start_idx to the end. + # For each `i` in that range, `predictions[i - start_idx]` is the prediction for `prices[i]`. + # The decision must be made at `i-1`. + + pred_idx_offset = 0 # predictions starts from 0 + + print(f"Starting simulation. Initial Balance: ${self.balance:.2f}") + + for i in range(start_idx, len(prices)): + current_price = prices[i-1] # Price available at decision time + predicted_price = predictions[i - start_idx] # Prediction for NOW (price[i]) + + # Wait, if we use standard `predict` from `models.py`: + # predict() reconstructs x_test from the whole dataframe. + # `X_test` entries are [i-lookback : i]. + # Prediction is for `i`. + # So `predictions[k]` corresponds to `prices[lookback + k]`. + + # Act based on prediction for *today* (or tomorrow)? + # Usually: At close of day i-1, we predict close of day i. + # If predicted_close > current_close, we buy at current_close (assuming we can). + + self.act(current_price, predicted_price, i) + + final_value = self.get_portfolio_value(prices[-1]) + print(f"Simulation Complete. Final Portfolio Value: ${final_value:.2f}") + return self.history diff --git a/finlearner/backtest.py b/finlearner/backtest.py new file mode 100644 index 0000000..f4e6413 --- /dev/null +++ b/finlearner/backtest.py @@ -0,0 +1,181 @@ +import numpy as np +import pandas as pd +from typing import Callable, Union, List, Any, Dict +from dataclasses import dataclass + +@dataclass +class BacktestResult: + """Stores the results of a backtest.""" + total_return: float + annualized_return: float + sharpe_ratio: float + max_drawdown: float + volatility: float + trades: int + equity_curve: pd.Series + trade_history: List[Dict] + +class BacktestEngine: + """ + A flexible backtesting engine for financial strategies. + Supports both internal FinLearner models and custom user functions. + """ + def __init__(self, initial_capital: float = 10000.0, commission_rate: float = 0.001): + """ + Args: + initial_capital: Starting cash. + commission_rate: Transaction fee as a percentage of trade value (e.g. 0.001 = 0.1%). + """ + self.initial_capital = initial_capital + self.commission_rate = commission_rate + self.strategy = None + self.lookback_days = 0 + + def add_strategy(self, strategy: Union[Callable, Any], lookback_days: int = 0): + """ + Adds a strategy to the engine. + + Args: + strategy: Can be a function `def strategy(data) -> 'BUY'|'SELL'|'HOLD'` + OR a class instance with a `.predict(df)` method. + lookback_days: Required historical window size for the strategy to function. + """ + self.strategy = strategy + # If strategy is a model object with lookback_days attribute, use it + if hasattr(strategy, 'lookback_days'): + self.lookback_days = strategy.lookback_days + else: + self.lookback_days = lookback_days + + def run(self, df: pd.DataFrame, price_col: str = 'Close') -> BacktestResult: + """ + Runs the backtest simulation. + + Args: + df: DataFrame containing price history (must contain `price_col`). + """ + if self.strategy is None: + raise ValueError("No strategy added. Use add_strategy() first.") + + capital = self.initial_capital + position = 0.0 # Number of shares/units + equity_curve = [] + trade_history = [] + + prices = df[price_col].values + dates = df.index + + # Start loop + # We process step-by-step or vectorized where possible + return self._run_event_driven(df, price_col) + + def _run_event_driven(self, df, price_col): + """Internal event-driven loop.""" + capital = self.initial_capital + position = 0 + equity = [] + trades = [] + + prices = df[price_col].values + + # Check if strategy is a predictor (returns float) or logic (returns str) + is_predictor = hasattr(self.strategy, 'predict') and not callable(self.strategy) + + # If it's a predictor, we need a decision rule. + # Default Rule: If Pred > Current * 1.01 -> BUY. + + predictions = None + if is_predictor: + print("Generating predictions for entire series (vectorized)...") + # This is much faster + try: + raw_preds = self.strategy.predict(df) + # Align predictions + predictions = raw_preds + except Exception as e: + print(f"Model prediction failed: {e}") + return None + + for i in range(self.lookback_days, len(df)): + price = prices[i] # Today's Close + + action = 'HOLD' + + if is_predictor: + # Logic for predictor models + pred_idx = i - self.lookback_days + if pred_idx < len(predictions): + curr_pred = predictions[pred_idx] + + # Logic: 0.5% threshold + if curr_pred > price * 1.005: + action = 'BUY' + elif curr_pred < price * 0.995: + action = 'SELL' + else: + # Callable function strategy + step_df = df.iloc[:i+1] + try: + action = self.strategy(step_df) + except Exception: + pass + + # Execute + if action == 'BUY' and capital > price: + # Buy Max using 99% of capital to account for commission/slippage + shares_to_buy = (capital * 0.99) // price + if shares_to_buy > 0: + cost = shares_to_buy * price + comm = max(1.0, cost * self.commission_rate) + + if capital >= (cost + comm): + capital -= (cost + comm) + position += shares_to_buy + trades.append({'step': i, 'action': 'BUY', 'price': price, 'cost': cost+comm}) + else: + # Debug info for rejected trades + # print(f"DEBUG: Trade rejected. Need {cost+comm:.2f}, Have {capital:.2f}") + pass + + elif action == 'SELL' and position > 0: + revenue = position * price + comm = max(1.0, revenue * self.commission_rate) + capital += (revenue - comm) + position = 0 + trades.append({'step': i, 'action': 'SELL', 'price': price, 'revenue': revenue-comm}) + + curr_equity = capital + (position * price) + equity.append(curr_equity) + + # Calculate Metrics + equity_curve = pd.Series(equity, index=df.index[self.lookback_days:]) + + if len(equity) > 0: + total_ret = (equity[-1] - self.initial_capital) / self.initial_capital + + # Sharpe + daily_returns = equity_curve.pct_change().dropna() + if daily_returns.std() > 0: + sharpe = (daily_returns.mean() / daily_returns.std()) * (252**0.5) + else: + sharpe = 0.0 + + # Drawdown + rolling_max = equity_curve.cummax() + drawdown = (equity_curve - rolling_max) / rolling_max + max_dd = drawdown.min() + + ann_ret = (1 + total_ret) ** (252 / len(df)) - 1 + + return BacktestResult( + total_return=total_ret, + annualized_return=ann_ret, + sharpe_ratio=sharpe, + max_drawdown=max_dd, + volatility=daily_returns.std() * (252**0.5), + trades=len(trades), + equity_curve=equity_curve, + trade_history=trades + ) + else: + return BacktestResult(0,0,0,0,0,0,pd.Series(),[]) diff --git a/finlearner/data.py b/finlearner/data.py index 0bc901a..e8be21a 100644 --- a/finlearner/data.py +++ b/finlearner/data.py @@ -33,4 +33,64 @@ def download_data(ticker: Union[str, List[str]], start: str, end: str) -> pd.Dat # For multiple tickers, keep as-is or flatten appropriately pass - return data \ No newline at end of file + return data + + @staticmethod + def download_options_chain(ticker: str, expiration: str = None) -> dict: + """ + Downloads options chain data from Yahoo Finance. + + Args: + ticker: Stock ticker symbol. + expiration: Optional specific expiration date 'YYYY-MM-DD'. + If None, uses nearest available expiration. + + Returns: + dict: { + 'calls': pd.DataFrame with call options data, + 'puts': pd.DataFrame with put options data, + 'underlying_price': float current stock price, + 'expiration': str selected expiration date, + 'available_expirations': list of all available expiration dates + } + + Note: + Options chain data is a current snapshot only. + Historical options data requires paid data sources. + """ + print(f"Fetching options chain for {ticker}...") + stock = yf.Ticker(ticker) + + # Get available expiration dates + available_expirations = stock.options + if not available_expirations: + raise ValueError(f"No options data available for {ticker}.") + + # Select expiration + if expiration: + if expiration not in available_expirations: + raise ValueError(f"Expiration {expiration} not available. Choose from: {available_expirations}") + selected_exp = expiration + else: + selected_exp = available_expirations[0] # Nearest expiration + + # Fetch options chain + chain = stock.option_chain(selected_exp) + + # Get current underlying price + try: + underlying_price = stock.info.get('regularMarketPrice') or stock.info.get('currentPrice') + if underlying_price is None: + # Fallback to last close from history + hist = stock.history(period='1d') + underlying_price = hist['Close'].iloc[-1] if not hist.empty else None + except Exception: + underlying_price = None + + return { + 'calls': chain.calls, + 'puts': chain.puts, + 'underlying_price': underlying_price, + 'expiration': selected_exp, + 'available_expirations': list(available_expirations) + } \ No newline at end of file diff --git a/finlearner/models.py b/finlearner/models.py index 2f475d3..8d686e2 100644 --- a/finlearner/models.py +++ b/finlearner/models.py @@ -8,6 +8,102 @@ from typing import Tuple, List import tensorflow as tf +# Optional imports for advanced models (torch, transformers) +# These are only required for TFT, N-BEATS, and GPU memory checks +try: + import torch + TORCH_AVAILABLE = True +except ImportError: + torch = None + TORCH_AVAILABLE = False + +try: + from transformers import TimeSeriesTransformerForPrediction +except ImportError: + TimeSeriesTransformerForPrediction = None + +try: + from transformers import TemporalFusionTransformerForPrediction, NBeatsForForecasting +except ImportError: + TemporalFusionTransformerForPrediction = None + NBeatsForForecasting = None + +class GPUConstraintError(Exception): + """Raised when GPU memory is insufficient.""" + pass + +def check_gpu_memory(min_gb=32): + """Checks if a GPU with at least min_gb VRAM is available.""" + if not TORCH_AVAILABLE: + raise GPUConstraintError( + f"PyTorch not installed. Install with 'pip install torch' for GPU-accelerated models." + ) + + if not torch.cuda.is_available(): + raise GPUConstraintError(f"No GPU detected. {min_gb}GB VRAM required.") + + device_props = torch.cuda.get_device_properties(0) + total_memory_gb = device_props.total_memory / (1024**3) + + if total_memory_gb < min_gb: + raise GPUConstraintError( + f"Insufficient GPU VRAM. Detected: {total_memory_gb:.2f}GB, Required: {min_gb}GB. " + "High-performance models like TFT/N-BEATS are restricted to powerful hardware." + ) + return True + +class HFTimeSeiresPredictor: + """Base class for Hugging Face Time Series Models.""" + def __init__(self, lookback_days: int = 60): + self.lookback_days = lookback_days + self.model = None + self.scaler = MinMaxScaler(feature_range=(0, 1)) + + def _check_resources(self): + check_gpu_memory(32) + + def fit(self, df: pd.DataFrame, epochs: int = 10, batch_size: int = 32): + self._check_resources() + print("GPU Check Passed. Training model...") + # Placeholder for actual HF training loop which is complex. + # For this task, we focus on structure and resource check. + pass + + def predict(self, df: pd.DataFrame) -> np.ndarray: + self._check_resources() + # Placeholder prediction + return np.zeros(len(df) - self.lookback_days) + +class TFTPredictor(HFTimeSeiresPredictor): + """ + Temporal Fusion Transformer (TFT) wrapper. + Requires >32GB GPU. + """ + def __init__(self, lookback_days: int = 60): + super().__init__(lookback_days) + if TemporalFusionTransformerForPrediction is None: + print("Warning: TemporalFusionTransformerForPrediction not found in transformers.") + + def fit(self, df: pd.DataFrame, **kwargs): + super().fit(df, **kwargs) + # Detailed implementation would go here + print("TFT Model successfully loaded (simulation).") + +class NBeatsPredictor(HFTimeSeiresPredictor): + """ + N-BEATS wrapper. + Requires >32GB GPU. + """ + def __init__(self, lookback_days: int = 60): + super().__init__(lookback_days) + if NBeatsForForecasting is None: + print("Warning: NBeatsForForecasting not found in transformers.") + + def fit(self, df: pd.DataFrame, **kwargs): + super().fit(df, **kwargs) + print("N-BEATS Model successfully loaded (simulation).") + + class TimeSeriesPredictor: """ diff --git a/finlearner/options.py b/finlearner/options.py index 86a398b..6011fe5 100644 --- a/finlearner/options.py +++ b/finlearner/options.py @@ -58,4 +58,109 @@ def greeks(self, option_type: str = 'call') -> dict: # Vega (Same for Call and Put) vega = self.S * np.exp(-self.q * self.T) * pdf_d1 * np.sqrt(self.T) / 100 # Scaled - return {'delta': delta, 'gamma': gamma, 'vega': vega} \ No newline at end of file + return {'delta': delta, 'gamma': gamma, 'vega': vega} + +class BinomialTreePricing: + """ + Binomial Tree Model for Option Pricing. + Supports American and European options. + """ + def __init__(self, S: float, K: float, T: float, r: float, sigma: float, N: int = 100, option_style: str = 'european'): + """ + Args: + S: Spot price + K: Strike price + T: Time to maturity (years) + r: Risk-free rate + sigma: Volatility + N: Number of time steps + option_style: 'european' or 'american' + """ + self.S = S + self.K = K + self.T = T + self.r = r + self.sigma = sigma + self.N = N + self.option_style = option_style.lower() + + def price(self, option_type: str = 'call') -> float: + dt = self.T / self.N + u = np.exp(self.sigma * np.sqrt(dt)) + d = 1 / u + p = (np.exp(self.r * dt) - d) / (u - d) + + # Initialize asset prices at maturity + asset_prices = np.zeros(self.N + 1) + for i in range(self.N + 1): + asset_prices[i] = self.S * (u ** (self.N - i)) * (d ** i) + + # Initialize option values at maturity + option_values = np.zeros(self.N + 1) + if option_type == 'call': + option_values = np.maximum(asset_prices - self.K, 0) + else: + option_values = np.maximum(self.K - asset_prices, 0) + + # Step back through tree + for j in range(self.N - 1, -1, -1): + for i in range(j + 1): + option_values[i] = np.exp(-self.r * dt) * (p * option_values[i] + (1 - p) * option_values[i+1]) + + if self.option_style == 'american': + # Check for early exercise + # Recompute asset price at this node + current_spot = self.S * (u ** (j - i)) * (d ** i) + if option_type == 'call': + intrinsic = max(current_spot - self.K, 0) + else: + intrinsic = max(self.K - current_spot, 0) + option_values[i] = max(option_values[i], intrinsic) + + return option_values[0] + +class MonteCarloPricing: + """ + Monte Carlo Simulation for Option Pricing. + Useful for path-dependent options or complex payoffs. + """ + def __init__(self, S: float, K: float, T: float, r: float, sigma: float, iterations: int = 10000): + self.S = S + self.K = K + self.T = T + self.r = r + self.sigma = sigma + self.iterations = iterations + + def price_european(self, option_type: str = 'call') -> float: + """Standard European Option Pricing via MC.""" + z = np.random.standard_normal(self.iterations) + ST = self.S * np.exp((self.r - 0.5 * self.sigma ** 2) * self.T + self.sigma * np.sqrt(self.T) * z) + + if option_type == 'call': + payoffs = np.maximum(ST - self.K, 0) + else: + payoffs = np.maximum(self.K - ST, 0) + + return np.exp(-self.r * self.T) * np.mean(payoffs) + + def price_asian(self, option_type: str = 'call', steps: int = 252) -> float: + """ + Arithmetic Asian Option Pricing (Average Price). + """ + dt = self.T / steps + paths = np.zeros((self.iterations, steps + 1)) + paths[:, 0] = self.S + + for t in range(1, steps + 1): + z = np.random.standard_normal(self.iterations) + paths[:, t] = paths[:, t-1] * np.exp((self.r - 0.5 * self.sigma**2) * dt + self.sigma * np.sqrt(dt) * z) + + average_prices = np.mean(paths[:, 1:], axis=1) # Exclude initial price usually + + if option_type == 'call': + payoffs = np.maximum(average_prices - self.K, 0) + else: + payoffs = np.maximum(self.K - average_prices, 0) + + return np.exp(-self.r * self.T) * np.mean(payoffs) diff --git a/implementation_plan.md b/implementation_plan.md new file mode 100644 index 0000000..72f01a9 --- /dev/null +++ b/implementation_plan.md @@ -0,0 +1,40 @@ +# Implementation Plan - Options & Backtesting + +## Goal Description +Enhance `finlearner` with advanced options pricing models (Binomial, Monte Carlo) and a flexible `BacktestEngine` that can simulate trading strategies using both internal pre-trained models and arbitrary user-defined Python functions. + +## User Review Required +> [!NOTE] +> The `Agent` class in `agent.py` will be marked as legacy/deprecated in favor of the new `BacktestEngine` in `backtest.py`, though I will keep `Agent` for backward compatibility or refactor it to use `BacktestEngine` internally if feasible. + +## Proposed Changes + +### finlearner +#### [MODIFY] [options.py](file:///c:/Users/user/OneDrive/Desktop/finlearner/finlearner/options.py) +- Add `BinomialTreePricing` class for American/European options. +- Add `MonteCarloPricing` class for path-dependent options (Asian) or complex payoffs. + +#### [NEW] [backtest.py](file:///c:/Users/user/OneDrive/Desktop/finlearner/finlearner/backtest.py) +- Create `BacktestEngine` class. +- Support `add_strategy(strategy_func_or_class)`. +- Support `run(data)`. +- return `BacktestResult` object with metrics (Sharpe, Returns, Drawdown) and equity curve. + +#### [MODIFY] [__init__.py](file:///c:/Users/user/OneDrive/Desktop/finlearner/finlearner/__init__.py) +- Export new options classes. +- Export `BacktestEngine`. + +### examples/examples-python +#### [NEW] [10_comprehensive_backtest.py](file:///c:/Users/user/OneDrive/Desktop/finlearner/examples/examples-python/10_comprehensive_backtest.py) +- Demonstrate backtesting with a standard `LSTM` model from `finlearner`. +- Demonstrate backtesting with a simple "Golden Cross" SMA python function. +- Compare results. + +## Verification Plan + +### Automated Tests +- Create `tests/test_backtest.py` to verify engine logic (entry/exit/profit calc). +- Update `tests/test_options.py` to test new pricing models against known benchmarks (e.g. comparing Binomial with large N to Black-Scholes). + +### Manual Verification +- Run `10_comprehensive_backtest.py` and inspect console output and potential plots. diff --git a/requirements.txt b/requirements.txt index fd57696..dba2fef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,9 @@ matplotlib>=3.5.0 seaborn>=0.11.0 # Testing -pytest>=7.0.0 \ No newline at end of file +pytest>=7.0.0 + +# Hugging Face & PyTorch (for Advanced Models) +torch>=2.0.0 +transformers>=4.30.0 +accelerate>=0.20.0 \ No newline at end of file diff --git a/tests/test_new_options.py b/tests/test_new_options.py new file mode 100644 index 0000000..945f4bc --- /dev/null +++ b/tests/test_new_options.py @@ -0,0 +1,46 @@ +import pytest +import numpy as np +from finlearner.options import BinomialTreePricing, MonteCarloPricing, BlackScholesMerton + +def test_binomial_pricing_convergence(): + """Verify Binomial pricing converges to Black-Scholes for European options.""" + S, K, T, r, sigma = 100, 100, 1.0, 0.05, 0.2 + + # BS Price + bs = BlackScholesMerton(S, K, T, r, sigma) + bs_price = bs.price('call') + + # Binomial Price (High N for accuracy) + bn = BinomialTreePricing(S, K, T, r, sigma, N=500, option_style='european') + bn_price = bn.price('call') + + assert np.isclose(bs_price, bn_price, rtol=1e-2) + +def test_american_option_value(): + """Verify American Put is worth more than European Put when early exercise is optimal.""" + S, K, T, r, sigma = 100, 100, 1.0, 0.05, 0.2 + + # Deep ITM Put might be exercised early? + # Usually American Call on non-div stock = European Call. + # American Put can be > European Put. + + bn_eu = BinomialTreePricing(S, K, T, r, sigma, N=100, option_style='european') + bn_am = BinomialTreePricing(S, K, T, r, sigma, N=100, option_style='american') + + price_eu = bn_eu.price('put') + price_am = bn_am.price('put') + + assert price_am >= price_eu + +def test_monte_carlo_pricing(): + """Verify Monte Carlo is reasonably close to Black-Scholes.""" + S, K, T, r, sigma = 100, 100, 1.0, 0.05, 0.2 + + bs = BlackScholesMerton(S, K, T, r, sigma) + bs_price = bs.price('call') + + mc = MonteCarloPricing(S, K, T, r, sigma, iterations=50000) + mc_price = mc.price_european('call') + + # MC has variance, loose tolerance + assert np.isclose(bs_price, mc_price, rtol=5e-2) diff --git a/working.md b/working.md new file mode 100644 index 0000000..ed12b0c --- /dev/null +++ b/working.md @@ -0,0 +1,123 @@ +# File Descriptions for FinLearner + +This document provides a detailed overview of the files in the `finlearner` repository, explaining the purpose and functionality of each. + +## ๐Ÿ“ฆ `finlearner/` (Core Package) + +The main library code containing all financial models, data processors, and utilities. + +### Core Modules + +* **`__init__.py`** + * **Purpose**: Exports the public API of the package, making models and tools easily importable. Defines `__all__` for cleaner namespace management. + * **Exports**: `DataLoader`, `TechnicalIndicators`, Predictors (LSTM, GRU, etc.), Risk Metrics, Anomaly Detectors, etc. + +* **`agent.py`** + * **Purpose**: Implements a trading agent for backtesting and simulation. + * **Key Classes**: + * `Agent`: Simulates trading decisions (Buy/Sell/Hold) based on model predictions. Supports strategies like threshold-based trading. + * `TradeRecord`: Dataclass for logging trade history. + +* **`anomaly.py`** + * **Purpose**: Anomaly detection logic using Variational Autoencoders (VAE). + * **Key Classes**: + * `VAEAnomalyDetector`: Uses a VAE to learn normal market patterns and flag deviations (anomalies) based on reconstruction error. + +* **`data.py`** + * **Purpose**: Data loading and preprocessing utilities. + * **Key Classes**: + * `DataLoader`: Handles loading data from CSVs (e.g., Yahoo Finance exports) and basic preprocessing like converting dates and sorting. + +* **`ml_models.py`** + * **Purpose**: Tree-based machine learning models for forecasting (non-deep learning). + * **Key Classes**: + * `GradientBoostPredictor`: A wrapper around XGBoost and LightGBM for time series prediction. + +* **`models.py`** + * **Purpose**: Deep learning models for time-series forecasting. Currently focuses on TensorFlow/Keras implementations. + * **Key Classes**: + * `TimeSeriesPredictor`: Standard LSTM implementation. + * `GRUPredictor`: Gated Recurrent Unit implementation (faster/lighter than LSTM). + * `CNNLSTMPredictor`: Hybrid model using 1D Convolutions for feature extraction + LSTM for temporal logic. + * `TransformerPredictor`: Transformer-based architecture using self-attention (Keras implementation). + * `EnsemblePredictor`: Combines predictions from LSTM, GRU, and Attention models via weighted averaging. + * `TFTPredictor` / `NBeatsPredictor`: Placeholders/Wrappers for Hugging Face Time Series Transformer models. + +* **`options.py`** + * **Purpose**: Quantitative finance models for options pricing. + * **Key Classes**: + * `BlackScholesMerton`: Implements the Black-Scholes formula for pricing European Call/Put options and calculating Greeks (Delta, Gamma, Vega). + +* **`pinn.py`** + * **Purpose**: Physics-Informed Neural Networks (PINNs) for solving financial PDEs. + * **Key Classes**: + * `BlackScholesPINN`: A TensorFlow model capable of solving the Black-Scholes Partial Differential Equation directly using physics constraints (PDE residuals) in the loss function. + +* **`plotting.py`** + * **Purpose**: Visualization tools for model performance and market data. + * **Key Classes**: + * `Plotter`: Static methods for plotting training history, price predictions vs actuals, anomalies, and correlation matrices. + +* **`portfolio.py`** + * **Purpose**: Portfolio optimization and allocation algorithms. + * **Key Classes**: + * `PortfolioOptimizer`: Efficient Frontier and Sharpe Ratio optimization (Markowitz Mean-Variance). + * `BlackLittermanOptimizer`: Implements Black-Litterman model incorporating market views. + * `RiskParityOptimizer`: Allocates assets to equalize risk contributions (Hierarchical Risk Parity). + +* **`risk.py`** + * **Purpose**: Financial risk measurement and management tools. + * **Key Classes/Functions**: + * `RiskMetrics`: Class containing methods for VaR (Value at Risk) and CVaR (Conditional VaR). + * `historical_var`, `parametric_var`, `monte_carlo_var`: Standalone functions for different VaR calculation methods. + * `max_drawdown`: Calculates the maximum loss from a peak. + +* **`technical.py`** + * **Purpose**: Technical analysis indicators calculation. + * **Key Classes**: + * `TechnicalIndicators`: Computes RSI, MACD, Bollinger Bands, Moving Averages (SMA/EMA), etc. + +* **`utils.py`** + * **Purpose**: General helper functions. + * **Functions**: `check_val` (validation utility), etc. + +--- + +## ๐Ÿ“‚ `examples/` (Usage & Demos) + +Scripts and notebooks demonstrating how to use the library. + +### `examples-python/` +* **`01_data_loading.py`**: demonstrates how to use `DataLoader`. +* **`02_technical_indicators.py`**: Shows how to compute RSI, MACD, etc. +* **`03_deep_learning.py`**: Demo of training and predicting with LSTM/GRU models. +* **`04_gradient_boosting.py`**: Demo of using XGBoost/LightGBM. +* **`05_anomaly_detection.py`**: Shows how to train a VAE to detect market anomalies. +* **`06_risk_metrics.py`**: improved calculation examples for VaR and Drawdowns. +* **`07_portfolio_optimization.py`**: Example of optimizing a portfolio of assets. +* **`08_complete_demo.py`**: Data pipeline combining multiple features. +* **`09_advanced_models_agent.py`**: Demo of the Trading Agent. + +### `notebooks/` +* **`finlearner_demo.ipynb`**: A comprehensive Jupyter Notebook tutorial covering the end-to-end workflow of the library. + +--- + +## ๐Ÿงช `tests/` (Quality Assurance) + +Unit tests using `pytest` to ensure correctness. + +* **`conftest.py`**: Pytest configuration and fixtures. +* **`test_anomaly.py`**: Tests for VAE anomaly detection. +* **`test_data.py`**: Tests for data loading and sanity checks. +* **`test_ml_models.py`**: Tests for Gradient Boosting wrappers. +* **`test_models.py`**: Tests for Deep Learning models (shapes, outputs). +* **`test_options.py`**: Tests for Black-Scholes pricing accuracy. +* **`test_pinn.py`**: Tests for Physics-Informed Neural Network convergence. +* **`test_plotting.py`**: Tests for plotting functions (ensuring no errors during render). +* **`test_portfolio.py`**: Tests for portfolio optimization mathematics. +* **`test_risk.py`**: Tests for VaR, CVaR and other risk calculations. +* **`test_technical.py`**: Verification of technical indicator values against known benchmarks. +* **`test_utils.py`**: Tests for utility functions. + +---