├── 15_session ├── requirements.txt ├── favicon.ico ├── tpq_logo_bic.png ├── genai_bootcamp.png ├── README.md ├── revert_deploy.sh ├── app.py ├── thankyou.html ├── deploy.sh └── index.html ├── 05_session ├── aapl-10-Q.pdf ├── amzn-10-Q.pdf ├── msft-10-Q.pdf ├── ai_strategy_gemini.pdf ├── 10_q_and_ai_prompt.txt ├── backtest_ai_perplexity.py ├── backtest_ai_gemini.py └── backtest_ai_gpt_4o.py ├── 10_options ├── mcdxa │ ├── pricers │ │ ├── __init__.py │ │ ├── european.py │ │ └── american.py │ ├── __init__.py │ ├── utils.py │ ├── exceptions.py │ ├── analytics.py │ ├── monte_carlo.py │ ├── bsm.py │ ├── merton.py │ ├── bates.py │ ├── heston.py │ └── payoffs.py ├── prompts │ ├── user_prompts.txt │ ├── prompt.txt │ ├── user_prompts_5.txt │ ├── user_prompts_1.txt │ ├── extract_user_prompts.py │ ├── user_prompts_2.txt │ ├── user_prompts_3.txt │ └── user_prompts_4.txt ├── tests │ ├── conftest.py │ ├── test_monte_carlo.py │ ├── test_models.py │ ├── test_custom_payoff.py │ ├── test_payoffs.py │ ├── test_pricers_european.py │ ├── test_bates.py │ └── test_pricers_american.py ├── test.md ├── README.md ├── scripts │ ├── exotics.py │ ├── benchmarks │ │ ├── benchmark_bsm.py │ │ ├── benchmark_american.py │ │ ├── benchmark_heston.py │ │ ├── benchmark_mjd.py │ │ └── benchmark_bates.py │ ├── orchestrate.py │ └── mcdxa.ipynb └── outline.md ├── 03_session ├── ai_job_apocalypse.pdf ├── portfolio_sonnet_4.pdf ├── fundamentals_prompts.txt ├── portfolio_deepseek_r1.md ├── aia_fundamentals.csv └── portfolio_gpt_4o.md ├── 06_session ├── fibonacci_orig.py ├── prompt.txt ├── find_np_mpl.sh ├── fibonacci.py ├── black_scholes.py └── index.html ├── README.md ├── 07_session ├── dataviz_howto.txt ├── dataviz.html └── spaceinvaders.html ├── 04_session ├── _store │ └── strat_perplexity_test.py ├── benchmark_perplexity.py ├── aia_fundamentals.csv ├── backtest_perplexity.py └── backtest_gemini_25_flash.py ├── 08_session ├── user_prompts.txt ├── extract_user_prompts.py ├── sma_browser.py └── sma_backtest.py ├── 09_session ├── user_prompts.txt ├── eod_data_visualizer.py └── streamlit_portfolio_app.py ├── .gitignore ├── 02_session └── data_analysis_gpt_4o.md └── 01_session ├── bsm_codex.py ├── codex_cli_explained.md └── breakout.html /15_session/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=2.0 2 | -------------------------------------------------------------------------------- /15_session/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/aia/main/15_session/favicon.ico -------------------------------------------------------------------------------- /05_session/aapl-10-Q.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/aia/main/05_session/aapl-10-Q.pdf -------------------------------------------------------------------------------- /05_session/amzn-10-Q.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/aia/main/05_session/amzn-10-Q.pdf -------------------------------------------------------------------------------- /05_session/msft-10-Q.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/aia/main/05_session/msft-10-Q.pdf -------------------------------------------------------------------------------- /10_options/mcdxa/pricers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | High-level pricers for option valuation. 3 | """ 4 | -------------------------------------------------------------------------------- /15_session/tpq_logo_bic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/aia/main/15_session/tpq_logo_bic.png -------------------------------------------------------------------------------- /10_options/mcdxa/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Option Pricing package initialization. 3 | """ 4 | __version__ = "0.1.0" -------------------------------------------------------------------------------- /15_session/genai_bootcamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/aia/main/15_session/genai_bootcamp.png -------------------------------------------------------------------------------- /03_session/ai_job_apocalypse.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/aia/main/03_session/ai_job_apocalypse.pdf -------------------------------------------------------------------------------- /03_session/portfolio_sonnet_4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/aia/main/03_session/portfolio_sonnet_4.pdf -------------------------------------------------------------------------------- /05_session/ai_strategy_gemini.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/aia/main/05_session/ai_strategy_gemini.pdf -------------------------------------------------------------------------------- /06_session/fibonacci_orig.py: -------------------------------------------------------------------------------- 1 | def fib_it_py(n): 2 | x, y = 0, 1 3 | for _ in range(1, n + 1): 4 | x, y = y, x + y 5 | return x -------------------------------------------------------------------------------- /10_options/mcdxa/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def discount_factor(r: float, T: float) -> float: 5 | """Compute discount factor exp(-r*T).""" 6 | return np.exp(-r * T) -------------------------------------------------------------------------------- /10_options/prompts/user_prompts.txt: -------------------------------------------------------------------------------- 1 | Change the mcdxa package repo/folder so that it becomes easily installable via pip (pip install git+https://github.com/yhilpisch/mcdxa.git). 2 | 3 | -------------------------------------------------------------------------------- /06_session/prompt.txt: -------------------------------------------------------------------------------- 1 | The task is to make a full-fledged Python module out of finbonacci.py. Analyze the file, add The Python Quants GmbH as the author incl. copyright. Add a detailed docstring and implement unit tests for the function within the same the file. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AI Assistants 2 | ============= 3 | 4 | **For Quant Research, Development, and Deployment** 5 | 6 | Dr. Yves J. Hilpisch
7 | CPF & TAQ Programs 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /06_session/find_np_mpl.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Recursively find Python files importing numpy and matplotlib 4 | find . -type f -name "*.py" | while IFS= read -r file; do 5 | if grep -qE '^\s*(import|from)\s+numpy' "$file" && grep -qE '^\s*(import|from)\s+matplotlib' "$file"; then 6 | echo "$file" 7 | fi 8 | done -------------------------------------------------------------------------------- /07_session/dataviz_howto.txt: -------------------------------------------------------------------------------- 1 | In order to use the app dataviz.html execute the following in this folder: 2 | 3 | python -m http.server 8000 4 | 5 | Then access the dataviz.html app in the browser via: 6 | 7 | http://localhost:8000/dataviz.html 8 | 9 | *** 10 | 11 | dataviz_ref.html relies on a remote CSV file which might lead to errors locally. -------------------------------------------------------------------------------- /10_options/mcdxa/exceptions.py: -------------------------------------------------------------------------------- 1 | class OptionPricingError(Exception): 2 | """Base exception for option pricing errors.""" 3 | pass 4 | 5 | class ModelError(OptionPricingError): 6 | """Error in model configuration or simulation.""" 7 | pass 8 | 9 | class PayoffError(OptionPricingError): 10 | """Error in payoff definition or evaluation.""" 11 | pass -------------------------------------------------------------------------------- /10_options/prompts/prompt.txt: -------------------------------------------------------------------------------- 1 | I want to create a Python package to flexibly price European and American options with arbitrary payoffs based on Monte Carlo simulation. The package shall be orthogonal and shall separate core functionalities in separate files/modules. The valuation shall primarily be done through MCS. Provide a concise outline and structure for the implementation of this project. -------------------------------------------------------------------------------- /10_options/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pytest 4 | 5 | # allow tests to import the top-level package 6 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 7 | 8 | @pytest.fixture(autouse=True) 9 | def fixed_seed(monkeypatch): 10 | """Fix the RNG seed for reproducible Monte Carlo tests.""" 11 | import numpy as np 12 | rng = np.random.default_rng(12345) 13 | monkeypatch.setattr(np.random, 'default_rng', lambda *args, **kwargs: rng) 14 | yield 15 | -------------------------------------------------------------------------------- /05_session/10_q_and_ai_prompt.txt: -------------------------------------------------------------------------------- 1 | Based on their most recent 10-Q filings (if possible, access them directly on sec.gov), derive the major financial/performance metrics for the following companies: 2 | 3 | AAPL,MSFT,AMZN,NFLX,META,GOOG,INTC,AMD,NVDA,GE,GS,BAC,JPM,MS 4 | 5 | 6 | For the following companies, research their current position, business strategy and product offerings with regard to AI adoption and integration. Create a ranking for both the tech companies and the financial institutions with regard to their AI competitive position. 7 | 8 | AAPL,MSFT,AMZN,NFLX,META,GOOG,INTC,AMD,NVDA,GE,GS,BAC,JPM,MS -------------------------------------------------------------------------------- /15_session/README.md: -------------------------------------------------------------------------------- 1 | # Algorithmic Trading Bootcamp Registration 2 | 3 | This is a simple web application to register for the Free Algorithmic Trading Bootcamp. It serves a static landing page and handles registration via a Python Flask backend, storing submissions in a SQLite database and appending to a CSV file. 4 | 5 | ## Requirements 6 | 7 | - Python 3.x 8 | - Flask 9 | 10 | Install dependencies: 11 | ```bash 12 | pip install -r requirements.txt 13 | ``` 14 | 15 | ## Running 16 | 17 | ```bash 18 | python app.py 19 | ``` 20 | 21 | The app will run on http://localhost:5000. 22 | 23 | ## Data Storage 24 | 25 | - Registrations are saved to `registrations.db` (SQLite3). 26 | - Each registration is appended to `registrations.csv`. 27 | -------------------------------------------------------------------------------- /04_session/_store/strat_perplexity_test.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import matplotlib.pyplot as plt 3 | 4 | # Load the price data 5 | df = pd.read_csv('aia_eod_data.csv', index_col='Date', parse_dates=True) 6 | 7 | # Define long and short portfolios 8 | longs = ['NVDA', 'AAPL', 'META'] 9 | shorts = ['NFLX', 'MS', 'INTC'] 10 | 11 | # Calculate daily returns 12 | returns = df[longs + shorts].pct_change().dropna() 13 | 14 | # Compute portfolio daily returns: equally weighted long-short 15 | portfolio_returns = returns[longs].mean(axis=1) - returns[shorts].mean(axis=1) 16 | 17 | # Calculate cumulative returns 18 | cumulative_returns = (1 + portfolio_returns).cumprod() 19 | 20 | # Plot the cumulative return 21 | plt.figure(figsize=(12, 6)) 22 | plt.plot(cumulative_returns.index, cumulative_returns.values, label='Long-Short Portfolio') 23 | plt.xlabel('Date') 24 | plt.ylabel('Cumulative Return') 25 | plt.title('Long-Short Portfolio Cumulative Return Over Time') 26 | plt.legend() 27 | plt.grid(True) 28 | plt.tight_layout() 29 | plt.show() 30 | -------------------------------------------------------------------------------- /10_options/tests/test_monte_carlo.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import pytest 4 | 5 | from mcdxa.monte_carlo import price_mc 6 | from mcdxa.models import BSM 7 | from mcdxa.payoffs import CallPayoff 8 | 9 | 10 | def test_price_mc_zero_volatility(): 11 | model = BSM(r=0.05, sigma=0.0, q=0.0) 12 | payoff = CallPayoff(strike=100.0) 13 | # With zero volatility, the payoff is deterministic and matches BSM analytic 14 | price, stderr = price_mc(payoff, model, S0=100.0, T=1.0, r=0.05, n_paths=50, n_steps=1) 15 | from mcdxa.analytics import bsm_price 16 | expected = bsm_price(100.0, 100.0, 1.0, 0.05, 0.0, option_type='call') 17 | assert stderr == pytest.approx(0.0, abs=1e-12) 18 | assert price == pytest.approx(expected, rel=1e-6) 19 | 20 | 21 | def test_price_mc_in_the_money(): 22 | model = BSM(r=0.0, sigma=0.0, q=0.0) 23 | payoff = CallPayoff(strike=50.0) 24 | price, stderr = price_mc(payoff, model, S0=100.0, T=1.0, r=0.0, n_paths=10, n_steps=1) 25 | # deterministic S=100, payoff=50, no discount 26 | assert stderr == 0.0 27 | assert price == pytest.approx(50.0) 28 | -------------------------------------------------------------------------------- /10_options/mcdxa/analytics.py: -------------------------------------------------------------------------------- 1 | # core pricing functions 2 | from .bsm import norm_cdf, bsm_price 3 | from .merton import merton_price 4 | from .heston import heston_price 5 | from .bates import simulate_bates, bates_price 6 | 7 | # Test script with provided parameters 8 | if __name__ == "__main__": 9 | # Model parameters 10 | S0 = 100.0 # Initial stock price 11 | K = 100.0 # Strike price 12 | T = 1.0 # Time to maturity (1 year) 13 | r = 0.05 # Risk-free rate 14 | q = 0.0 # Dividend yield 15 | kappa = 2.0 # Mean reversion rate 16 | theta = 0.04 # Long-term variance 17 | xi = 0.2 # Volatility of variance 18 | rho = -0.7 # Correlation 19 | v0 = 0.02 # Initial variance 20 | 21 | # Calculate the call option price with a finite integration limit 22 | for K in [50, 75, 100, 125, 150]: 23 | call_price = heston_price( 24 | S0, K, T, r, 25 | kappa, theta, xi, rho, v0, 26 | q=q, 27 | integration_limit=50 28 | ) 29 | print(f"European Call Option Price (Heston Model): {call_price:.4f}") 30 | -------------------------------------------------------------------------------- /10_options/mcdxa/monte_carlo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def price_mc(payoff, model, S0: float, T: float, r: float, 5 | n_paths: int, n_steps: int = 1, 6 | rng: np.random.Generator = None) -> tuple: 7 | """ 8 | Generic Monte Carlo pricer. 9 | 10 | Args: 11 | payoff (callable): Payoff function on terminal prices. 12 | model: Model instance with a simulate method. 13 | S0 (float): Initial asset price. 14 | T (float): Time to maturity. 15 | r (float): Risk-free rate. 16 | n_paths (int): Number of Monte Carlo paths. 17 | n_steps (int): Number of time steps per path. 18 | rng (np.random.Generator, optional): Random generator. 19 | 20 | Returns: 21 | price (float): Discounted Monte Carlo price. 22 | stderr (float): Standard error of the estimate. 23 | """ 24 | paths = model.simulate(S0, T, n_paths, n_steps, rng=rng) 25 | ST = paths[:, -1] 26 | payoffs = payoff(ST) 27 | discounted = np.exp(-r * T) * payoffs 28 | price = discounted.mean() 29 | stderr = discounted.std(ddof=1) / np.sqrt(n_paths) 30 | return price, stderr -------------------------------------------------------------------------------- /10_options/prompts/user_prompts_5.txt: -------------------------------------------------------------------------------- 1 | Add to the mcdxa package functionality for custom payoffs in the form, for example, of max(S ** 0.5 - K, 0) or max(K - S ** 0.5, 0). 2 | 3 | Can this new custom payoff be used for both European and American options? If so, add appropriate tests. Also add a test for "plain vanilla custom payoffs" and compare the resulting MCS values to the BSM ones (within a broader range). 4 | 5 | Create scripts/mcdxa.ipynb and use this notebook to show in detail how to use the mcdxa package based on a comprehensive set of examples. Also include visualizations where appropriate. 6 | 7 | In the notebook, there are multiple line breaks missing. Every line must end with "\n". 8 | 9 | To keep the package consistent, change the model name "MertonJumpDiffusion" to just "Merton". 10 | 11 | Generate a README.md that provides a detailed overview and explanation of the mcdxa package. Include our logo (to be found at https://hilpisch.com/tpq_logo.png). Also include our company The Python Quants GmbH and a copyright. 12 | 13 | *** 14 | 15 | Change the mcdxa package repo/folder so that it becomes easily installable via pip (pip install git+https://github.com/yhilpisch/mcdxa.git). 16 | 17 | -------------------------------------------------------------------------------- /10_options/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | import pytest 4 | 5 | from mcdxa.models import BSM, Heston, Merton 6 | 7 | 8 | def test_bsm_deterministic_growth(): 9 | model = BSM(r=0.05, sigma=0.0, q=0.0) 10 | paths = model.simulate(S0=100.0, T=1.0, n_paths=3, n_steps=4) 11 | # With zero volatility and zero dividend, S = S0 * exp(r * t) 12 | t_grid = np.linspace(0, 1.0, 5) 13 | expected = 100.0 * np.exp(0.05 * t_grid) 14 | for p in paths: 15 | assert np.allclose(p, expected) 16 | 17 | 18 | def test_heston_nonnegative_and_shape(): 19 | model = Heston(r=0.03, kappa=1.0, theta=0.04, xi=0.2, rho=0.0, v0=0.04, q=0.0) 20 | paths = model.simulate(S0=100.0, T=1.0, n_paths=10, n_steps=5) 21 | assert paths.shape == (10, 6) 22 | assert np.all(paths >= 0) 23 | 24 | 25 | def test_mjd_jump_effect(): 26 | # With high jump intensity and zero diffusion, expect jumps 27 | model = Merton(r=0.0, sigma=0.0, lam=10.0, mu_j=0.0, sigma_j=0.0, q=0.0) 28 | paths = model.simulate(S0=1.0, T=1.0, n_paths=1000, n_steps=1) 29 | # With zero jump size variance and mu_j=0, jumps yield Y=1, so S should equal S0 30 | assert paths.shape == (1000, 2) 31 | assert np.allclose(paths[:, -1], 1.0) 32 | -------------------------------------------------------------------------------- /10_options/tests/test_custom_payoff.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from mcdxa.payoffs import CustomPayoff 5 | 6 | 7 | def test_custom_payoff_requires_callable(): 8 | with pytest.raises(TypeError): 9 | CustomPayoff(123) 10 | 11 | 12 | @pytest.mark.parametrize("values, K, func, expected", [ 13 | # call-style: max(sqrt(S) - K, 0) 14 | (np.array([0, 1, 4, 9, 16]), 3, 15 | lambda s: np.maximum(np.sqrt(s) - 3, 0), 16 | np.maximum(np.sqrt(np.array([0, 1, 4, 9, 16])) - 3, 0)), 17 | # put-style: max(K - sqrt(S), 0) 18 | (np.array([0, 1, 4, 9, 16]), 3, 19 | lambda s: np.maximum(3 - np.sqrt(s), 0), 20 | np.maximum(3 - np.sqrt(np.array([0, 1, 4, 9, 16])), 0)), 21 | ]) 22 | def test_custom_payoff_terminal(values, K, func, expected): 23 | payoff = CustomPayoff(func) 24 | result = payoff(values) 25 | assert np.allclose(result, expected) 26 | 27 | 28 | def test_custom_payoff_on_paths(): 29 | # values as paths: terminal prices in last column 30 | paths = np.array([[1, 4, 9], [16, 25, 36]]) 31 | K = 5 32 | payoff = CustomPayoff(lambda s: np.maximum(np.sqrt(s) - K, 0)) 33 | # terminal prices are 9 and 36 34 | expected = np.maximum(np.sqrt(np.array([9, 36])) - 5, 0) 35 | result = payoff(paths) 36 | assert np.allclose(result, expected) 37 | -------------------------------------------------------------------------------- /10_options/mcdxa/pricers/european.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ..monte_carlo import price_mc 4 | 5 | 6 | class EuropeanPricer: 7 | """ 8 | Monte Carlo pricer for European options. 9 | 10 | Attributes: 11 | model: Asset price model with simulate method. 12 | payoff: Payoff callable. 13 | n_paths (int): Number of simulation paths. 14 | n_steps (int): Number of time steps per path. 15 | rng: numpy random generator. 16 | """ 17 | def __init__(self, model, payoff, n_paths: int = 100_000, 18 | n_steps: int = 1, seed: int = None): 19 | self.model = model 20 | self.payoff = payoff 21 | self.n_paths = n_paths 22 | self.n_steps = n_steps 23 | self.rng = None if seed is None else np.random.default_rng(seed) 24 | 25 | def price(self, S0: float, T: float, r: float) -> tuple: 26 | """ 27 | Price the option via Monte Carlo simulation. 28 | 29 | Args: 30 | S0 (float): Initial asset price. 31 | T (float): Time to maturity. 32 | r (float): Risk-free rate. 33 | 34 | Returns: 35 | tuple: (price, stderr) 36 | """ 37 | return price_mc( 38 | self.payoff, self.model, S0, T, r, 39 | self.n_paths, self.n_steps, rng=self.rng 40 | ) 41 | -------------------------------------------------------------------------------- /10_options/tests/test_payoffs.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from mcdxa.payoffs import ( 5 | CallPayoff, PutPayoff, 6 | AsianCallPayoff, AsianPutPayoff, 7 | LookbackCallPayoff, LookbackPutPayoff, 8 | ) 9 | 10 | 11 | @pytest.mark.parametrize("payoff_cls, spot, strike, expected", [ 12 | (CallPayoff, np.array([50, 100, 150]), 100, np.array([0, 0, 50])), 13 | (PutPayoff, np.array([50, 100, 150]), 100, np.array([50, 0, 0])), 14 | ]) 15 | def test_vanilla_payoff(payoff_cls, spot, strike, expected): 16 | payoff = payoff_cls(strike) 17 | result = payoff(spot) 18 | assert np.allclose(result, expected) 19 | 20 | 21 | @pytest.mark.parametrize("payoff_cls, path, expected", [ 22 | (AsianCallPayoff, np.array([[1, 3, 5], [2, 2, 2]]), np.array([max((1+3+5)/3 - 3, 0), max((2+2+2)/3 - 3, 0)])), 23 | (AsianPutPayoff, np.array([[1, 3, 5], [2, 2, 2]]), np.array([max(3 - (1+3+5)/3, 0), max(3 - (2+2+2)/3, 0)])), 24 | (LookbackCallPayoff, np.array([[1, 4], [3, 2]]), np.array([max(4-2, 0), max(3-2, 0)])), 25 | (LookbackPutPayoff, np.array([[1, 4], [3, 2]]), np.array([max(2-1, 0), max(2-3, 0)])), 26 | ]) 27 | def test_path_payoff(payoff_cls, path, expected): 28 | strike = 3 if 'Asian' in payoff_cls.__name__ else 2 29 | payoff = payoff_cls(strike) 30 | result = payoff(path) 31 | assert np.allclose(result, expected) 32 | -------------------------------------------------------------------------------- /15_session/revert_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # revert_deploy.sh — undo deployment performed by deploy.sh 4 | # Usage: ./revert_deploy.sh 5 | 6 | set -euo pipefail 7 | 8 | if [ "$#" -ne 1 ]; then 9 | echo "Usage: $0 " 10 | exit 1 11 | fi 12 | 13 | SERVER_IP=$1 14 | SSH_DEST=root@${SERVER_IP} 15 | SERVICE_NAME=bootcamp 16 | APP_DIR=/opt/bootcamp 17 | DOMAIN=genai.tpq.io 18 | 19 | echo "=== Reverting deployment on ${SERVER_IP} ===" 20 | ssh ${SSH_DEST} << UNDO 21 | set -euo pipefail 22 | 23 | # Stop and disable the Gunicorn service 24 | systemctl stop ${SERVICE_NAME}.service || true 25 | systemctl disable ${SERVICE_NAME}.service || true 26 | rm -f /etc/systemd/system/${SERVICE_NAME}.service 27 | systemctl daemon-reload 28 | 29 | # Remove nginx site configuration and restore default 30 | rm -f /etc/nginx/sites-enabled/${SERVICE_NAME} 31 | rm -f /etc/nginx/sites-available/${SERVICE_NAME} 32 | ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || true 33 | nginx -t 34 | systemctl reload nginx 35 | 36 | # Revoke and delete the SSL certificate 37 | certbot delete --noninteractive --cert-name ${DOMAIN} || true 38 | 39 | # Remove application directory and virtualenv 40 | rm -rf ${APP_DIR} 41 | 42 | # (Optional) Remove installed packages: 43 | # apt remove --purge -y python3-venv python3-pip nginx certbot python3-certbot-nginx || true 44 | # apt autoremove -y || true 45 | 46 | echo "=== Reversion complete on ${SERVER_IP} ===" 47 | UNDO 48 | -------------------------------------------------------------------------------- /10_options/test.md: -------------------------------------------------------------------------------- 1 | # Test Suite Highlights 2 | 3 | This document summarizes the comprehensive pytest-based test suite for the option_pricing package, following the project outline. 4 | 5 | ``` 6 | tests/ 7 | ├── conftest.py # test fixtures (fixed RNG seed) 8 | ├── test_models.py # model simulation unit tests (BSM, Heston, MJD) 9 | ├── test_payoffs.py # payoff function tests (vanilla & path-dependent) 10 | ├── test_monte_carlo.py # core Monte Carlo engine tests 11 | ├── test_pricers_european.py # EuropeanPricer vs analytic BSM benchmarks 12 | └── test_pricers_american.py # American pricers (LSM MC vs CRR binomial) 13 | ``` 14 | 15 | **Key test scenarios:** 16 | 17 | - **Fixed RNG seed** via `conftest.py` for reproducible MC results. 18 | - **BSM zero-volatility**: exact deterministic growth of paths. 19 | - **Heston simulation**: non-negative paths of correct shape under Euler truncation. 20 | - **Merton jump-diffusion**: pure jump tests with zero jump size variance. 21 | - **Vanilla payoffs**: call/put payoffs on sample spot arrays. 22 | - **Path-dependent payoffs**: arithmetic Asian & lookback payoffs on synthetic paths. 23 | - **Monte Carlo engine**: zero-volatility, in-the-money call to verify price & stderr. 24 | - **EuropeanPricer**: matches analytic BSM price when volatility = 0. 25 | - **American pricers**: 26 | - CRR binomial call with no volatility → immediate exercise intrinsic value. 27 | - Longstaff-Schwartz put with no volatility → immediate exercise intrinsic value, zero stderr. 28 | 29 | **Running the tests:** 30 | 31 | ```bash 32 | pytest -q 33 | ``` 34 | -------------------------------------------------------------------------------- /10_options/mcdxa/bsm.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | def norm_cdf(x: float) -> float: 4 | """Standard normal cumulative distribution function.""" 5 | return 0.5 * (1.0 + math.erf(x / math.sqrt(2))) 6 | 7 | def bsm_price( 8 | S0: float, 9 | K: float, 10 | T: float, 11 | r: float, 12 | sigma: float, 13 | q: float = 0.0, 14 | option_type: str = "call", 15 | ) -> float: 16 | """ 17 | Black-Scholes-Merton (BSM) price for European call or put option. 18 | 19 | Parameters: 20 | - S0: Spot price 21 | - K: Strike price 22 | - T: Time to maturity (in years) 23 | - r: Risk-free interest rate 24 | - sigma: Volatility of the underlying asset 25 | - q: Dividend yield 26 | - option_type: 'call' or 'put' 27 | 28 | Returns: 29 | - price: Option price (call or put) 30 | """ 31 | # handle zero volatility (degenerate case) 32 | if sigma <= 0 or T <= 0: 33 | # forward intrinsic value, floored at zero 34 | forward = S0 * math.exp(-q * T) - K * math.exp(-r * T) 35 | if option_type == "call": 36 | return max(forward, 0.0) 37 | else: 38 | return max(-forward, 0.0) 39 | 40 | d1 = (math.log(S0 / K) + (r - q + 0.5 * sigma ** 2) * T) / \ 41 | (sigma * math.sqrt(T)) 42 | d2 = d1 - sigma * math.sqrt(T) 43 | if option_type == "call": 44 | return S0 * math.exp(-q * T) * norm_cdf(d1) - K * math.exp(-r * T) * norm_cdf(d2) 45 | elif option_type == "put": 46 | return K * math.exp(-r * T) * norm_cdf(-d2) - S0 * math.exp(-q * T) * norm_cdf(-d1) 47 | else: 48 | raise ValueError("option_type must be 'call' or 'put'") 49 | -------------------------------------------------------------------------------- /08_session/user_prompts.txt: -------------------------------------------------------------------------------- 1 | Create a Python script that prompts the user for a ticker/symbol (such as SPY) and retrieves data for it via the yfinance Python package, say for the last three years. The data shall be visualized. 2 | 3 | Now ask the user for two SMA values (fast, slow) and visualize those SMAs as well. 4 | 5 | Now allow for the definition of the number of months for which the data is retrieved, like 48 months for the last 4 years. 6 | 7 | Plot the SMAs as dashed lines with lw=1. Add upward (downward) triangles whenever the fast SMA crosses the slow one from above (below). 8 | 9 | Implement a backtest for the crossover trading strategy. Start at the date where there is full data for the first time (and not at the first crossover). Show the results in the figure (benchmark + strategy). 10 | 11 | Allow for both long-only and long-short trading strategies. Prompt the user for it. 12 | 13 | Change the filename to sma_backtest.py. Change the color palette to something like coolwarm (red/blue). And display the performance number in the performance chart. Make sure that there is no overlap. 14 | 15 | Make sure that the colors in all subplots are according to the coolwarm (blue/red) color map! 16 | 17 | Change back the color of the downwards triangles to gree. And add a frame to the box with the performance results. 18 | 19 | What options do we have to migrate the Python script to one with a Web interface, i.e. so that I can us it in the browser? 20 | 21 | We go with option 1. Implement it accordingly in a new file that I can run. 22 | 23 | There are no new files. And I asked you to use streamlit to port the Python app to the browser. Please do so and call the new file sma_browser.py. 24 | 25 | -------------------------------------------------------------------------------- /09_session/user_prompts.txt: -------------------------------------------------------------------------------- 1 | Create a Python script that retrieves for multiple tickers EOD data via yfinance and visualizes that data. Set as default ticker symbols AAPL, MSFT, NFLX. 2 | 3 | Add the option to normalize the data for plotting. 4 | 5 | Use "python" instead of "python3" when testing the code. Add a grid to the plot. 6 | 7 | Now copy the core functionality to a new Python file. Add functionality to derive the maximal Sharpe portfolio composition as well as the minimum variance portfolio. Report the detailed statistics. 8 | 9 | KeyError: 'Close' 10 | 11 | Now set as default that short sales are not allowed. But also provide an option to allow for short sales. 12 | 13 | Now add also the visualization of the (normalized) time series data (as before). And add a visualization of the two optimal portfolios' weights. 14 | 15 | Change the weights figure to pie charts when short sales are not allowed. 16 | 17 | Change the all the color maps to coolwarm (blue to red). 18 | 19 | Change it back to the default color map from before. 20 | 21 | Now add a figure which shows Monte Carlso simulated portfolios in risk-return space. Allow for the specification of the number of portfolios to be simulated (default: 500). Also show the minimum variance and optimal Sharpe portfolio in that third figure. 22 | 23 | Now redesign the code and migrate it to a streamlit application in a new Python file. 24 | 25 | Make sure that all number are rounded to a maximum of 4 digits. And change the header "Normalized Price Series" to "Price Series". And set the default for "normalize prices" to True. 26 | 27 | When allowing for short sales, make sure that the bar chart is a regular one and not a stacked one. 28 | 29 | -------------------------------------------------------------------------------- /05_session/backtest_ai_perplexity.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | # Load CSV 6 | data = pd.read_csv('aia_eod_data.csv', parse_dates=['Date'], index_col='Date') 7 | 8 | # Select relevant tickers 9 | tickers = ['NVDA', 'JPM', 'GE', 'MS'] 10 | prices = data[tickers] 11 | 12 | # Filter last 3 years from last available date 13 | end_date = prices.index.max() 14 | start_date = end_date - pd.DateOffset(years=3) 15 | prices = prices.loc[start_date:end_date] 16 | 17 | # Calculate daily returns 18 | returns = prices.pct_change().dropna() 19 | 20 | # Portfolio weights 21 | weights = {'NVDA': 0.25, 'JPM': 0.25, 'GE': -0.25, 'MS': -0.25} 22 | 23 | # Calculate portfolio returns 24 | portfolio_returns = (returns * pd.Series(weights)).sum(axis=1) 25 | 26 | # Calculate cumulative returns 27 | cumulative_returns = (1 + portfolio_returns).cumprod() 28 | 29 | # Performance metrics 30 | total_return = cumulative_returns[-1] - 1 31 | annualized_vol = portfolio_returns.std() * np.sqrt(252) 32 | rolling_max = cumulative_returns.cummax() 33 | drawdown = (cumulative_returns - rolling_max) / rolling_max 34 | max_drawdown = drawdown.min() 35 | sharpe_ratio = (portfolio_returns.mean() / portfolio_returns.std()) * np.sqrt(252) 36 | 37 | # Output results 38 | print(f'Cumulative Return: {total_return:.2%}') 39 | print(f'Annualized Volatility: {annualized_vol:.2%}') 40 | print(f'Max Drawdown: {max_drawdown:.2%}') 41 | print(f'Sharpe Ratio: {sharpe_ratio:.2f}') 42 | 43 | # Plot portfolio value 44 | plt.figure(figsize=(12,6)) 45 | plt.plot(cumulative_returns, label='Long/Short Portfolio') 46 | plt.title('3-Year Backtest: Long NVDA/JPM, Short GE/MS') 47 | plt.xlabel('Date') 48 | plt.ylabel('Cumulative Return') 49 | plt.legend() 50 | plt.grid(True) 51 | plt.show() 52 | -------------------------------------------------------------------------------- /10_options/prompts/user_prompts_1.txt: -------------------------------------------------------------------------------- 1 | I want to create a Python package to flexibly price European and American options with arbitrary payoffs based on Monte Carlo simulation. The package shall be orthogonal and shall separate core functionalities in separate files/modules. The valuation shall primarily be done through MCS. Provide a concise outline and structure for the implementation of this project. 2 | 3 | Write the outline in markdown in a new file outline.md. 4 | 5 | Start with the first implementations so that we can value plain vanilla European put and call options in the BSM 1973 models. 6 | 7 | No, don't do that — stop for now. 8 | 9 | When executing Python code, use "python" instead of "python3". Implement a script that benchmarks the European option pricing based on Monte Carlo against the analytical BSM 1973 values. 10 | 11 | Update the benchmark to include for both options ATM/ITM cases. Present the results in a table. Including absolute and % valuation errors in each case. 12 | 13 | Also add the respective OTM cases. 14 | 15 | Now add the pricing of American options to the package. For the benchmarking of the ATM/ITM/OTM option generate benchmark values based on the CRR 1979 binomial option pricing model. 16 | 17 | You should implement a benchmark for the European options prices against the analytical ones AND for the American options prices from MCS against the American option values from the CRR 1979 model. In both cases for puts and calls ATM/ITM/OTM. 18 | 19 | Now add as an additional model for simulating paths the Merton jump diffusion model. 20 | 21 | For the benchmarking of the jump diffusion model, implement pricing formulas for that model based on the characteristic function. Amend the benchmarking file to include bencharks for European put/call ATM/ITM/OTM options in that model. 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifics 2 | *.swp 3 | *.cfg 4 | _build/ 5 | .DS_Store 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | -------------------------------------------------------------------------------- /10_options/tests/test_pricers_european.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from mcdxa.models import BSM 4 | from mcdxa.payoffs import CallPayoff, PutPayoff, CustomPayoff 5 | from mcdxa.pricers.european import EuropeanPricer 6 | from mcdxa.analytics import bsm_price 7 | 8 | 9 | @pytest.mark.parametrize("opt_type", ["call", "put"]) 10 | def test_european_pricer_matches_bsm(opt_type): 11 | S0, K, T, r, sigma = 100.0, 100.0, 1.0, 0.05, 0.0 12 | model = BSM(r, sigma) 13 | payoff = CallPayoff(K) if opt_type == "call" else PutPayoff(K) 14 | pricer = EuropeanPricer(model, payoff, n_paths=20, n_steps=1, seed=42) 15 | price_mc, stderr = pricer.price(S0, T, r) 16 | price_bs = bsm_price(S0, K, T, r, sigma, option_type=opt_type) 17 | # Zero volatility: MC should produce deterministic result equal to analytic 18 | assert stderr == pytest.approx(0.0) 19 | assert price_mc == pytest.approx(price_bs) 20 | 21 | 22 | @pytest.mark.parametrize("opt_type,S0,K", [ 23 | ("call", 80.0, 100.0), 24 | ("put", 80.0, 100.0), 25 | ("call", 100.0, 80.0), 26 | ("put", 100.0, 80.0), 27 | ]) 28 | def test_european_pricer_custom_plain_vanilla(opt_type, S0, K): 29 | """ 30 | Test that CustomPayoff can express vanilla call/put and matches analytic BSM for zero volatility. 31 | """ 32 | T, r, sigma = 1.0, 0.05, 0.0 33 | model = BSM(r, sigma) 34 | # define equivalent vanilla payoff via CustomPayoff 35 | if opt_type == "call": 36 | func = lambda s: np.maximum(s - K, 0) 37 | else: 38 | func = lambda s: np.maximum(K - s, 0) 39 | payoff = CustomPayoff(func) 40 | pricer = EuropeanPricer(model, payoff, n_paths=20, n_steps=1, seed=42) 41 | price_mc, stderr = pricer.price(S0, T, r) 42 | price_bs = bsm_price(S0, K, T, r, sigma, option_type=opt_type) 43 | assert stderr == pytest.approx(0.0) 44 | assert price_mc == pytest.approx(price_bs) 45 | -------------------------------------------------------------------------------- /03_session/fundamentals_prompts.txt: -------------------------------------------------------------------------------- 1 | You are a data analyst AI. You are provided with a CSV file containing a single set of fundamental metrics for multiple entities. 2 | Please: 3 | * Examine the data for patterns, trends, or notable differences among the entities. 4 | * Identify which entities appear strongest or weakest based on the available metrics. 5 | * Highlight any outliers or unusual values. 6 | * Compare the entities across all metrics and summarize key insights. 7 | * Present your findings in a clear, structured format with bullet points and concise explanations. 8 | * Base your analysis solely on the provided data, without using any external information. 9 | 10 | Based on your previous analysis of the provided fundamental data for multiple entities, please construct a hypothetical portfolio using long and short positions. 11 | Specifically: 12 | * Select a subset of entities to go long (buy) and a subset to go short (sell), using your earlier insights and comparisons. 13 | * Clearly explain the criteria and reasoning for each selection (e.g., relative strength, weakness, or outlier status in key metrics). 14 | * Specify the recommended weight or proportion for each long and short position, ensuring the portfolio is balanced. 15 | * Summarize the expected rationale or potential advantage of this portfolio construction, based solely on the fundamental data. 16 | * Present your recommendations in a clear, structured format (such as a table or bullet points). 17 | * Do not use any price or market data, and do not reference external information—base your portfolio solely on the provided fundamental metrics and your prior analysis. 18 | 19 | *** 20 | 21 | Context window problem with Clause/DeepSeek 22 | 23 | https://www.perplexity.ai/search/why-can-a-simple-csv-file-with-913XA4UITvi1wDhJxdb6sA#0 24 | 25 | *** 26 | 27 | All price and fundamentals data from https://eodhd.com/r/?ref=X8R79ISB. 28 | 29 | *** 30 | 31 | To run the TypeScript (.tsx) app, you need to install additional tools/packages. See: 32 | 33 | https://www.perplexity.ai/search/what-is-a-tsx-file-and-how-do-0B8MYmFSRzyfMKZARntqGg 34 | -------------------------------------------------------------------------------- /10_options/tests/test_bates.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from mcdxa.models import Bates 7 | from mcdxa.bates import simulate_bates, bates_price 8 | from mcdxa.analytics import heston_price 9 | 10 | 11 | def test_simulate_bates_shape_and_values(): 12 | # basic shape and initial value 13 | rng = np.random.default_rng(123) 14 | paths = simulate_bates( 15 | S0=100.0, T=1.0, r=0.05, 16 | kappa=2.0, theta=0.04, xi=0.2, rho=0.0, v0=0.02, 17 | lam=0.3, mu_j=-0.1, sigma_j=0.2, q=0.0, 18 | n_paths=5, n_steps=3, rng=rng 19 | ) 20 | # Expect shape (n_paths, n_steps+1) 21 | assert paths.shape == (5, 4) 22 | # initial column should equal S0 23 | assert np.allclose(paths[:, 0], 100.0) 24 | 25 | 26 | @pytest.mark.parametrize("opt_type", ["call", "put"]) 27 | def test_bates_price_zero_jumps_equals_heston(opt_type): 28 | # When lam=0, Bates reduces to Heston model 29 | S0, K, T, r = 100.0, 100.0, 1.0, 0.05 30 | kappa, theta, xi, rho, v0 = 2.0, 0.04, 0.2, -0.5, 0.03 31 | # zero jumps 32 | p_bates = bates_price( 33 | S0, K, T, r, 34 | kappa, theta, xi, rho, v0, 35 | lam=0.0, mu_j=-0.1, sigma_j=0.2, 36 | q=0.0, option_type=opt_type 37 | ) 38 | p_heston = heston_price( 39 | S0, K, T, r, 40 | kappa, theta, xi, rho, v0, 41 | q=0.0, option_type=opt_type 42 | ) 43 | assert p_bates == pytest.approx(p_heston, rel=1e-7) 44 | 45 | 46 | def test_bates_put_call_parity(): 47 | # Test put-call parity for Bates 48 | S0, K, T, r = 100.0, 100.0, 1.0, 0.03 49 | params = dict( 50 | kappa=1.5, theta=0.05, xi=0.3, rho=0.0, v0=0.04, 51 | lam=0.2, mu_j=-0.05, sigma_j=0.15, q=0.0, 52 | ) 53 | call = bates_price(S0, K, T, r, **params, option_type="call") 54 | put = bates_price(S0, K, T, r, **params, option_type="put") 55 | # Parity: call - put = S0*exp(-qT) - K*exp(-rT) 56 | expected = S0 * math.exp(-params['q'] * T) - K * math.exp(-r * T) 57 | assert (call - put) == pytest.approx(expected, rel=1e-7) 58 | -------------------------------------------------------------------------------- /06_session/fibonacci.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | fibonacci.py 4 | 5 | Module providing an efficient iterative algorithm to compute Fibonacci numbers. 6 | 7 | Author: The Python Quants GmbH 8 | Copyright (c) 2025 The Python Quants GmbH 9 | """ 10 | 11 | __author__ = "The Python Quants GmbH" 12 | __copyright__ = "Copyright (c) 2025 The Python Quants GmbH" 13 | __version__ = "1.0.0" 14 | 15 | def fib_it_py(n): 16 | """ 17 | Calculate the n-th Fibonacci number using an iterative algorithm. 18 | 19 | Fibonacci sequence: 20 | F(0) = 0 21 | F(1) = 1 22 | F(n) = F(n-1) + F(n-2) for n >= 2 23 | 24 | Parameters 25 | ---------- 26 | n : int 27 | Non-negative integer index of the desired Fibonacci number. 28 | 29 | Returns 30 | ------- 31 | int 32 | The n-th Fibonacci number. 33 | 34 | Raises 35 | ------ 36 | TypeError 37 | If n is not an integer. 38 | ValueError 39 | If n is a negative integer. 40 | """ 41 | if not isinstance(n, int): 42 | raise TypeError(f"n must be an integer, got {type(n).__name__}") 43 | if n < 0: 44 | raise ValueError(f"n must be non-negative, got {n}") 45 | 46 | x, y = 0, 1 47 | for _ in range(1, n + 1): 48 | x, y = y, x + y 49 | return x 50 | 51 | import unittest 52 | 53 | class TestFibItPy(unittest.TestCase): 54 | def test_zero(self): 55 | self.assertEqual(fib_it_py(0), 0) 56 | 57 | def test_one(self): 58 | self.assertEqual(fib_it_py(1), 1) 59 | 60 | def test_small_numbers(self): 61 | expected = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 62 | for i, val in enumerate(expected): 63 | self.assertEqual(fib_it_py(i), val) 64 | 65 | def test_larger_number(self): 66 | self.assertEqual(fib_it_py(20), 6765) 67 | 68 | def test_type_error(self): 69 | with self.assertRaises(TypeError): 70 | fib_it_py(3.14) 71 | 72 | def test_value_error(self): 73 | with self.assertRaises(ValueError): 74 | fib_it_py(-5) 75 | 76 | if __name__ == "__main__": 77 | unittest.main() 78 | -------------------------------------------------------------------------------- /08_session/extract_user_prompts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Extract all user prompts from a Codex session JSON file. 4 | 5 | Reads 'codex_session.json' by default and writes the text of every user 6 | input prompt into an output text file (default 'user_prompts.txt'), 7 | separating each prompt by a blank line. 8 | 9 | Usage: 10 | python3 extract_user_prompts.py [INPUT_JSON] [OUTPUT_TXT] 11 | """ 12 | 13 | import json 14 | import sys 15 | 16 | 17 | def extract_prompts(input_path): 18 | with open(input_path, 'r', encoding='utf-8') as f: 19 | session = json.load(f) 20 | prompts = [] 21 | for item in session.get('items', []): 22 | if item.get('role') == 'user': 23 | for entry in item.get('content', []): 24 | text = entry.get('text') 25 | if text: 26 | prompts.append(text) 27 | return prompts 28 | 29 | 30 | def main(): 31 | import argparse 32 | 33 | parser = argparse.ArgumentParser( 34 | description='Extract user prompts from Codex session JSON.' 35 | ) 36 | parser.add_argument( 37 | 'input_json', nargs='?', default='codex_session.json', 38 | help='Path to the session JSON file' 39 | ) 40 | parser.add_argument( 41 | 'output_txt', nargs='?', default='user_prompts.txt', 42 | help='Path to write extracted prompts' 43 | ) 44 | args = parser.parse_args() 45 | 46 | try: 47 | prompts = extract_prompts(args.input_json) 48 | except FileNotFoundError: 49 | print(f"Error: file not found: {args.input_json}", file=sys.stderr) 50 | sys.exit(1) 51 | except json.JSONDecodeError as e: 52 | print(f"Error: invalid JSON in {args.input_json}: {e}", file=sys.stderr) 53 | sys.exit(1) 54 | 55 | if not prompts: 56 | print(f"No user prompts found in {args.input_json}", file=sys.stderr) 57 | sys.exit(1) 58 | 59 | with open(args.output_txt, 'w', encoding='utf-8') as f: 60 | for p in prompts: 61 | f.write(p.rstrip()) 62 | f.write("\n\n") 63 | 64 | print(f"Wrote {len(prompts)} prompts to {args.output_txt}") 65 | 66 | 67 | if __name__ == '__main__': 68 | main() -------------------------------------------------------------------------------- /15_session/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, redirect, send_from_directory 2 | import sqlite3 3 | import os 4 | from datetime import datetime 5 | import csv 6 | 7 | DB_FILE = 'registrations.db' 8 | CSV_FILE = 'registrations.csv' 9 | 10 | app = Flask(__name__, static_folder='.', static_url_path='') 11 | 12 | def init_db(): 13 | conn = sqlite3.connect(DB_FILE) 14 | c = conn.cursor() 15 | c.execute(''' 16 | CREATE TABLE IF NOT EXISTS registrations ( 17 | id INTEGER PRIMARY KEY AUTOINCREMENT, 18 | first_name TEXT NOT NULL, 19 | last_name TEXT NOT NULL, 20 | email TEXT NOT NULL, 21 | timestamp TEXT NOT NULL 22 | ) 23 | ''') 24 | conn.commit() 25 | conn.close() 26 | 27 | def append_csv(first_name, last_name, email, timestamp): 28 | file_exists = os.path.exists(CSV_FILE) 29 | with open(CSV_FILE, 'a', newline='', encoding='utf-8') as csvfile: 30 | writer = csv.writer(csvfile) 31 | if not file_exists or os.stat(CSV_FILE).st_size == 0: 32 | writer.writerow(['first_name', 'last_name', 'email', 'timestamp']) 33 | writer.writerow([first_name, last_name, email, timestamp]) 34 | 35 | @app.route('/', methods=['GET']) 36 | def index(): 37 | return send_from_directory('.', 'index.html') 38 | 39 | @app.route('/register', methods=['POST']) 40 | def register(): 41 | first = request.form.get('first_name', '').strip() 42 | last = request.form.get('last_name', '').strip() 43 | email = request.form.get('email', '').strip() 44 | if not (first and last and email): 45 | return "Missing data", 400 46 | timestamp = datetime.utcnow().isoformat() 47 | conn = sqlite3.connect(DB_FILE) 48 | c = conn.cursor() 49 | c.execute( 50 | 'INSERT INTO registrations (first_name, last_name, email, timestamp) VALUES (?, ?, ?, ?)', 51 | (first, last, email, timestamp) 52 | ) 53 | conn.commit() 54 | conn.close() 55 | append_csv(first, last, email, timestamp) 56 | return redirect('/thankyou') 57 | 58 | @app.route('/thankyou', methods=['GET']) 59 | def thank_you(): 60 | return send_from_directory('.', 'thankyou.html') 61 | 62 | if __name__ == '__main__': 63 | init_db() 64 | app.run(host='0.0.0.0', port=5001) 65 | -------------------------------------------------------------------------------- /10_options/mcdxa/merton.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.integrate import quad 3 | import math 4 | 5 | def merton_price( 6 | S0: float, 7 | K: float, 8 | T: float, 9 | r: float, 10 | sigma: float, 11 | lam: float = 0.0, 12 | mu_j: float = 0.0, 13 | sigma_j: float = 0.0, 14 | q: float = 0.0, 15 | option_type: str = "call", 16 | integration_limit: float = 250, 17 | ) -> float: 18 | """ 19 | European option price under the Merton (1976) jump-diffusion model via Lewis (2001) 20 | single-integral formula. 21 | 22 | Parameters: 23 | - S0: Initial stock price 24 | - K: Strike price 25 | - T: Time to maturity (in years) 26 | - r: Risk-free interest rate 27 | - sigma: Volatility of the diffusion component 28 | - lam: Jump intensity (lambda) 29 | - mu_j: Mean of log jump size 30 | - sigma_j: Standard deviation of log jump size 31 | - q: Dividend yield 32 | - option_type: 'call' or 'put' 33 | - integration_limit: Upper bound for numerical integration 34 | 35 | Returns: 36 | - price: Price of the European option (call or put) 37 | """ 38 | def _char(u): 39 | # Jump-diffusion characteristic function of log-returns under risk-neutral measure 40 | kappa_j = math.exp(mu_j + 0.5 * sigma_j ** 2) - 1 41 | drift = r - q - lam * kappa_j - 0.5 * sigma ** 2 42 | return np.exp( 43 | (1j * u * drift - 0.5 * u ** 2 * sigma ** 2) * T 44 | + lam * T * (np.exp(1j * u * mu_j - 0.5 * 45 | u ** 2 * sigma_j ** 2) - 1) 46 | ) 47 | 48 | def _lewis_integrand(u): 49 | # Lewis (2001) integrand for call under jump-diffusion 50 | cf_val = _char(u - 0.5j) 51 | return 1.0 / (u ** 2 + 0.25) * (np.exp(1j * u * math.log(S0 / K)) * cf_val).real 52 | 53 | integral_value = quad(_lewis_integrand, 0, integration_limit)[0] 54 | call_price = S0 * np.exp(-q * T) - np.exp(-r * T) * \ 55 | np.sqrt(S0 * K) / np.pi * integral_value 56 | 57 | if option_type == "call": 58 | price = call_price 59 | elif option_type == "put": 60 | price = call_price - S0 * math.exp(-q * T) + K * math.exp(-r * T) 61 | else: 62 | raise ValueError("Option type must be 'call' or 'put'.") 63 | 64 | return max(price, 0.0) 65 | -------------------------------------------------------------------------------- /10_options/tests/test_pricers_american.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | 4 | from mcdxa.models import BSM 5 | from mcdxa.payoffs import CallPayoff, PutPayoff 6 | from mcdxa.pricers.american import AmericanBinomialPricer, LongstaffSchwartzPricer 7 | from mcdxa.payoffs import CustomPayoff 8 | 9 | 10 | @pytest.fixture(autouse=True) 11 | def fix_seed(monkeypatch): 12 | import numpy as np 13 | rng = np.random.default_rng(12345) 14 | monkeypatch.setattr(np.random, 'default_rng', lambda *args, **kwargs: rng) 15 | 16 | 17 | def test_crr_binomial_call_no_dividend_intrinsic(): 18 | model = BSM(r=0.05, sigma=0.0, q=0.0) 19 | K, S0, T = 100.0, 110.0, 1.0 20 | payoff = CallPayoff(K) 21 | pricer = AmericanBinomialPricer(model, payoff, n_steps=10) 22 | price = pricer.price(S0, T, r=0.05) 23 | assert price == pytest.approx(S0 - K) 24 | 25 | 26 | def test_crr_binomial_custom_call_intrinsic(): 27 | """Ensure CustomPayoff works with AmericanBinomialPricer for intrinsic call payoff.""" 28 | model = BSM(r=0.05, sigma=0.0, q=0.0) 29 | K, S0, T = 100.0, 110.0, 1.0 30 | payoff = CustomPayoff(lambda s: np.maximum(s - K, 0)) 31 | pricer = AmericanBinomialPricer(model, payoff, n_steps=10) 32 | price = pricer.price(S0, T, r=0.05) 33 | assert price == pytest.approx(S0 - K) 34 | 35 | 36 | def test_lsm_put_no_volatility_exercise(): 37 | model = BSM(r=0.0, sigma=0.0, q=0.0) 38 | K, S0, T = 100.0, 90.0, 1.0 39 | payoff = PutPayoff(K) 40 | pricer = LongstaffSchwartzPricer( 41 | model, payoff, n_paths=50, n_steps=5, seed=42) 42 | price, stderr = pricer.price(S0, T, r=0.0) 43 | # Zero vol: always exercise immediately, price equals intrinsic 44 | assert stderr == pytest.approx(0.0) 45 | assert price == pytest.approx(K - S0) 46 | 47 | 48 | def test_lsm_custom_put_no_volatility_exercise(): 49 | """Ensure CustomPayoff works with LongstaffSchwartzPricer for intrinsic put payoff.""" 50 | model = BSM(r=0.0, sigma=0.0, q=0.0) 51 | K, S0, T = 100.0, 90.0, 1.0 52 | payoff = CustomPayoff(lambda s: np.maximum(K - s, 0)) 53 | pricer = LongstaffSchwartzPricer( 54 | model, payoff, n_paths=50, n_steps=5, seed=42 55 | ) 56 | price, stderr = pricer.price(S0, T, r=0.0) 57 | # Zero vol: always exercise immediately, price equals intrinsic 58 | assert stderr == pytest.approx(0.0) 59 | assert price == pytest.approx(K - S0) 60 | -------------------------------------------------------------------------------- /15_session/thankyou.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Thank You - Algorithmic Trading Bootcamp 7 | 8 | 72 | 73 | 74 |
75 | 76 |

Algorithmic Trading Bootcamp

77 |
78 |
79 |

Thank You for Registering!

80 |

We have received your registration and will be in touch with the details soon. We look forward to seeing you at the bootcamp!

81 | Back to Home 82 |
83 |
84 | © 2023 TPQ Bootcamp. All rights reserved. 85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /04_session/benchmark_perplexity.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | 4 | import warnings 5 | warnings.simplefilter('ignore') 6 | 7 | # Portfolio metrics from previous analysis 8 | portfolio_metrics = { 9 | 'Total Return': 6.125, 10 | 'Annualized Return': 0.2158, 11 | 'Annualized Volatility': 0.2436, 12 | 'Sharpe Ratio': 0.89, 13 | 'Max Drawdown': -0.3673, 14 | 'Daily Mean Return': 0.00089, 15 | 'Daily Volatility': 0.01534 16 | } 17 | 18 | # Load SPY price data 19 | spy = pd.read_csv('aia_spy_prices.csv', parse_dates=['Date']) 20 | spy.set_index('Date', inplace=True) 21 | 22 | # Calculate SPY returns 23 | spy_returns = spy['SPY.US'].pct_change().dropna() 24 | 25 | # Calculate SPY metrics 26 | total_return_spy = (1 + spy_returns).prod() - 1 27 | annualized_return_spy = (1 + spy_returns.mean())**252 - 1 28 | annualized_volatility_spy = spy_returns.std() * np.sqrt(252) 29 | sharpe_ratio_spy = annualized_return_spy / annualized_volatility_spy if annualized_volatility_spy > 0 else np.nan 30 | 31 | # Calculate max drawdown for SPY 32 | cum_returns_spy = (1 + spy_returns).cumprod() 33 | roll_max_spy = cum_returns_spy.cummax() 34 | drawdown_spy = cum_returns_spy / roll_max_spy - 1 35 | max_drawdown_spy = drawdown_spy.min() 36 | 37 | # Daily mean and volatility for SPY 38 | daily_mean_spy = spy_returns.mean() 39 | daily_volatility_spy = spy_returns.std() 40 | 41 | # Create comparison table 42 | comparison_data = { 43 | 'Metric': ['Total Return', 'Annualized Return', 'Annualized Volatility', 'Sharpe Ratio', 'Max Drawdown', 'Daily Mean Return', 'Daily Volatility'], 44 | 'Portfolio': [portfolio_metrics['Total Return'], portfolio_metrics['Annualized Return'], portfolio_metrics['Annualized Volatility'], portfolio_metrics['Sharpe Ratio'], portfolio_metrics['Max Drawdown'], portfolio_metrics['Daily Mean Return'], portfolio_metrics['Daily Volatility']], 45 | 'SPY ETF': [total_return_spy, annualized_return_spy, annualized_volatility_spy, sharpe_ratio_spy, max_drawdown_spy, daily_mean_spy, daily_volatility_spy] 46 | } 47 | 48 | comparison_df = pd.DataFrame(comparison_data) 49 | 50 | # Format as percentage where appropriate 51 | percentage_metrics = ['Total Return', 'Annualized Return', 'Annualized Volatility', 'Max Drawdown', 'Daily Mean Return', 'Daily Volatility'] 52 | for metric in percentage_metrics: 53 | comparison_df.loc[comparison_df['Metric'] == metric, ['Portfolio', 'SPY ETF']] = \ 54 | comparison_df.loc[comparison_df['Metric'] == metric, ['Portfolio', 'SPY ETF']].map(lambda x: f'{x:.2%}') 55 | 56 | print(comparison_df) 57 | -------------------------------------------------------------------------------- /10_options/prompts/extract_user_prompts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Extract all user prompts from a Codex session JSON file. 4 | 5 | Reads 'codex_session.json' by default and writes the text of every user 6 | input prompt into an output text file (default 'user_prompts.txt'), 7 | separating each prompt by a blank line. 8 | Supports session JSON where prompts are under 'items' or provided as a top-level list. 9 | 10 | Usage: 11 | python3 extract_user_prompts.py [INPUT_JSON] [OUTPUT_TXT] 12 | """ 13 | 14 | import json 15 | import sys 16 | 17 | 18 | def extract_prompts(input_path): 19 | with open(input_path, 'r', encoding='utf-8') as f: 20 | session = json.load(f) 21 | # Support session JSON as dict with 'items' or as a list of items directly. 22 | if isinstance(session, dict): 23 | items = session.get('items', []) 24 | elif isinstance(session, list): 25 | items = session 26 | else: 27 | items = [] 28 | 29 | prompts = [] 30 | for item in items: 31 | if item.get('role') == 'user': 32 | for entry in item.get('content', []): 33 | text = entry.get('text') 34 | if text: 35 | prompts.append(text) 36 | return prompts 37 | 38 | 39 | def main(): 40 | import argparse 41 | 42 | parser = argparse.ArgumentParser( 43 | description='Extract user prompts from Codex session JSON.' 44 | ) 45 | parser.add_argument( 46 | 'input_json', nargs='?', default='codex_session.json', 47 | help='Path to the session JSON file' 48 | ) 49 | parser.add_argument( 50 | 'output_txt', nargs='?', default='user_prompts.txt', 51 | help='Path to write extracted prompts' 52 | ) 53 | args = parser.parse_args() 54 | 55 | try: 56 | prompts = extract_prompts(args.input_json) 57 | except FileNotFoundError: 58 | print(f"Error: file not found: {args.input_json}", file=sys.stderr) 59 | sys.exit(1) 60 | except json.JSONDecodeError as e: 61 | print(f"Error: invalid JSON in {args.input_json}: {e}", file=sys.stderr) 62 | sys.exit(1) 63 | 64 | if not prompts: 65 | print(f"No user prompts found in {args.input_json}", file=sys.stderr) 66 | sys.exit(1) 67 | 68 | with open(args.output_txt, 'w', encoding='utf-8') as f: 69 | for p in prompts: 70 | f.write(p.rstrip()) 71 | f.write("\n\n") 72 | 73 | print(f"Wrote {len(prompts)} prompts to {args.output_txt}") 74 | 75 | 76 | if __name__ == '__main__': 77 | main() 78 | -------------------------------------------------------------------------------- /05_session/backtest_ai_gemini.py: -------------------------------------------------------------------------------- 1 | 2 | import pandas as pd 3 | import datetime 4 | 5 | # Assuming 'aia_eod_data.csv' is in the same directory as this script. 6 | # If running in an environment where the file is provided in a different way (e.g., through a platform), 7 | # you might need to adjust the file loading mechanism. 8 | try: 9 | df = pd.read_csv('aia_eod_data.csv') 10 | except FileNotFoundError: 11 | print("Error: 'aia_eod_data.csv' not found. Please ensure the file is in the correct directory.") 12 | exit() 13 | 14 | # Convert 'Date' column to datetime objects 15 | df['Date'] = pd.to_datetime(df['Date']) 16 | 17 | # Set 'Date' as the index 18 | df = df.set_index('Date') 19 | 20 | # Define the backtesting period (last three years from a specific end date) 21 | # Using June 12, 2025 as the end date for consistency with the analysis 22 | end_date = datetime.date(2025, 6, 12) 23 | start_date = end_date - datetime.timedelta(days=3 * 365) # Approximate 3 years 24 | 25 | # Filter data for the backtesting period 26 | portfolio_data = df[(df.index >= pd.Timestamp(start_date)) & (df.index <= pd.Timestamp(end_date))] 27 | 28 | # Define the portfolio assets and their positions (long/short) 29 | # Long: NVDA, JPM 30 | # Short: NFLX, GE 31 | # Assuming equal weight for simplicity for each position (1/4 = 0.25) 32 | # Short positions mean negative weight 33 | portfolio_weights = { 34 | 'NVDA': 0.25, 35 | 'JPM': 0.25, 36 | 'NFLX': -0.25, 37 | 'GE': -0.25 38 | } 39 | 40 | # Select only the columns needed for the portfolio 41 | selected_assets = list(portfolio_weights.keys()) 42 | portfolio_prices = portfolio_data[selected_assets] 43 | 44 | # Calculate daily returns for each asset 45 | daily_returns = portfolio_prices.pct_change().dropna() 46 | 47 | # Calculate daily portfolio returns 48 | # Multiply each asset's return by its weight and sum them up 49 | portfolio_daily_returns = (daily_returns * pd.Series(portfolio_weights)).sum(axis=1) 50 | 51 | # Calculate cumulative returns 52 | # Start with an initial value of 1 to represent the starting portfolio value 53 | cumulative_returns = (1 + portfolio_daily_returns).cumprod() 54 | 55 | # Print the final cumulative return 56 | final_cumulative_return = cumulative_returns.iloc[-1] 57 | print(f"Final Cumulative Return of the L/S portfolio: {final_cumulative_return:.2f}") 58 | 59 | # Optional: Plotting the cumulative returns 60 | import matplotlib.pyplot as plt 61 | plt.figure(figsize=(12, 6)) 62 | cumulative_returns.plot(title='L/S Portfolio Cumulative Returns (Last 3 Years)') 63 | plt.xlabel('Date') 64 | plt.ylabel('Cumulative Returns') 65 | plt.grid(True) 66 | plt.show() 67 | -------------------------------------------------------------------------------- /10_options/README.md: -------------------------------------------------------------------------------- 1 | # mcdxa 2 | 3 | ![The Python Quants GmbH Logo](https://hilpisch.com/tpq_logo.png) 4 | 5 | mcdxa is a Python package for pricing European and American options with arbitrary payoffs via Monte Carlo simulation and analytic models. It provides a modular framework that cleanly separates stochastic asset price models, payoff definitions (including plain‑vanilla, path‑dependent, and custom functions), Monte Carlo engines, and pricer classes for European and American-style options. 6 | 7 | ## Features 8 | 9 | - **Stochastic models**: Black–Scholes–Merton (GBM), Merton jump‑diffusion (Merton), Heston stochastic volatility, and Bates (Heston + Merton jumps). 10 | - **Payoffs**: vanilla calls/puts, arithmetic Asian, lookback, and fully custom payoff functions via `CustomPayoff`. 11 | - **Monte Carlo engine**: generic path generator and pricing framework with standard error estimation. 12 | - **European pricer**: Monte Carlo wrapper with direct comparison to Black–Scholes analytic formulas. 13 | - **American pricers**: Cox‑Ross‑Rubinstein binomial tree and Longstaff‑Schwartz least-squares Monte Carlo. 14 | - **Analytics**: built‑in functions for Black–Scholes and Merton jump‑diffusion analytic pricing. 15 | 16 | ## Installation 17 | 18 | Clone the repository and install in editable mode: 19 | 20 | ```bash 21 | git clone https://github.com/yhilpisch/mcdxa.git 22 | cd mcdxa 23 | pip install -e . 24 | ``` 25 | 26 | ## Quickstart 27 | 28 | ```python 29 | import numpy as np 30 | from mcdxa.models import BSM 31 | from mcdxa.payoffs import CallPayoff 32 | from mcdxa.pricers.european import EuropeanPricer 33 | from mcdxa.analytics import bsm_price 34 | 35 | # Parameters 36 | S0, K, T, r, sigma = 100.0, 100.0, 1.0, 0.05, 0.2 37 | 38 | # Define model and payoff 39 | model = BSM(r, sigma) 40 | payoff = CallPayoff(K) 41 | 42 | # Monte Carlo pricing 43 | pricer = EuropeanPricer(model, payoff, n_paths=50_000, n_steps=50, seed=42) 44 | price_mc, stderr = pricer.price(S0, T, r) 45 | 46 | # Analytic Black–Scholes price for comparison 47 | price_bs = bsm_price(S0, K, T, r, sigma, option_type='call') 48 | 49 | print(f"MC Price: {price_mc:.4f} ± {stderr:.4f}") 50 | print(f"BS Price: {price_bs:.4f}") 51 | ``` 52 | 53 | ## Documentation and Examples 54 | 55 | Explore the [Jupyter notebook tutorial](scripts/mcdxa.ipynb) for detailed examples on custom payoffs, path simulations, convergence plots, and American option pricing. 56 | 57 | ## Testing 58 | 59 | Run the full test suite with pytest: 60 | 61 | ```bash 62 | pytest -q 63 | ``` 64 | 65 | ## Company 66 | 67 | **The Python Quants GmbH** 68 | 69 | © 2025 The Python Quants GmbH. All rights reserved. 70 | -------------------------------------------------------------------------------- /10_options/scripts/exotics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Benchmark path-dependent European payoffs (Asian, Lookback) under BSM Monte Carlo. 4 | Shows ATM/ITM/OTM results for calls and puts. 5 | """ 6 | 7 | import os 8 | import sys 9 | import time 10 | import argparse 11 | 12 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 13 | import numpy as np 14 | 15 | from mcdxa.models import BSM 16 | from mcdxa.payoffs import ( 17 | AsianCallPayoff, AsianPutPayoff, 18 | LookbackCallPayoff, LookbackPutPayoff 19 | ) 20 | from mcdxa.pricers.european import EuropeanPricer 21 | 22 | 23 | def main(): 24 | parser = argparse.ArgumentParser( 25 | description="Benchmark path-dependent European payoffs under BSM Monte Carlo" 26 | ) 27 | parser.add_argument("--K", type=float, default=100.0, help="Strike price") 28 | parser.add_argument("--T", type=float, default=1.0, help="Time to maturity") 29 | parser.add_argument("--r", type=float, default=0.05, help="Risk-free rate") 30 | parser.add_argument("--sigma", type=float, default=0.2, help="Volatility") 31 | parser.add_argument("--n_paths", type=int, default=100_000, 32 | help="Number of Monte Carlo paths") 33 | parser.add_argument("--n_steps", type=int, default=50, 34 | help="Number of time steps per path") 35 | parser.add_argument("--seed", type=int, default=42, help="Random seed") 36 | args = parser.parse_args() 37 | 38 | # Base BSM model 39 | model = BSM(args.r, args.sigma) 40 | 41 | # Scenarios: ATM, ITM, OTM (varying S0, fixed K) 42 | moneyness = 0.10 43 | scenarios = [ 44 | ("ATM", args.K), 45 | ("ITM", args.K * (1 + moneyness)), 46 | ("OTM", args.K * (1 - moneyness)), 47 | ] 48 | 49 | payoffs = [ 50 | ("AsianCall", AsianCallPayoff), 51 | ("AsianPut", AsianPutPayoff), 52 | ("LookbackCall", LookbackCallPayoff), 53 | ("LookbackPut", LookbackPutPayoff), 54 | ] 55 | 56 | header = f"{'Payoff':<15}{'Case':<6}{'Price':>12}{'StdErr':>12}{'Time(s)':>10}" 57 | print(header) 58 | print('-' * len(header)) 59 | 60 | for name, payoff_cls in payoffs: 61 | for case, S0 in scenarios: 62 | payoff = payoff_cls(args.K) 63 | pricer = EuropeanPricer( 64 | model, payoff, 65 | n_paths=args.n_paths, 66 | n_steps=args.n_steps, 67 | seed=args.seed 68 | ) 69 | t0 = time.time() 70 | price, stderr = pricer.price(S0, args.T, args.r) 71 | dt = time.time() - t0 72 | print(f"{name:<15}{case:<6}{price:12.6f}{stderr:12.6f}{dt:10.4f}") 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /06_session/black_scholes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | black_scholes.py 4 | 5 | Module implementing the Black-Scholes-Merton model for European option pricing. 6 | """ 7 | import math 8 | 9 | 10 | class BlackScholes: 11 | """ 12 | Class for pricing European call and put options using the Black-Scholes-Merton formula. 13 | """ 14 | def __init__(self, S, K, T, r, sigma, q=0.0): 15 | """ 16 | Initialize model parameters. 17 | 18 | Parameters: 19 | S : float : current price of the underlying asset 20 | K : float : strike price of the option 21 | T : float : time to maturity in years 22 | r : float : risk-free interest rate (annual) 23 | sigma : float : volatility of the underlying asset (annual) 24 | q : float : continuous dividend yield (annual), default 0.0 25 | """ 26 | self.S = float(S) 27 | self.K = float(K) 28 | self.T = float(T) 29 | self.r = float(r) 30 | self.sigma = float(sigma) 31 | self.q = float(q) 32 | 33 | @staticmethod 34 | def _norm_cdf(x): 35 | """ 36 | Standard normal cumulative distribution function. 37 | """ 38 | return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0))) 39 | 40 | def _d1(self): 41 | return ((math.log(self.S / self.K) 42 | + (self.r - self.q + 0.5 * self.sigma ** 2) * self.T) 43 | / (self.sigma * math.sqrt(self.T))) 44 | 45 | def _d2(self): 46 | return self._d1() - self.sigma * math.sqrt(self.T) 47 | 48 | def call_price(self): 49 | """ 50 | Compute the European call option price. 51 | """ 52 | d1 = self._d1() 53 | d2 = self._d2() 54 | discounted_S = self.S * math.exp(-self.q * self.T) 55 | discounted_K = self.K * math.exp(-self.r * self.T) 56 | return discounted_S * self._norm_cdf(d1) - discounted_K * self._norm_cdf(d2) 57 | 58 | def put_price(self): 59 | """ 60 | Compute the European put option price. 61 | """ 62 | d1 = self._d1() 63 | d2 = self._d2() 64 | discounted_S = self.S * math.exp(-self.q * self.T) 65 | discounted_K = self.K * math.exp(-self.r * self.T) 66 | return discounted_K * self._norm_cdf(-d2) - discounted_S * self._norm_cdf(-d1) 67 | 68 | 69 | if __name__ == '__main__': 70 | # Example usage 71 | params = { 72 | 'S': 100, # asset price 73 | 'K': 100, # strike price 74 | 'T': 1, # time to maturity (1 year) 75 | 'r': 0.05, # risk-free rate 76 | 'sigma': 0.2, # volatility 77 | 'q': 0.0 # dividend yield 78 | } 79 | model = BlackScholes(**params) 80 | print(f"Call Price: {model.call_price():.4f}") 81 | print(f"Put Price: {model.put_price():.4f}") 82 | -------------------------------------------------------------------------------- /03_session/portfolio_deepseek_r1.md: -------------------------------------------------------------------------------- 1 | ### Hypothetical Long-Short Portfolio 2 | Based on fundamental metrics, this portfolio leverages **relative strength** (long) vs. **weakness/overvaluation** (short). Positions are equally weighted for balance (100% long exposure offset by 100% short exposure). 3 | 4 | #### **Long Positions (Strong Fundamentals)** 5 | | Ticker | Weight | Rationale | Key Supporting Metrics | 6 | |--------|--------|-----------|------------------------| 7 | | **NVDA** | 33% | Best-in-class profitability and growth. | ROE: 119.18%, Op. Margin: 61.11%, Rev. Growth (YoY): 77.9%, Earnings Growth (YoY): 83.6% | 8 | | **MSFT** | 33% | High profitability, robust growth, and scale. | Profit Margin: 35.79%, Rev. Growth (YoY): 13.3%, Market Cap: $3.42T | 9 | | **META** | 33% | Exceptional margins and earnings growth. | Profit Margin: 39.11%, Earnings Growth (YoY): 36.5%, Op. Margin: 41.49% | 10 | 11 | #### **Short Positions (Weak/Overvalued)** 12 | | Ticker | Weight | Rationale | Key Supporting Metrics | 13 | |--------|--------|-----------|------------------------| 14 | | **INTC** | 33% | Fundamental deterioration. | Negative Profit Margin (-36.19%), Negative ROE (-18.13%), Declining Rev. (-0.4% YoY) | 15 | | **GE** | 33% | Subpar profitability vs. peers. | Low ROE (27.2%), Profit Margin (17.63%) < sector avg., Rev. Growth (10.9%) lags leaders | 16 | | **MS** | 33% | Extreme overvaluation signal. | PEG Ratio outlier (148.32), ROE (13.88%) weak for valuation | 17 | 18 | --- 19 | 20 | ### **Portfolio Rationale & Expected Advantage** 21 | 1. **Long Side Rationale**: 22 | - Targets **high-growth, high-efficiency** entities (NVDA, MSFT, META) with: 23 | - Superior profitability (avg. Profit Margin: 43.6% vs. dataset avg: 24.3%). 24 | - Strong growth (avg. Earnings Growth: 62.3% vs. dataset avg: 48.7%). 25 | - Balances exposure across semiconductor (NVDA), software (MSFT), and digital services (META). 26 | 27 | 2. **Short Side Rationale**: 28 | - Capitalizes on **fundamental weakness/overvaluation**: 29 | - INTC: Structural issues (negative margins, shrinking revenue). 30 | - GE: Mediocre efficiency (ROE 27.2% < tech/financial leaders). 31 | - MS: Dangerously high PEG (148.32) implies unsustainable growth expectations. 32 | - Diversified across semiconductors (INTC), industrials (GE), and financials (MS). 33 | 34 | 3. **Expected Advantage**: 35 | - **Relative value capture**: Longs dominate in efficiency/growth, while shorts are laggards/overvalued. 36 | - **Hedging**: Sector diversification reduces idiosyncratic risk (e.g., tech vs. industrials/financials). 37 | - **Outlier exploitation**: Shorting MS’s extreme PEG and INTC’s negative metrics amplifies potential alpha. 38 | 39 | > **Note**: Weights assume equal capital allocation (100% gross exposure). No external data used—analysis based solely on provided fundamentals. -------------------------------------------------------------------------------- /10_options/mcdxa/bates.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | from scipy.integrate import quad 4 | from .models import Bates 5 | 6 | 7 | def simulate_bates( 8 | S0: float, 9 | T: float, 10 | r: float, 11 | kappa: float, 12 | theta: float, 13 | xi: float, 14 | rho: float, 15 | v0: float, 16 | lam: float, 17 | mu_j: float, 18 | sigma_j: float, 19 | q: float = 0.0, 20 | n_paths: int = 10000, 21 | n_steps: int = 50, 22 | rng: np.random.Generator = None, 23 | ) -> np.ndarray: 24 | """ 25 | Wrapper: instantiate Bates model and simulate via its .simulate() method. 26 | """ 27 | model = Bates(r, kappa, theta, xi, rho, v0, lam, mu_j, sigma_j, q) 28 | return model.simulate(S0, T, n_paths, n_steps, rng=rng) 29 | 30 | 31 | def bates_price( 32 | S0: float, 33 | K: float, 34 | T: float, 35 | r: float, 36 | kappa: float, 37 | theta: float, 38 | xi: float, 39 | rho: float, 40 | v0: float, 41 | lam: float, 42 | mu_j: float, 43 | sigma_j: float, 44 | q: float = 0.0, 45 | option_type: str = "call", 46 | integration_limit: float = 250, 47 | ) -> float: 48 | """ 49 | Bates (1996) model price for European call or put via Lewis (2001) single-integral. 50 | 51 | Combines Heston stochastic volatility characteristic function 52 | with log-normal jumps (Merton). 53 | """ 54 | def _char_heston(u): 55 | d = np.sqrt((kappa - rho * xi * u * 1j) ** 2 + (u ** 2 + u * 1j) * xi ** 2) 56 | g = (kappa - rho * xi * u * 1j - d) / (kappa - rho * xi * u * 1j + d) 57 | # add jump drift compensator E[Y - 1] 58 | kappa_j = math.exp(mu_j + 0.5 * sigma_j ** 2) - 1 59 | C = ( 60 | (r - q - lam * kappa_j) * u * 1j * T 61 | + (kappa * theta / xi ** 2) 62 | * ((kappa - rho * xi * u * 1j - d) * T 63 | - 2 * np.log((1 - g * np.exp(-d * T)) / (1 - g))) 64 | ) 65 | D = ((kappa - rho * xi * u * 1j - d) / xi ** 2) * \ 66 | ((1 - np.exp(-d * T)) / (1 - g * np.exp(-d * T))) 67 | return np.exp(C + D * v0) 68 | 69 | def _char_bates(u): 70 | # add jump component to characteristic function 71 | jump_cf = np.exp(lam * T * (np.exp(1j * u * mu_j - 0.5 * u ** 2 * sigma_j ** 2) - 1)) 72 | return _char_heston(u) * jump_cf 73 | 74 | def _lewis_integrand(u): 75 | cf_val = _char_bates(u - 0.5j) 76 | return (np.exp(1j * u * math.log(S0 / K)) * cf_val).real / (u ** 2 + 0.25) 77 | 78 | integral_value = quad(_lewis_integrand, 0, integration_limit)[0] 79 | call_price = S0 * math.exp(-q * T) - math.exp(-r * T) * math.sqrt(S0 * K) / math.pi * integral_value 80 | if option_type == "call": 81 | price = call_price 82 | elif option_type == "put": 83 | price = call_price - S0 * math.exp(-q * T) + K * math.exp(-r * T) 84 | else: 85 | raise ValueError("Option type must be 'call' or 'put'.") 86 | return max(price, 0.0) 87 | -------------------------------------------------------------------------------- /10_options/scripts/benchmarks/benchmark_bsm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Benchmark European option pricing (MC vs analytical BSM) for ATM, ITM, and OTM cases. 4 | """ 5 | 6 | import os 7 | import sys 8 | import time 9 | import math 10 | import argparse 11 | 12 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) 13 | import numpy as np 14 | from mcdxa.models import BSM 15 | from mcdxa.payoffs import CallPayoff, PutPayoff 16 | from mcdxa.pricers.european import EuropeanPricer 17 | from mcdxa.analytics import bsm_price 18 | 19 | 20 | def main(): 21 | parser = argparse.ArgumentParser( 22 | description="Benchmark European options: MC vs analytical BSM" 23 | ) 24 | parser.add_argument("--K", type=float, default=100.0, help="Strike price") 25 | parser.add_argument("--T", type=float, default=1.0, help="Time to maturity") 26 | parser.add_argument("--r", type=float, default=0.05, help="Risk-free rate") 27 | parser.add_argument("--sigma", type=float, default=0.2, help="Volatility") 28 | parser.add_argument( 29 | "--n_paths", type=int, default=100000, help="Number of Monte Carlo paths" 30 | ) 31 | parser.add_argument( 32 | "--n_steps", type=int, default=50, help="Number of time steps per path" 33 | ) 34 | parser.add_argument("--seed", type=int, default=42, help="Random seed") 35 | parser.add_argument("--q", type=float, default=0.0, help="Dividend yield") 36 | args = parser.parse_args() 37 | 38 | model = BSM(args.r, args.sigma, q=args.q) 39 | moneyness = 0.10 40 | scenarios = [] 41 | for opt_type, payoff_cls in [("call", CallPayoff), ("put", PutPayoff)]: 42 | if opt_type == "call": 43 | S0_cases = [args.K * (1 + moneyness), args.K, args.K * (1 - moneyness)] 44 | else: 45 | S0_cases = [args.K * (1 - moneyness), args.K, args.K * (1 + moneyness)] 46 | for case, S0_case in zip(["ITM", "ATM", "OTM"], S0_cases): 47 | scenarios.append((opt_type, payoff_cls, case, S0_case)) 48 | 49 | header = f"{'Type':<6}{'Case':<6}{'MC Price':>12}{'StdErr':>12}{'BSM':>12}{'Abs Err':>12}{'% Err':>10}{'Time(s)':>12}" 50 | print(header) 51 | print('-' * len(header)) 52 | 53 | for opt_type, payoff_cls, case, S0_case in scenarios: 54 | payoff = payoff_cls(args.K) 55 | pricer = EuropeanPricer( 56 | model, payoff, n_paths=args.n_paths, n_steps=args.n_steps, seed=args.seed 57 | ) 58 | t0 = time.time() 59 | price_mc, stderr = pricer.price(S0_case, args.T, args.r) 60 | dt = time.time() - t0 61 | 62 | price_bs = bsm_price( 63 | S0_case, args.K, args.T, args.r, args.sigma, 64 | q=args.q, option_type=opt_type 65 | ) 66 | 67 | abs_err = abs(price_mc - price_bs) 68 | pct_err = abs_err / price_bs * 100.0 if price_bs != 0 else float('nan') 69 | print(f"{opt_type.capitalize():<6}{case:<6}{price_mc:12.6f}{stderr:12.6f}{price_bs:12.6f}{abs_err:12.6f}{pct_err:10.2f}{dt:12.4f}") 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /10_options/prompts/user_prompts_2.txt: -------------------------------------------------------------------------------- 1 | I want to create a Python package to flexibly price European and American options with arbitrary payoffs based on Monte Carlo simulation. The package shall be orthogonal and shall separate core functionalities in separate files/modules. The valuation shall primarily be done through MCS. Provide a concise outline and structure for the implementation of this project. 2 | 3 | Write the outline in markdown in a new file outline.md. 4 | 5 | Start with the first implementations so that we can value plain vanilla European put and call options in the BSM 1973 models. 6 | 7 | No, don't do that — stop for now. 8 | 9 | When executing Python code, use "python" instead of "python3". Implement a script that benchmarks the European option pricing based on Monte Carlo against the analytical BSM 1973 values. 10 | 11 | Update the benchmark to include for both options ATM/ITM cases. Present the results in a table. Including absolute and % valuation errors in each case. 12 | 13 | Also add the respective OTM cases. 14 | 15 | Now add the pricing of American options to the package. For the benchmarking of the ATM/ITM/OTM option generate benchmark values based on the CRR 1979 binomial option pricing model. 16 | 17 | You should implement a benchmark for the European options prices against the analytical ones AND for the American options prices from MCS against the American option values from the CRR 1979 model. In both cases for puts and calls ATM/ITM/OTM. 18 | 19 | Now add as an additional model for simulating paths the Merton jump diffusion model. 20 | 21 | For the benchmarking of the jump diffusion model, implement pricing formulas for that model based on the characteristic function. Amend the benchmarking file to include bencharks for European put/call ATM/ITM/OTM options in that model. 22 | 23 | I have renamed the benchmarking file to just benchmark.py. Now add the Heston stochastic volatility model for simulation and pricing. And add the semi-analytical formula for European options in that model so that European put/call prices can be benchmarked as before. Adjust the benchmark.py file accordingly. 24 | 25 | Separate the (semi-)analytical formulae from the benchmark.py script into separate scripts in the pricing package. Also correct the following: /Users/yves/Dropbox/Program/cpf/57_ai_assistants/git/10_session/scripts/benchmark.py:138: DeprecationWarning: `trapz` is deprecated. Use `trapezoid` instead, or one of the numerical integration functions in `scipy.integrate`. 26 | 27 | benchmark.py still includes the analytical pricing functions. 28 | 29 | Now add the functionality that allows the definition of custom, maybe quite complex, option payoffs. Get started with path-dependent options, such as Asian/lookback options. Focus on European exercise first. Create a new test script which values different types of such options and show ATM/ITM/OTM results. 30 | 31 | When running the original benchmark.py script, the following happens: File "/Users/yves/Dropbox/Program/cpf/57_ai_assistants/git/10_session/option_pricing/payoffs.py", line 17, in __call__ 32 | S_end = S[:, -1] if S.ndim == 2 else S 33 | 34 | -------------------------------------------------------------------------------- /03_session/aia_fundamentals.csv: -------------------------------------------------------------------------------- 1 | ,AAPL,MSFT,AMZN,NFLX,META,GOOG,INTC,AMD,NVDA,GE,GS,BAC,JPM,MS 2 | MarketCapitalization,2990296530944,3424096878592,2187190730752,515609034752,1576632188928,2050458648576,89639092224,185747587072,3304519892992,257831780352,188931194880,333059751936,737264795648,207294201856 3 | MarketCapitalizationMln,2990296.5309,3424096.8786,2187190.7308,515609.0348,1576632.1889,2050458.6486,89639.0922,185747.5871,3304519.893,257831.7804,188931.1949,333059.7519,737264.7956,207294.2019 4 | EBITDA,138865999872,149172994048,126144004096,11453826048,87979999232,135705001984,8114999808,5878000128,83316998144,9761000448,,,, 5 | PERatio,30.3809,34.848,32.8057,55.8585,24.5067,18.9314,,80.6761,44.5724,36.7447,13.8897,12.8921,12.7851,14.7669 6 | PEGRatio,1.9879,2.1129,2.2241,2.1578,2.114,1.3334,0.5009,0.584,1.8804,8.934,6.7563,1.6256,7.0961,148.3176 7 | WallStreetTargetPrice,228.7593,509.9169,238.634,1116.6874,711.7766,199.3125,21.39,127.5346,163.0138,228.1172,585.579,49.0652,267.0148,124.1263 8 | BookValue,4.471,43.3,28.82,56.446,73.337,28.405,22.869,35.817,3.241,18.053,371.454,36.386,119.238,60.407 9 | DividendShare,1.0,3.24,,,2.025,1.0,0.245,,0.034,1.2,11.75,1.02,5.05,3.7 10 | DividendYield,0.0053,0.0074,,,0.0033,0.005,0.0122,,0.0003,0.0062,0.02,0.0241,0.0215,0.0293 11 | EarningsShare,6.59,13.22,6.28,21.69,26.21,9.19,-4.59,1.42,3.04,6.58,44.33,3.43,20.75,8.75 12 | EPSEstimateCurrentYear,7.1778,13.3929,6.1971,25.5728,25.7398,9.5878,0.3024,3.9891,2.9539,5.5578,44.5834,3.6462,18.4041,8.6916 13 | EPSEstimateNextYear,7.8284,15.1242,7.2683,30.7818,28.4273,10.1716,0.8215,5.7217,4.4343,6.475,50.0136,4.2175,19.4192,9.3273 14 | EPSEstimateNextQuarter,1.4639,3.3128,1.3915,6.2333,5.5714,2.1239,0.0635,0.8815,0.0,1.3197,10.3762,0.9187,4.5335,2.0521 15 | EPSEstimateCurrentQuarter,1.6118,3.216,1.3566,5.658,5.2229,2.0108,0.0045,0.9336,0.8459,1.2699,12.2712,0.8161,4.645,2.1925 16 | MostRecentQuarter,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-01-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31 17 | ProfitMargin,0.243,0.3579,0.1014,0.2307,0.3911,0.3086,-0.3619,0.0802,0.5585,0.1763,0.2806,0.2858,0.3538,0.2235 18 | OperatingMarginTTM,0.3103,0.4567,0.1182,0.3175,0.4149,0.3392,-0.0025,0.1084,0.6111,0.2198,0.3815,0.3135,0.4334,0.3746 19 | ReturnOnAssetsTTM,0.2381,0.1458,0.0763,0.1379,0.1788,0.169,-0.0109,0.0256,0.5742,0.0372,0.0086,0.0084,0.0141,0.0114 20 | ReturnOnEquityTTM,1.3802,0.3361,0.2524,0.4084,0.3984,0.3479,-0.1813,0.039,1.1918,0.272,0.1222,0.0946,0.1735,0.1388 21 | RevenueTTM,400366010368,270010007552,650313007104,40173326336,170359996416,359713013760,53043998720,27750000640,130497003520,39680999424,53043998720,97452998656,168713994240,63958999040 22 | RevenuePerShareTTM,26.455,36.325,61.785,93.794,67.349,29.338,12.321,17.122,5.314,36.75,163.514,12.521,59.163,40.308 23 | QuarterlyRevenueGrowthYOY,0.051,0.133,0.086,0.125,0.161,0.12,-0.004,0.359,0.779,0.109,0.063,0.057,0.048,0.163 24 | GrossProfitTTM,186699005952,186509000704,319681986560,18848788480,139297996800,210757992448,17568999424,14867999744,97858002944,12650999808,43939999744,97452998656,168713994240,55355998208 25 | DilutedEpsTTM,6.59,13.22,6.28,21.69,26.21,9.19,-4.59,1.42,3.04,6.58,44.33,3.43,20.75,8.75 26 | QuarterlyEarningsGrowthYOY,0.078,0.177,0.622,0.252,0.365,0.488,-0.717,5.252,0.836,0.314,0.22,0.184,0.142,0.287 27 | -------------------------------------------------------------------------------- /04_session/aia_fundamentals.csv: -------------------------------------------------------------------------------- 1 | ,AAPL,MSFT,AMZN,NFLX,META,GOOG,INTC,AMD,NVDA,GE,GS,BAC,JPM,MS 2 | MarketCapitalization,2990296530944,3424096878592,2187190730752,515609034752,1576632188928,2050458648576,89639092224,185747587072,3304519892992,257831780352,188931194880,333059751936,737264795648,207294201856 3 | MarketCapitalizationMln,2990296.5309,3424096.8786,2187190.7308,515609.0348,1576632.1889,2050458.6486,89639.0922,185747.5871,3304519.893,257831.7804,188931.1949,333059.7519,737264.7956,207294.2019 4 | EBITDA,138865999872,149172994048,126144004096,11453826048,87979999232,135705001984,8114999808,5878000128,83316998144,9761000448,,,, 5 | PERatio,30.3809,34.848,32.8057,55.8585,24.5067,18.9314,,80.6761,44.5724,36.7447,13.8897,12.8921,12.7851,14.7669 6 | PEGRatio,1.9879,2.1129,2.2241,2.1578,2.114,1.3334,0.5009,0.584,1.8804,8.934,6.7563,1.6256,7.0961,148.3176 7 | WallStreetTargetPrice,228.7593,509.9169,238.634,1116.6874,711.7766,199.3125,21.39,127.5346,163.0138,228.1172,585.579,49.0652,267.0148,124.1263 8 | BookValue,4.471,43.3,28.82,56.446,73.337,28.405,22.869,35.817,3.241,18.053,371.454,36.386,119.238,60.407 9 | DividendShare,1.0,3.24,,,2.025,1.0,0.245,,0.034,1.2,11.75,1.02,5.05,3.7 10 | DividendYield,0.0053,0.0074,,,0.0033,0.005,0.0122,,0.0003,0.0062,0.02,0.0241,0.0215,0.0293 11 | EarningsShare,6.59,13.22,6.28,21.69,26.21,9.19,-4.59,1.42,3.04,6.58,44.33,3.43,20.75,8.75 12 | EPSEstimateCurrentYear,7.1778,13.3929,6.1971,25.5728,25.7398,9.5878,0.3024,3.9891,2.9539,5.5578,44.5834,3.6462,18.4041,8.6916 13 | EPSEstimateNextYear,7.8284,15.1242,7.2683,30.7818,28.4273,10.1716,0.8215,5.7217,4.4343,6.475,50.0136,4.2175,19.4192,9.3273 14 | EPSEstimateNextQuarter,1.4639,3.3128,1.3915,6.2333,5.5714,2.1239,0.0635,0.8815,0.0,1.3197,10.3762,0.9187,4.5335,2.0521 15 | EPSEstimateCurrentQuarter,1.6118,3.216,1.3566,5.658,5.2229,2.0108,0.0045,0.9336,0.8459,1.2699,12.2712,0.8161,4.645,2.1925 16 | MostRecentQuarter,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-01-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31,2025-03-31 17 | ProfitMargin,0.243,0.3579,0.1014,0.2307,0.3911,0.3086,-0.3619,0.0802,0.5585,0.1763,0.2806,0.2858,0.3538,0.2235 18 | OperatingMarginTTM,0.3103,0.4567,0.1182,0.3175,0.4149,0.3392,-0.0025,0.1084,0.6111,0.2198,0.3815,0.3135,0.4334,0.3746 19 | ReturnOnAssetsTTM,0.2381,0.1458,0.0763,0.1379,0.1788,0.169,-0.0109,0.0256,0.5742,0.0372,0.0086,0.0084,0.0141,0.0114 20 | ReturnOnEquityTTM,1.3802,0.3361,0.2524,0.4084,0.3984,0.3479,-0.1813,0.039,1.1918,0.272,0.1222,0.0946,0.1735,0.1388 21 | RevenueTTM,400366010368,270010007552,650313007104,40173326336,170359996416,359713013760,53043998720,27750000640,130497003520,39680999424,53043998720,97452998656,168713994240,63958999040 22 | RevenuePerShareTTM,26.455,36.325,61.785,93.794,67.349,29.338,12.321,17.122,5.314,36.75,163.514,12.521,59.163,40.308 23 | QuarterlyRevenueGrowthYOY,0.051,0.133,0.086,0.125,0.161,0.12,-0.004,0.359,0.779,0.109,0.063,0.057,0.048,0.163 24 | GrossProfitTTM,186699005952,186509000704,319681986560,18848788480,139297996800,210757992448,17568999424,14867999744,97858002944,12650999808,43939999744,97452998656,168713994240,55355998208 25 | DilutedEpsTTM,6.59,13.22,6.28,21.69,26.21,9.19,-4.59,1.42,3.04,6.58,44.33,3.43,20.75,8.75 26 | QuarterlyEarningsGrowthYOY,0.078,0.177,0.622,0.252,0.365,0.488,-0.717,5.252,0.836,0.314,0.22,0.184,0.142,0.287 27 | -------------------------------------------------------------------------------- /02_session/data_analysis_gpt_4o.md: -------------------------------------------------------------------------------- 1 | # 📊 Statistical Pairs Trading Analysis Report 2 | 3 | ## 1. Overview 4 | 5 | This report investigates statistical relationships between major U.S. stocks from May 2015 to 2025 using cointegration analysis and backtesting of pairs trading strategies. The analysis covers: 6 | 7 | * Cointegration screening across all stock pairs 8 | * Spread visualization of top cointegrated pairs 9 | * A z-score-based backtest strategy 10 | * Key recommendations for implementation 11 | 12 | --- 13 | 14 | ## 2. Data Summary 15 | 16 | * **Number of Trading Days**: 2,533 17 | * **Number of Stocks Analyzed**: 14 18 | * **Sectors Covered**: Primarily Tech and Finance 19 | * **Source**: End-of-day (EOD) adjusted closing prices 20 | 21 | --- 22 | 23 | ## 3. Price Evolution 24 | 25 | The following chart illustrates long-term trends in selected tech stocks: 26 | 27 | 📈 *Selected Tech Stocks: AAPL, MSFT, AMZN, GOOG, NVDA* 28 | 29 | *(Note: Include the generated line chart here in your rendered version)* 30 | 31 | --- 32 | 33 | ## 4. Correlation Analysis 34 | 35 | Daily return correlations reveal strong clustering among tech stocks, supporting diversification between tech and finance: 36 | 37 | 🧊 *Correlation Heatmap of Daily Returns* 38 | 39 | *(Note: Include the heatmap visualization here)* 40 | 41 | --- 42 | 43 | ## 5. Cointegration Testing 44 | 45 | The Engle-Granger test was applied to all 91 possible stock pairs. Below are the top-ranked results: 46 | 47 | | Rank | Pair | P-Value | Interpretation | 48 | | ---- | ----------- | ------- | ------------------------------------ | 49 | | 1 | META & JPM | 0.0197 | Strong signal, cross-sector pair | 50 | | 2 | MSFT & GOOG | 0.0282 | Strong, logical tech pair | 51 | | 3 | INTC & GE | 0.0478 | Moderate tech-industrial correlation | 52 | 53 | These pairs exhibit statistically significant mean-reverting relationships in price. 54 | 55 | --- 56 | 57 | ## 6. Spread-Based Strategy and Backtesting 58 | 59 | ### Strategy Logic 60 | 61 | * **Enter Long** when z-score of spread < -1.0 62 | * **Enter Short** when z-score > +1.0 63 | * **Exit** when spread reverts toward 0 64 | 65 | ### Cumulative PnL Results 66 | 67 | 📉 *Backtest PnL: MSFT-GOOG vs. META-JPM* 68 | 69 | *(Note: Include the cumulative PnL line chart here)* 70 | 71 | **Observations:** 72 | 73 | * **MSFT-GOOG** produced smoother returns and better performance — a clear candidate for deployment. 74 | * **META-JPM**, despite strong cointegration, shows higher volatility and sectoral mismatch. 75 | 76 | --- 77 | 78 | ## 7. Recommendations 79 | 80 | 1. **Prioritize MSFT-GOOG** for production strategies due to its stability and sector alignment. 81 | 2. **Use rolling hedge ratios and z-score thresholds** for more adaptive strategies. 82 | 3. **Incorporate transaction cost models** and position sizing rules to assess real-world feasibility. 83 | 4. **Expand analysis using ML techniques** for pair selection, anomaly detection, and signal timing. 84 | 85 | --- 86 | 87 | ## 8. Next Steps 88 | 89 | Would you like to: 90 | 91 | * Add rolling regression or OU modeling? 92 | * Simulate real capital allocation and Sharpe ratio analysis? 93 | * Generate a dashboard for live spread monitoring? 94 | 95 | Let me know how you'd like to proceed. I can deliver the next phase as code, notebook, or LaTeX. 96 | -------------------------------------------------------------------------------- /03_session/portfolio_gpt_4o.md: -------------------------------------------------------------------------------- 1 | ### 📊 Hypothetical Long/Short Portfolio Based on Fundamental Metrics 2 | 3 | This portfolio is **market-neutral**, equally allocating long and short positions based solely on the **relative strength and weakness of fundamental metrics** (e.g., Market Cap, EBITDA, PE Ratio, PEG Ratio). 4 | 5 | --- 6 | 7 | ## 🟩 LONG POSITIONS (Buy – Strong Fundamentals) 8 | 9 | | Ticker | Rationale | Weight | 10 | | -------- | ----------------------------------------------------------------------------------------------------------------------------- | ------ | 11 | | **MSFT** | Highest EBITDA (\$149.2B), strong market cap, moderate PE (34.8) and PEG (2.11) → consistent profitability + growth potential | 20% | 12 | | **GOOG** | Low PE (18.9) despite strong EBITDA and high market cap → undervalued relative to peers | 20% | 13 | | **NVDA** | Largest market cap (\$3.3T), high EBITDA (\$83.3B), PEG < 2 → strong growth + fundamentals | 20% | 14 | | **META** | Strong EBITDA and high market cap, attractive PE (24.5), reasonable PEG (2.11) | 20% | 15 | | **AAPL** | High EBITDA and market cap, PEG near sector average, reliable financial strength | 20% | 16 | 17 | **Total Long Allocation: 100%** 18 | 19 | --- 20 | 21 | ## 🟥 SHORT POSITIONS (Sell – Weak/Overvalued Fundamentals) 22 | 23 | | Ticker | Rationale | Weight | 24 | | -------- | ---------------------------------------------------------------------------------------- | ------ | 25 | | **AMD** | Extremely high PE (80.7), low EBITDA → potential overvaluation | 25% | 26 | | **INTC** | Low PEG (0.50), missing PE, low EBITDA relative to market cap → weak growth expectations | 25% | 27 | | **MS** | Abnormally high PEG (148.3) → likely forecasting error or poor fundamentals | 25% | 28 | | **NFLX** | Very high PE (55.8), relatively low EBITDA (\$11.5B) → questionable valuation | 25% | 29 | 30 | **Total Short Allocation: 100%** 31 | 32 | --- 33 | 34 | ## 🧠 Portfolio Rationale 35 | 36 | * **Objective**: Exploit valuation inefficiencies suggested by fundamental metrics. 37 | * **Long Criteria**: 38 | 39 | * High EBITDA relative to market cap. 40 | * Reasonable or low PE and PEG ratios indicating balanced valuation and growth. 41 | * **Short Criteria**: 42 | 43 | * High PE or PEG suggesting overvaluation. 44 | * Weak profitability metrics (e.g., EBITDA), even if market cap is high. 45 | * Outlier or inconsistent metrics suggesting potential downside. 46 | 47 | --- 48 | 49 | ## 📈 Expected Advantages 50 | 51 | * **Market Neutrality**: Equal exposure to long and short sides reduces beta risk. 52 | * **Fundamental Divergence**: Capitalizes on entities that appear fundamentally undervalued (longs) vs. overvalued or weak (shorts). 53 | * **Purely Data-Driven**: Portfolio is built entirely from reported fundamentals, avoiding noise from price-based momentum or market sentiment. 54 | 55 | Would you like a visual summary (e.g., bar chart of weights or radar plot of fundamentals)? 56 | -------------------------------------------------------------------------------- /10_options/scripts/benchmarks/benchmark_american.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Benchmark American option pricing (LSM MC vs CRR binomial) for ITM, ATM, and OTM cases. 4 | """ 5 | 6 | import os 7 | import sys 8 | import time 9 | import argparse 10 | 11 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) 12 | import numpy as np 13 | from mcdxa.models import BSM 14 | from mcdxa.payoffs import CallPayoff, PutPayoff 15 | from mcdxa.pricers.european import EuropeanPricer 16 | from mcdxa.pricers.american import AmericanBinomialPricer, LongstaffSchwartzPricer 17 | 18 | 19 | def main(): 20 | parser = argparse.ArgumentParser( 21 | description="Benchmark American exercise pricing: MC (LSM) vs CRR binomial" 22 | ) 23 | parser.add_argument("--K", type=float, default=100.0, help="Strike price") 24 | parser.add_argument("--T", type=float, default=1.0, help="Time to maturity") 25 | parser.add_argument("--r", type=float, default=0.05, help="Risk-free rate") 26 | parser.add_argument("--sigma", type=float, default=0.2, help="Volatility") 27 | parser.add_argument( 28 | "--n_paths", type=int, default=100000, help="Number of Monte Carlo paths" 29 | ) 30 | parser.add_argument( 31 | "--n_steps", type=int, default=50, help="Number of time steps per path" 32 | ) 33 | parser.add_argument("--n_tree", type=int, default=200, help="Number of binomial steps") 34 | parser.add_argument("--seed", type=int, default=42, help="Random seed") 35 | parser.add_argument("--q", type=float, default=0.0, help="Dividend yield") 36 | args = parser.parse_args() 37 | 38 | model = BSM(args.r, args.sigma, q=args.q) 39 | moneyness = 0.10 40 | scenarios = [] 41 | for opt_type, payoff_cls in [("call", CallPayoff), ("put", PutPayoff)]: 42 | if opt_type == "call": 43 | S0_cases = [args.K * (1 + moneyness), args.K, args.K * (1 - moneyness)] 44 | else: 45 | S0_cases = [args.K * (1 - moneyness), args.K, args.K * (1 + moneyness)] 46 | for case, S0_case in zip(["ITM", "ATM", "OTM"], S0_cases): 47 | scenarios.append((opt_type, payoff_cls, case, S0_case)) 48 | 49 | header = f"{'Type':<6}{'Case':<6}{'MC Price':>12}{'StdErr':>12}{'CRR Price':>12}{'Abs Err':>12}{'% Err':>10}{'MC Time(s)':>12}{'Tree Time(s)':>12}" 50 | print(header) 51 | print('-' * len(header)) 52 | 53 | for opt_type, payoff_cls, case, S0_case in scenarios: 54 | payoff = payoff_cls(args.K) 55 | # LSM Monte Carlo 56 | lsm = LongstaffSchwartzPricer( 57 | model, payoff, n_paths=args.n_paths, n_steps=args.n_steps, seed=args.seed 58 | ) 59 | t0 = time.time() 60 | price_mc, stderr = lsm.price(S0_case, args.T, args.r) 61 | t_mc = time.time() - t0 62 | 63 | # CRR binomial 64 | binom = AmericanBinomialPricer(model, payoff, n_steps=args.n_tree) 65 | t0 = time.time() 66 | price_crr = binom.price(S0_case, args.T, args.r) 67 | t_crr = time.time() - t0 68 | 69 | abs_err = abs(price_mc - price_crr) 70 | pct_err = abs_err / price_crr * 100.0 if price_crr != 0 else float('nan') 71 | print(f"{opt_type.capitalize():<6}{case:<6}{price_mc:12.6f}{stderr:12.6f}{price_crr:12.6f}{abs_err:12.6f}{pct_err:10.2f}{t_mc:12.4f}{t_crr:12.4f}") 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /10_options/scripts/orchestrate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Orchestrate all benchmark suites: BSM, Merton jump-diffusion, Heston, and American. 4 | """ 5 | 6 | import os 7 | import sys 8 | import subprocess 9 | import argparse 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser( 14 | description="Orchestrate all benchmark suites: BSM, MJD, Bates, Heston, American" 15 | ) 16 | # global flags for any benchmark 17 | parser.add_argument("--K", type=float, help="Strike price") 18 | parser.add_argument("--T", type=float, help="Time to maturity") 19 | parser.add_argument("--r", type=float, help="Risk-free rate") 20 | parser.add_argument("--sigma", type=float, help="Volatility") 21 | parser.add_argument("--lam", type=float, help="Jump intensity (lambda)") 22 | parser.add_argument("--mu_j", type=float, help="Jump mean mu_j") 23 | parser.add_argument("--sigma_j", type=float, 24 | help="Jump volatility sigma_j") 25 | parser.add_argument("--kappa", type=float, 26 | help="Heston mean-reversion speed kappa") 27 | parser.add_argument("--theta", type=float, 28 | help="Heston long-term variance theta") 29 | parser.add_argument("--xi", type=float, help="Heston vol-of-vol xi") 30 | parser.add_argument("--rho", type=float, help="Heston correlation rho") 31 | parser.add_argument("--v0", type=float, help="Initial variance v0") 32 | parser.add_argument("--n_paths", type=int, 33 | help="Number of Monte Carlo paths") 34 | parser.add_argument("--n_steps", type=int, 35 | help="Number of time steps per path") 36 | parser.add_argument("--n_tree", type=int, help="Number of binomial steps") 37 | parser.add_argument("--seed", type=int, help="Random seed") 38 | parser.add_argument("--q", type=float, help="Dividend yield") 39 | args = parser.parse_args() 40 | 41 | scripts = [ 42 | 'benchmarks/benchmark_bsm.py', 43 | 'benchmarks/benchmark_mjd.py', 44 | 'benchmarks/benchmark_bates.py', 45 | 'benchmarks/benchmark_heston.py', 46 | 'benchmarks/benchmark_american.py', 47 | ] 48 | # which flags apply to each benchmark 49 | script_args = { 50 | 'benchmark_bsm.py': ["K", "T", "r", "sigma", "n_paths", "n_steps", "seed", "q"], 51 | 'benchmark_mjd.py': ["K", "T", "r", "sigma", "lam", "mu_j", "sigma_j", 52 | "n_paths", "n_steps", "seed", "q"], 53 | 'benchmark_bates.py': ["K", "T", "r", "kappa", "theta", "xi", "rho", "v0", 54 | "lam", "mu_j", "sigma_j", "n_paths", "n_steps", "seed", "q"], 55 | 'benchmark_heston.py': ["K", "T", "r", "kappa", "theta", "xi", "rho", "v0", 56 | "n_paths", "n_steps", "seed", "q"], 57 | 'benchmark_american.py': ["K", "T", "r", "sigma", "n_paths", "n_steps", 58 | "n_tree", "seed", "q"], 59 | } 60 | here = os.path.dirname(__file__) 61 | for script in scripts: 62 | path = os.path.join(here, script) 63 | print(f"\nRunning {script}...\n") 64 | cmd = [sys.executable, path] 65 | name = os.path.basename(script) 66 | for key in script_args.get(name, []): 67 | val = getattr(args, key) 68 | if val is not None: 69 | cmd += [f"--{key}", str(val)] 70 | subprocess.run(cmd) 71 | 72 | 73 | if __name__ == '__main__': 74 | main() 75 | -------------------------------------------------------------------------------- /05_session/backtest_ai_gpt_4o.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | from pylab import plt 4 | 5 | # Load the uploaded CSV file 6 | file_path = 'aia_eod_data.csv' 7 | price_data = pd.read_csv(file_path, parse_dates=['Date'], index_col='Date') 8 | 9 | # Filter for the tickers in our portfolio 10 | tickers = ['NVDA', 'JPM', 'NFLX', 'GS'] 11 | price_data = price_data[tickers].dropna() 12 | 13 | # Define weights for long/short portfolio 14 | weights = {'NVDA': 0.25, 'JPM': 0.25, 'NFLX': -0.25, 'GS': -0.25} 15 | 16 | # Calculate daily returns 17 | daily_returns = price_data.pct_change().dropna() 18 | 19 | # Compute portfolio daily returns 20 | portfolio_returns = daily_returns.dot([weights[t] for t in tickers]) 21 | 22 | # Compute cumulative returns 23 | cumulative_returns = (1 + portfolio_returns).cumprod() 24 | cumulative_df = (1 + daily_returns).cumprod() 25 | cumulative_df['Portfolio'] = cumulative_returns 26 | 27 | # Performance metrics 28 | cagr = cumulative_returns[-1] ** (1 / ((cumulative_returns.index[-1] - cumulative_returns.index[0]).days / 365.25)) - 1 29 | volatility = portfolio_returns.std() * np.sqrt(252) 30 | sharpe_ratio = portfolio_returns.mean() / portfolio_returns.std() * np.sqrt(252) 31 | 32 | # Plot cumulative returns 33 | plt.figure(figsize=(12, 6)) 34 | for ticker in tickers: 35 | plt.plot(cumulative_df[ticker], label=ticker, linestyle='--') 36 | plt.plot(cumulative_df['Portfolio'], label='Long/Short Portfolio', linewidth=2) 37 | plt.title("Cumulative Returns: Long NVDA & JPM / Short NFLX & GS") 38 | plt.xlabel("Date") 39 | plt.ylabel("Cumulative Return") 40 | plt.grid(True) 41 | plt.legend() 42 | plt.tight_layout() 43 | plt.show() 44 | 45 | # Output performance metrics 46 | { 47 | 'CAGR': f"{cagr:.2%}", 48 | 'Annual Volatility': f"{volatility:.2%}", 49 | 'Sharpe Ratio': f"{sharpe_ratio:.2f}" 50 | } 51 | 52 | # Restrict data to the last 3 years 53 | three_years_ago = price_data.index.max() - pd.DateOffset(years=3) 54 | price_data_3y = price_data[price_data.index >= three_years_ago] 55 | 56 | # Recalculate daily returns 57 | daily_returns_3y = price_data_3y.pct_change().dropna() 58 | 59 | # Recalculate portfolio returns 60 | portfolio_returns_3y = daily_returns_3y.dot([weights[t] for t in tickers]) 61 | 62 | # Recalculate cumulative returns 63 | cumulative_returns_3y = (1 + portfolio_returns_3y).cumprod() 64 | cumulative_df_3y = (1 + daily_returns_3y).cumprod() 65 | cumulative_df_3y['Portfolio'] = cumulative_returns_3y 66 | 67 | # Recalculate performance metrics 68 | cagr_3y = cumulative_returns_3y[-1] ** (1 / ((cumulative_returns_3y.index[-1] - cumulative_returns_3y.index[0]).days / 365.25)) - 1 69 | volatility_3y = portfolio_returns_3y.std() * np.sqrt(252) 70 | sharpe_ratio_3y = portfolio_returns_3y.mean() / portfolio_returns_3y.std() * np.sqrt(252) 71 | 72 | # Plot cumulative returns 73 | plt.figure(figsize=(12, 6)) 74 | for ticker in tickers: 75 | plt.plot(cumulative_df_3y[ticker], label=ticker, linestyle='--') 76 | plt.plot(cumulative_df_3y['Portfolio'], label='Long/Short Portfolio', linewidth=2) 77 | plt.title("3-Year Cumulative Returns: Long NVDA & JPM / Short NFLX & GS") 78 | plt.xlabel("Date") 79 | plt.ylabel("Cumulative Return") 80 | plt.grid(True) 81 | plt.legend() 82 | plt.tight_layout() 83 | plt.show() 84 | 85 | # Output performance metrics for the 3-year period 86 | { 87 | '3-Year CAGR': f"{cagr_3y:.2%}", 88 | '3-Year Annual Volatility': f"{volatility_3y:.2%}", 89 | '3-Year Sharpe Ratio': f"{sharpe_ratio_3y:.2f}" 90 | } 91 | 92 | 93 | -------------------------------------------------------------------------------- /09_session/eod_data_visualizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Retrieve end-of-day (EOD) stock data for multiple tickers using yfinance and plot closing prices. 4 | 5 | Usage: 6 | python eod_data_visualizer.py [--tickers AAPL MSFT NFLX] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--period PERIOD] [--normalize] 7 | Examples: 8 | python eod_data_visualizer.py 9 | python eod_data_visualizer.py --tickers GOOG AMZN --period 6mo 10 | python eod_data_visualizer.py --start 2021-01-01 --end 2021-12-31 11 | python eod_data_visualizer.py --normalize 12 | """ 13 | 14 | import argparse 15 | import datetime as dt 16 | 17 | import matplotlib.pyplot as plt 18 | import yfinance as yf 19 | 20 | 21 | def parse_args(): 22 | parser = argparse.ArgumentParser( 23 | description="Retrieve and plot EOD stock data for multiple tickers." 24 | ) 25 | parser.add_argument( 26 | "-t", "--tickers", 27 | nargs="+", 28 | default=["AAPL", "MSFT", "NFLX"], 29 | help="List of ticker symbols (default: AAPL MSFT NFLX)", 30 | ) 31 | parser.add_argument( 32 | "--start", 33 | type=str, 34 | default=None, 35 | help="Start date in YYYY-MM-DD format (default: 1 year ago if --period is not set)", 36 | ) 37 | parser.add_argument( 38 | "--end", 39 | type=str, 40 | default=None, 41 | help="End date in YYYY-MM-DD format (default: today if --period is not set)", 42 | ) 43 | parser.add_argument( 44 | "--period", 45 | type=str, 46 | default=None, 47 | help="Data period (e.g., 1y, 6mo). Overrides --start/--end when set.", 48 | ) 49 | parser.add_argument( 50 | "--normalize", 51 | action="store_true", 52 | help="Normalize each series to start at 1 (divide by the first closing price).", 53 | ) 54 | return parser.parse_args() 55 | 56 | 57 | def main(): 58 | args = parse_args() 59 | 60 | if args.period: 61 | data = yf.download( 62 | args.tickers, 63 | period=args.period, 64 | group_by='ticker', 65 | ) 66 | else: 67 | end_date = args.end or dt.datetime.today().strftime("%Y-%m-%d") 68 | start_date = args.start or ( 69 | dt.datetime.today() - dt.timedelta(days=365) 70 | ).strftime("%Y-%m-%d") 71 | data = yf.download( 72 | args.tickers, 73 | start=start_date, 74 | end=end_date, 75 | group_by='ticker', 76 | ) 77 | 78 | plt.figure(figsize=(10, 6)) 79 | if len(args.tickers) > 1: 80 | for ticker in args.tickers: 81 | df = data[ticker] 82 | series = df["Close"] 83 | if args.normalize: 84 | series = series / series.iloc[0] 85 | plt.plot(df.index, series, label=ticker) 86 | else: 87 | series = data["Close"] 88 | if args.normalize: 89 | series = series / series.iloc[0] 90 | plt.plot(data.index, series, label=args.tickers[0]) 91 | 92 | plt.xlabel('Date') 93 | plt.ylabel('Closing Price (USD)') 94 | title = f"EOD Closing Prices for {' '.join(args.tickers)}{' (Normalized)' if args.normalize else ''}" 95 | plt.title(title) 96 | plt.legend() 97 | plt.tight_layout() 98 | plt.grid(True) 99 | plt.show() 100 | 101 | 102 | if __name__ == '__main__': 103 | main() -------------------------------------------------------------------------------- /15_session/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # deploy.sh — automated deployment of the Algorithmic Trading Bootcamp landing page 4 | # Usage: ./deploy.sh 5 | 6 | set -euo pipefail 7 | 8 | if [ "$#" -ne 1 ]; then 9 | echo "Usage: $0 " 10 | exit 1 11 | fi 12 | 13 | SERVER_IP=$1 14 | SSH_DEST=root@${SERVER_IP} 15 | APP_DIR=/opt/bootcamp 16 | SERVICE_NAME=bootcamp 17 | DOMAIN=genai.tpq.io # subdomain for SSL 18 | EMAIL=admin@tpq.io # email for Let's Encrypt notifications 19 | 20 | echo "=== Installing system packages on ${SERVER_IP} ===" 21 | ssh ${SSH_DEST} << 'INSTALL_PACKAGES' 22 | apt update 23 | # optional: 24 | # apt upgrade 25 | apt install -y python3 python3-venv python3-pip nginx certbot python3-certbot-nginx 26 | INSTALL_PACKAGES 27 | 28 | echo "=== Syncing application files to ${SSH_DEST}:${APP_DIR} ===" 29 | rsync -avz --exclude '__pycache__' --exclude '.git' --exclude '.ipynb_checkpoints/' ./ ${SSH_DEST}:${APP_DIR}/ 30 | 31 | echo "=== Configuring application on remote host ===" 32 | ssh ${SSH_DEST} << REMOTE_SETUP 33 | set -euo pipefail 34 | cd ${APP_DIR} 35 | 36 | # Create & activate virtual environment, install dependencies 37 | python3 -m venv venv 38 | source venv/bin/activate 39 | pip install --upgrade pip 40 | pip install -r requirements.txt gunicorn 41 | deactivate 42 | 43 | # systemd service for Gunicorn 44 | cat > /etc/systemd/system/${SERVICE_NAME}.service << SERVICE_EOF 45 | [Unit] 46 | Description=Gunicorn instance to serve Algorithmic Trading Bootcamp 47 | After=network.target 48 | 49 | [Service] 50 | User=www-data 51 | Group=www-data 52 | WorkingDirectory=${APP_DIR} 53 | Environment="PATH=${APP_DIR}/venv/bin" 54 | # start Gunicorn, creating socket with group write via its --umask flag 55 | ExecStart=${APP_DIR}/venv/bin/gunicorn --workers 3 \ 56 | --bind unix:${APP_DIR}/${SERVICE_NAME}.sock \ 57 | --umask 007 app:app 58 | 59 | [Install] 60 | WantedBy=multi-user.target 61 | SERVICE_EOF 62 | 63 | # reload systemd, enable the service, and ensure correct ownership for socket dir 64 | systemctl daemon-reload 65 | systemctl enable ${SERVICE_NAME}.service 66 | chown -R www-data:www-data ${APP_DIR} 67 | # restart Gunicorn so it can bind its socket under correct permissions 68 | systemctl restart ${SERVICE_NAME}.service 69 | 70 | # nginx site configuration 71 | cat > /etc/nginx/sites-available/${SERVICE_NAME} << 'NGINX_EOF' 72 | server { 73 | listen 80; 74 | server_name ${DOMAIN}; 75 | root ${APP_DIR}; 76 | 77 | location / { 78 | proxy_pass http://unix:${APP_DIR}/${SERVICE_NAME}.sock; 79 | proxy_set_header Host \$host; 80 | proxy_set_header X-Real-IP \$remote_addr; 81 | proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; 82 | proxy_set_header X-Forwarded-Proto \$scheme; 83 | } 84 | 85 | location /tpq_logo_bic.png { try_files \$uri =404; } 86 | location /genai_bootcamp.png { try_files \$uri =404; } 87 | } 88 | NGINX_EOF 89 | 90 | ln -sf /etc/nginx/sites-available/${SERVICE_NAME} /etc/nginx/sites-enabled/${SERVICE_NAME} 91 | rm -f /etc/nginx/sites-enabled/default 92 | nginx -t 93 | systemctl restart nginx 94 | 95 | # Obtain/renew SSL certificate via Let's Encrypt 96 | certbot --nginx --noninteractive --agree-tos --email ${EMAIL} \ 97 | -d ${DOMAIN} --redirect 98 | 99 | # ensure proper file ownership 100 | chown -R www-data:www-data ${APP_DIR} 101 | REMOTE_SETUP 102 | 103 | echo "=== Deployment to ${SERVER_IP} completed. ===" 104 | -------------------------------------------------------------------------------- /10_options/prompts/user_prompts_3.txt: -------------------------------------------------------------------------------- 1 | --auto-edit 2 | 3 | Transform the merton_price function in option_pricing/analytics.py to a valuation based on Lewis 2001 and the characteristic function of the Merton 1976 model. Also unify the signature and docstrings of all pricing functions in that module. 4 | 5 | /compact 6 | 7 | Check the merton_price implementation in analytics.py. The values are by far not correct. See the output from scripts/benchmark.py: Merton jump-diffusion European (MC vs analytic): 8 | Type Case MC Price Analytic Abs Err % Err MC Time(s) 9 | ---------------------------------------------------------------------- 10 | Call ATM 11.558846 0.000000 11.558846 nan 0.1786 11 | Call ITM 18.761541 0.000000 18.761541 nan 0.1784 12 | Call OTM 6.023161 0.000000 6.023161 nan 0.1772 13 | Put ATM 6.637481 -4.877058 11.514538 -236.10 0.1779 14 | Put ITM 11.106226 5.122942 5.983284 116.79 0.2409 15 | Put OTM 3.835745 -14.877058 18.712802 -125.78 0.1789 16 | 17 | Transform the signature of heston_price to be in line with those the BSM/Merton functions in analytics.py. 18 | 19 | Analyze the benchmark.py file. Separate out the single benchmarks into several modules. Adjust the order of the cases to ITM-ATM-OTM. Add to the single tables the StdErr values from the MCS right after MC Price. Create a file orchestrate.py that orchestrates the complete benchmark based on the new modules. 20 | 21 | Put the single benchmark modules in a separate sub-folder in scripts/. 22 | 23 | Traceback (most recent call last): 24 | File "/Users/yves/Dropbox/Program/cpf/57_ai_assistants/git/10_options/scripts/benchmarks/benchmark_american.py", line 13, in 25 | from option_pricing.models import BSM 26 | ModuleNotFoundError: No module named 'option_pricing' 27 | 28 | Implement a comprehensive test suite according to the project plan as outlined in the file outline.md 29 | 30 | Write the above highlights into a new file test.md and add relevant details if appropriate. 31 | 32 | When running the tests, getting: tests/test_pricers_european.py:2: in 33 | from option_pricing.models import BSM 34 | E ModuleNotFoundError: No module named 'option_pricing' 35 | 36 | Among others, I get from pytest -q the following: option_pricing/analytics.py:35: ZeroDivisionError 37 | ============================================================================== short test summary info =============================================================================== 38 | FAILED tests/test_monte_carlo.py::test_price_mc_zero_volatility - assert np.float64(1.2688263138573217e-16) == 0.0 39 | FAILED tests/test_pricers_american.py::test_crr_binomial_call_no_dividend_intrinsic - ZeroDivisionError: float division by zero 40 | FAILED tests/test_pricers_european.py::test_european_pricer_matches_bsm[call] - ZeroDivisionError: float division by zero 41 | FAILED tests/test_pricers_european.py::test_european_pricer_matches_bsm[put] - ZeroDivisionError: float division by zero 42 | 4 failed, 11 passed in 0.43s 43 | 44 | > assert stderr == 0.0 45 | E assert np.float64(1.2688263138573217e-16) == 0.0 46 | 47 | tests/test_monte_carlo.py:15: AssertionError 48 | ============================================================================== short test summary info =============================================================================== 49 | FAILED tests/test_monte_carlo.py::test_price_mc_zero_volatility - assert np.float64(1.2688263138573217e-16) == 0.0 50 | 1 failed, 14 passed in 0.39s 51 | 52 | -------------------------------------------------------------------------------- /10_options/scripts/benchmarks/benchmark_heston.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Benchmark Heston stochastic-volatility European options (MC vs semi-analytic) for ITM, ATM, and OTM cases. 4 | """ 5 | 6 | import os 7 | import sys 8 | import time 9 | import argparse 10 | 11 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) 12 | import numpy as np 13 | from mcdxa.models import Heston 14 | from mcdxa.payoffs import CallPayoff, PutPayoff 15 | from mcdxa.pricers.european import EuropeanPricer 16 | from mcdxa.analytics import heston_price 17 | 18 | 19 | def main(): 20 | parser = argparse.ArgumentParser( 21 | description="Benchmark Heston model European options: MC vs semi-analytic" 22 | ) 23 | parser.add_argument("--K", type=float, default=100.0, help="Strike price") 24 | parser.add_argument("--T", type=float, default=1.0, help="Time to maturity") 25 | parser.add_argument("--r", type=float, default=0.05, help="Risk-free rate") 26 | parser.add_argument("--kappa", type=float, default=2.0, help="Mean-reversion speed kappa") 27 | parser.add_argument("--theta", type=float, default=0.04, help="Long-term variance theta") 28 | parser.add_argument("--xi", type=float, default=0.2, help="Vol-of-vol xi") 29 | parser.add_argument("--rho", type=float, default=-0.7, help="Correlation rho") 30 | parser.add_argument("--v0", type=float, default=0.02, help="Initial variance v0") 31 | parser.add_argument("--n_paths", type=int, default=100000, help="Number of Monte Carlo paths") 32 | parser.add_argument("--n_steps", type=int, default=50, help="Number of time steps per path") 33 | parser.add_argument("--seed", type=int, default=42, help="Random seed") 34 | parser.add_argument("--q", type=float, default=0.0, help="Dividend yield") 35 | args = parser.parse_args() 36 | 37 | model = Heston( 38 | args.r, args.kappa, args.theta, args.xi, args.rho, args.v0, q=args.q 39 | ) 40 | moneyness = 0.10 41 | scenarios = [] 42 | for opt_type, payoff_cls in [("call", CallPayoff), ("put", PutPayoff)]: 43 | if opt_type == "call": 44 | S0_cases = [args.K * (1 + moneyness), args.K, args.K * (1 - moneyness)] 45 | else: 46 | S0_cases = [args.K * (1 - moneyness), args.K, args.K * (1 + moneyness)] 47 | for case, S0_case in zip(["ITM", "ATM", "OTM"], S0_cases): 48 | scenarios.append((opt_type, payoff_cls, case, S0_case)) 49 | 50 | header = f"{'Type':<6}{'Case':<6}{'MC Price':>12}{'StdErr':>12}{'Analytic':>14}" 51 | header += f"{'Abs Err':>12}{'% Err':>10}{'Time(s)':>12}" 52 | print(header) 53 | print('-' * len(header)) 54 | 55 | for opt_type, payoff_cls, case, S0_case in scenarios: 56 | payoff = payoff_cls(args.K) 57 | pricer = EuropeanPricer( 58 | model, payoff, n_paths=args.n_paths, n_steps=args.n_steps, seed=args.seed 59 | ) 60 | t0 = time.time() 61 | price_mc, stderr = pricer.price(S0_case, args.T, args.r) 62 | dt = time.time() - t0 63 | 64 | price_an = heston_price( 65 | S0_case, args.K, args.T, args.r, 66 | args.kappa, args.theta, args.xi, args.rho, args.v0, 67 | q=args.q, option_type=opt_type 68 | ) 69 | 70 | abs_err = abs(price_mc - price_an) 71 | pct_err = abs_err / price_an * 100.0 if price_an != 0 else float('nan') 72 | print(f"{opt_type.capitalize():<6}{case:<6}{price_mc:12.6f}{stderr:12.6f}{price_an:14.6f}{abs_err:12.6f}{pct_err:10.2f}{dt:12.4f}") 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /10_options/mcdxa/heston.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.integrate import quad 3 | import math 4 | 5 | # the following Heston pricing implementation is from Gemini, after numerous tries with 6 | # different LLMs, basically none was able to provide a properly working implementation; 7 | # Gemini only came up with that one after having seen my own reference implementation 8 | # from my book "Derivatives Analytics with Python"; basically the first time that 9 | # none of the AI Assistants was able to provide a solution to such a quant finance problem 10 | 11 | def heston_price( 12 | S0: float, 13 | K: float, 14 | T: float, 15 | r: float, 16 | kappa: float, 17 | theta: float, 18 | xi: float, 19 | rho: float, 20 | v0: float, 21 | q: float = 0.0, 22 | option_type: str = "call", 23 | integration_limit: float = 250, 24 | ) -> float: 25 | """ 26 | Heston (1993) model price for European call or put option via Lewis (2001) 27 | single-integral formula. Negative prices are floored at zero. 28 | 29 | Parameters: 30 | - S0: Initial stock price 31 | - K: Strike price 32 | - T: Time to maturity (in years) 33 | - r: Risk-free interest rate 34 | - kappa: Mean reversion rate of variance 35 | - theta: Long-term variance 36 | - xi: Volatility of variance (vol of vol) 37 | - rho: Correlation between stock price and variance processes 38 | - v0: Initial variance 39 | - q: Dividend yield 40 | - option_type: 'call' or 'put' 41 | - integration_limit: Upper bound for numerical integration 42 | 43 | Returns: 44 | - price: Price of the European option (call or put) 45 | """ 46 | 47 | def _lewis_integrand(u, S0, K, T, r, q, kappa, theta, xi, rho, v0): 48 | """The integrand for the Lewis (2001) single-integral formula.""" 49 | 50 | # Calculate the characteristic function value at the complex point u - i/2 51 | char_func_val = _lewis_char_func( 52 | u - 0.5j, T, r, q, kappa, theta, xi, rho, v0) 53 | 54 | # The Lewis formula integrand 55 | integrand = 1 / (u**2 + 0.25) * \ 56 | (np.exp(1j * u * np.log(S0 / K)) * char_func_val).real 57 | 58 | return integrand 59 | 60 | def _lewis_char_func(u, T, r, q, kappa, theta, xi, rho, v0): 61 | """The Heston characteristic function of the log-price.""" 62 | 63 | d = np.sqrt((kappa - rho * xi * u * 1j)**2 + (u**2 + u * 1j) * xi**2) 64 | 65 | g = (kappa - rho * xi * u * 1j - d) / (kappa - rho * xi * u * 1j + d) 66 | 67 | C = (r - q) * u * 1j * T + (kappa * theta / xi**2) * ( 68 | (kappa - rho * xi * u * 1j - d) * T - 2 * 69 | np.log((1 - g * np.exp(-d * T)) / (1 - g)) 70 | ) 71 | 72 | D = ((kappa - rho * xi * u * 1j - d) / xi**2) * \ 73 | ((1 - np.exp(-d * T)) / (1 - g * np.exp(-d * T))) 74 | 75 | return np.exp(C + D * v0) 76 | 77 | # Perform the integration 78 | integral_value = quad( 79 | lambda u: _lewis_integrand( 80 | u, S0, K, T, r, q, kappa, theta, xi, rho, v0), 81 | 0, 82 | integration_limit 83 | )[0] 84 | 85 | # Calculate the final call price using the Lewis formula 86 | call_price = S0 * np.exp(-q * T) - np.exp(-r * T) * \ 87 | np.sqrt(S0 * K) / np.pi * integral_value 88 | 89 | if option_type == "call": 90 | price = call_price 91 | elif option_type == "put": 92 | price = call_price - S0 * math.exp(-q * T) + K * math.exp(-r * T) 93 | else: 94 | raise ValueError("Option type must be 'call' or 'put'.") 95 | 96 | return max(price, 0.0) 97 | -------------------------------------------------------------------------------- /01_session/bsm_codex.py: -------------------------------------------------------------------------------- 1 | """ 2 | bsm_codex.py 3 | Black-Scholes-Merton option pricing formula implementation for European calls and puts. 4 | """ 5 | import math 6 | import unittest 7 | 8 | class BlackScholesMerton: 9 | """ 10 | Black-Scholes-Merton model for European option pricing. 11 | 12 | Attributes: 13 | S (float): Current price of the underlying asset. 14 | K (float): Strike price of the option. 15 | T (float): Time to maturity in years. 16 | r (float): Risk-free interest rate (annualized, continuous compounding). 17 | sigma (float): Volatility of the underlying asset (annualized). 18 | q (float): Dividend yield of the underlying asset (annualized, continuous compounding). 19 | """ 20 | 21 | def __init__(self, S, K, T, r, sigma, q=0.0): 22 | self.S = float(S) 23 | self.K = float(K) 24 | self.T = float(T) 25 | self.r = float(r) 26 | self.sigma = float(sigma) 27 | self.q = float(q) 28 | 29 | @staticmethod 30 | def _cdf(x): 31 | """ 32 | Standard normal cumulative distribution function. 33 | """ 34 | return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0))) 35 | 36 | def _d1(self): 37 | """ 38 | Calculate d1 in the BSM formula. 39 | """ 40 | return ( 41 | math.log(self.S / self.K) 42 | + (self.r - self.q + 0.5 * self.sigma ** 2) * self.T 43 | ) / (self.sigma * math.sqrt(self.T)) 44 | 45 | def _d2(self): 46 | """ 47 | Calculate d2 in the BSM formula. 48 | """ 49 | return self._d1() - self.sigma * math.sqrt(self.T) 50 | 51 | def call_price(self): 52 | """ 53 | Calculate the European call option price. 54 | 55 | Returns: 56 | float: Call option price. 57 | """ 58 | d1 = self._d1() 59 | d2 = self._d2() 60 | return ( 61 | self.S * math.exp(-self.q * self.T) * self._cdf(d1) 62 | - self.K * math.exp(-self.r * self.T) * self._cdf(d2) 63 | ) 64 | 65 | def put_price(self): 66 | """ 67 | Calculate the European put option price. 68 | 69 | Returns: 70 | float: Put option price. 71 | """ 72 | d1 = self._d1() 73 | d2 = self._d2() 74 | return ( 75 | self.K * math.exp(-self.r * self.T) * self._cdf(-d2) 76 | - self.S * math.exp(-self.q * self.T) * self._cdf(-d1) 77 | ) 78 | 79 | 80 | class TestBlackScholesMerton(unittest.TestCase): 81 | """Unit tests for BlackScholesMerton model.""" 82 | 83 | def test_call_price(self): 84 | bsm = BlackScholesMerton(S=100, K=100, T=1, r=0.05, sigma=0.2) 85 | price = bsm.call_price() 86 | # Known value ~10.4506 87 | self.assertAlmostEqual(price, 10.4506, places=4) 88 | 89 | def test_put_price(self): 90 | bsm = BlackScholesMerton(S=100, K=100, T=1, r=0.05, sigma=0.2) 91 | price = bsm.put_price() 92 | # Known value ~5.5735 93 | self.assertAlmostEqual(price, 5.5735, places=4) 94 | 95 | def test_put_call_parity(self): 96 | bsm = BlackScholesMerton(S=50, K=55, T=0.5, r=0.03, sigma=0.25, q=0.01) 97 | call = bsm.call_price() 98 | put = bsm.put_price() 99 | # Call - Put should equal forward price difference 100 | lhs = call - put 101 | rhs = bsm.S * math.exp(-bsm.q * bsm.T) - bsm.K * math.exp(-bsm.r * bsm.T) 102 | self.assertAlmostEqual(lhs, rhs, places=8) 103 | 104 | 105 | if __name__ == "__main__": # pragma: no cover 106 | unittest.main() -------------------------------------------------------------------------------- /04_session/backtest_perplexity.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | from datetime import datetime 4 | import matplotlib.pyplot as plt 5 | 6 | # --- Load Data --- 7 | 8 | # Load price data 9 | price_df = pd.read_csv('aia_eod_data.csv', parse_dates=['Date']) 10 | price_df.set_index('Date', inplace=True) 11 | price_df = price_df[['NVDA', 'MSFT', 'AMZN', 'INTC', 'GE', 'NFLX']].dropna() 12 | 13 | # Align dates for all stocks 14 | price_df = price_df.dropna() 15 | 16 | # --- Portfolio Construction --- 17 | 18 | # Define portfolio constituents 19 | longs = ['NVDA', 'MSFT', 'AMZN'] 20 | shorts = ['INTC', 'GE', 'NFLX'] 21 | 22 | # Equal weights for each leg (dollar neutral, 3 long, 3 short) 23 | n_long = len(longs) 24 | n_short = len(shorts) 25 | long_weights = np.array([1.0 / n_long] * n_long) 26 | short_weights = np.array([-1.0 / n_short] * n_short) 27 | 28 | # --- Compute Daily Returns --- 29 | 30 | returns = price_df.pct_change().dropna() 31 | 32 | # --- Compute Portfolio Returns --- 33 | 34 | # Long leg 35 | long_returns = returns[longs].dot(long_weights) 36 | # Short leg 37 | short_returns = returns[shorts].dot(short_weights) 38 | # Total L/S portfolio 39 | portfolio_returns = long_returns + short_returns 40 | 41 | # --- Compute Cumulative Returns --- 42 | 43 | cum_returns = (1 + portfolio_returns).cumprod() 44 | 45 | # --- Compute Performance Metrics --- 46 | 47 | def annualized_return(r, periods_per_year=252): 48 | return (1 + r.mean())**periods_per_year - 1 49 | 50 | def annualized_volatility(r, periods_per_year=252): 51 | return r.std() * np.sqrt(periods_per_year) 52 | 53 | def sharpe_ratio(r, risk_free=0.0, periods_per_year=252): 54 | ann_ret = annualized_return(r, periods_per_year) 55 | ann_vol = annualized_volatility(r, periods_per_year) 56 | return (ann_ret - risk_free) / ann_vol if ann_vol > 0 else np.nan 57 | 58 | def max_drawdown(cum_ret): 59 | roll_max = cum_ret.cummax() 60 | dd = cum_ret / roll_max - 1 61 | return dd.min() 62 | 63 | # Calculate statistics 64 | ann_ret = annualized_return(portfolio_returns) 65 | ann_vol = annualized_volatility(portfolio_returns) 66 | sharpe = sharpe_ratio(portfolio_returns) 67 | mdd = max_drawdown(cum_returns) 68 | 69 | # --- Output Statistics --- 70 | 71 | print("L/S Portfolio Backtest Results (May 2015 - Jun 2025)") 72 | print(f"Total Return: {cum_returns.iloc[-1]-1:.2%}") 73 | print(f"Annualized Return: {ann_ret:.2%}") 74 | print(f"Annualized Volatility: {ann_vol:.2%}") 75 | print(f"Sharpe Ratio: {sharpe:.2f}") 76 | print(f"Max Drawdown: {mdd:.2%}") 77 | print(f"Mean Daily Return: {portfolio_returns.mean():.4%}") 78 | print(f"Std Daily Return: {portfolio_returns.std():.4%}") 79 | 80 | # --- Plot Cumulative Returns --- 81 | 82 | plt.figure(figsize=(12,6)) 83 | plt.plot(cum_returns, label='L/S Portfolio') 84 | plt.title('Long/Short Portfolio Cumulative Returns') 85 | plt.xlabel('Date') 86 | plt.ylabel('Cumulative Return (Gross)') 87 | plt.legend() 88 | plt.grid(True) 89 | plt.tight_layout() 90 | plt.show() 91 | 92 | # --- Optional: Compare to S&P 500 ETF (SPY) --- 93 | 94 | spy = pd.read_csv('aia_spy_prices.csv', parse_dates=['Date']) 95 | spy.set_index('Date', inplace=True) 96 | spy = spy.reindex(price_df.index).dropna() 97 | spy_returns = spy['SPY.US'].pct_change().dropna() 98 | spy_cum = (1 + spy_returns).cumprod() 99 | 100 | plt.figure(figsize=(12,6)) 101 | plt.plot(cum_returns, label='L/S Portfolio') 102 | plt.plot(spy_cum, label='SPY ETF') 103 | plt.title('L/S Portfolio vs SPY ETF') 104 | plt.xlabel('Date') 105 | plt.ylabel('Cumulative Return (Gross)') 106 | plt.legend() 107 | plt.grid(True) 108 | plt.tight_layout() 109 | plt.show() 110 | -------------------------------------------------------------------------------- /10_options/mcdxa/payoffs.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Payoff: 5 | """Base class for payoff definitions.""" 6 | def __call__(self, S: np.ndarray) -> np.ndarray: 7 | raise NotImplementedError 8 | 9 | 10 | class CustomPayoff(Payoff): 11 | """ 12 | Custom payoff defined by an arbitrary function of the terminal asset price. 13 | 14 | Args: 15 | func (callable): Function mapping terminal price array (n_paths,) 16 | or scalar to payoff values. The function should accept a numpy 17 | array or scalar and return an array or scalar of payoffs. 18 | 19 | Example: 20 | # payoff = max(sqrt(S_T) - K, 0) 21 | payoff = CustomPayoff(lambda s: np.maximum(np.sqrt(s) - K, 0)) 22 | """ 23 | def __init__(self, func): 24 | if not callable(func): 25 | raise TypeError(f"func must be callable, got {type(func)}") 26 | self.func = func 27 | 28 | def __call__(self, S: np.ndarray) -> np.ndarray: 29 | S = np.asarray(S) 30 | # extract terminal price if full path provided 31 | S_end = S[:, -1] if S.ndim == 2 else S 32 | # apply custom function to terminal prices 33 | return np.asarray(self.func(S_end)) 34 | 35 | 36 | class CallPayoff(Payoff): 37 | """European call option payoff.""" 38 | def __init__(self, strike: float): 39 | self.strike = strike 40 | 41 | def __call__(self, S: np.ndarray) -> np.ndarray: 42 | S = np.asarray(S) 43 | # handle terminal price if full path provided 44 | S_end = S[:, -1] if S.ndim == 2 else S 45 | return np.maximum(S_end - self.strike, 0.0) 46 | 47 | 48 | class PutPayoff(Payoff): 49 | """European put option payoff.""" 50 | def __init__(self, strike: float): 51 | self.strike = strike 52 | 53 | def __call__(self, S: np.ndarray) -> np.ndarray: 54 | S = np.asarray(S) 55 | S_end = S[:, -1] if S.ndim == 2 else S 56 | return np.maximum(self.strike - S_end, 0.0) 57 | 58 | 59 | class AsianCallPayoff(Payoff): 60 | """Arithmetic Asian (path-dependent) European call payoff.""" 61 | def __init__(self, strike: float): 62 | self.strike = strike 63 | 64 | def __call__(self, S: np.ndarray) -> np.ndarray: 65 | S = np.asarray(S) 66 | # average price over the path 67 | avg = S.mean(axis=1) if S.ndim == 2 else S 68 | return np.maximum(avg - self.strike, 0.0) 69 | 70 | 71 | class AsianPutPayoff(Payoff): 72 | """Arithmetic Asian (path-dependent) European put payoff.""" 73 | def __init__(self, strike: float): 74 | self.strike = strike 75 | 76 | def __call__(self, S: np.ndarray) -> np.ndarray: 77 | S = np.asarray(S) 78 | avg = S.mean(axis=1) if S.ndim == 2 else S 79 | return np.maximum(self.strike - avg, 0.0) 80 | 81 | 82 | class LookbackCallPayoff(Payoff): 83 | """Lookback (path-dependent) European call payoff (max(S) - strike).""" 84 | def __init__(self, strike: float): 85 | self.strike = strike 86 | 87 | def __call__(self, S: np.ndarray) -> np.ndarray: 88 | S = np.asarray(S) 89 | high = S.max(axis=1) if S.ndim == 2 else S 90 | return np.maximum(high - self.strike, 0.0) 91 | 92 | 93 | class LookbackPutPayoff(Payoff): 94 | """Lookback (path-dependent) European put payoff (strike - min(S)).""" 95 | def __init__(self, strike: float): 96 | self.strike = strike 97 | 98 | def __call__(self, S: np.ndarray) -> np.ndarray: 99 | S = np.asarray(S) 100 | low = S.min(axis=1) if S.ndim == 2 else S 101 | return np.maximum(self.strike - low, 0.0) 102 | -------------------------------------------------------------------------------- /10_options/scripts/benchmarks/benchmark_mjd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Benchmark Merton jump-diffusion European options (MC vs analytic) for ITM, ATM, and OTM cases. 4 | """ 5 | 6 | import os 7 | import sys 8 | import time 9 | import math 10 | import argparse 11 | 12 | sys.path.insert(0, os.path.abspath( 13 | os.path.join(os.path.dirname(__file__), '..', '..'))) 14 | import numpy as np 15 | from mcdxa.models import Merton 16 | from mcdxa.payoffs import CallPayoff, PutPayoff 17 | from mcdxa.pricers.european import EuropeanPricer 18 | from mcdxa.analytics import merton_price 19 | 20 | 21 | def main(): 22 | parser = argparse.ArgumentParser( 23 | description="Benchmark Merton jump-diffusion European options: MC vs analytic" 24 | ) 25 | parser.add_argument("--K", type=float, default=100.0, help="Strike price") 26 | parser.add_argument("--T", type=float, default=1.0, 27 | help="Time to maturity") 28 | parser.add_argument("--r", type=float, default=0.05, help="Risk-free rate") 29 | parser.add_argument("--sigma", type=float, default=0.2, 30 | help="Diffusion volatility") 31 | parser.add_argument("--lam", type=float, default=0.3, 32 | help="Jump intensity (lambda)") 33 | parser.add_argument("--mu_j", type=float, default=- 34 | 0.1, help="Mean jump size (mu_j)") 35 | parser.add_argument("--sigma_j", type=float, default=0.2, 36 | help="Jump volatility (sigma_j)") 37 | parser.add_argument("--n_paths", type=int, default=100000, 38 | help="Number of Monte Carlo paths") 39 | parser.add_argument("--n_steps", type=int, default=50, 40 | help="Number of time steps per path") 41 | parser.add_argument("--seed", type=int, default=42, help="Random seed") 42 | parser.add_argument("--q", type=float, default=0.0, help="Dividend yield") 43 | args = parser.parse_args() 44 | 45 | model = Merton( 46 | args.r, args.sigma, args.lam, args.mu_j, args.sigma_j, q=args.q 47 | ) 48 | moneyness = 0.10 49 | scenarios = [] 50 | for opt_type, payoff_cls in [("call", CallPayoff), ("put", PutPayoff)]: 51 | if opt_type == "call": 52 | S0_cases = [args.K * (1 + moneyness), args.K, 53 | args.K * (1 - moneyness)] 54 | else: 55 | S0_cases = [args.K * (1 - moneyness), args.K, 56 | args.K * (1 + moneyness)] 57 | for case, S0_case in zip(["ITM", "ATM", "OTM"], S0_cases): 58 | scenarios.append((opt_type, payoff_cls, case, S0_case)) 59 | 60 | header = f"{'Type':<6}{'Case':<6}{'MC Price':>12}{'StdErr':>12}{'Analytic':>12}{'Abs Err':>12}{'% Err':>10}{'Time(s)':>12}" 61 | print(header) 62 | print('-' * len(header)) 63 | 64 | for opt_type, payoff_cls, case, S0_case in scenarios: 65 | payoff = payoff_cls(args.K) 66 | pricer = EuropeanPricer( 67 | model, payoff, n_paths=args.n_paths, n_steps=args.n_steps, seed=args.seed 68 | ) 69 | t0 = time.time() 70 | price_mc, stderr = pricer.price(S0_case, args.T, args.r) 71 | dt = time.time() - t0 72 | 73 | price_an = merton_price( 74 | S0_case, args.K, args.T, args.r, args.sigma, 75 | args.lam, args.mu_j, args.sigma_j, 76 | q=args.q, option_type=opt_type 77 | ) 78 | 79 | abs_err = abs(price_mc - price_an) 80 | pct_err = abs_err / price_an * 100.0 if price_an != 0 else float('nan') 81 | print(f"{opt_type.capitalize():<6}{case:<6}{price_mc:12.6f}{stderr:12.6f}{price_an:12.6f}{abs_err:12.6f}{pct_err:10.2f}{dt:12.4f}") 82 | 83 | 84 | if __name__ == "__main__": 85 | main() 86 | -------------------------------------------------------------------------------- /10_options/scripts/benchmarks/benchmark_bates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Benchmark Bates (1996) jump-diffusion with stochastic volatility European options 4 | (Heston+Merton jumps): MC vs semi-analytic via Lewis (2001) 5 | """ 6 | 7 | import os 8 | import sys 9 | import time 10 | import argparse 11 | 12 | sys.path.insert(0, os.path.abspath( 13 | os.path.join(os.path.dirname(__file__), '..', '..'))) 14 | import numpy as np 15 | from mcdxa.models import Bates 16 | from mcdxa.payoffs import CallPayoff, PutPayoff 17 | from mcdxa.pricers.european import EuropeanPricer 18 | from mcdxa.analytics import bates_price 19 | 20 | 21 | def main(): 22 | parser = argparse.ArgumentParser( 23 | description="Benchmark Bates model European options: MC vs semi-analytic" 24 | ) 25 | parser.add_argument("--K", type=float, default=100.0, help="Strike price") 26 | parser.add_argument("--T", type=float, default=1.0, help="Time to maturity") 27 | parser.add_argument("--r", type=float, default=0.05, help="Risk-free rate") 28 | parser.add_argument("--kappa", type=float, default=2.0, help="Heston mean-reversion speed kappa") 29 | parser.add_argument("--theta", type=float, default=0.04, help="Heston long-term variance theta") 30 | parser.add_argument("--xi", type=float, default=0.2, help="Heston vol-of-vol xi") 31 | parser.add_argument("--rho", type=float, default=-0.7, help="Heston correlation rho") 32 | parser.add_argument("--v0", type=float, default=0.02, help="Heston initial variance v0") 33 | parser.add_argument("--lam", type=float, default=0.3, help="Jump intensity lambda") 34 | parser.add_argument("--mu_j", type=float, default=-0.1, help="Jump mean mu_j") 35 | parser.add_argument("--sigma_j", type=float, default=0.2, help="Jump volatility sigma_j") 36 | parser.add_argument("--n_paths", type=int, default=100000, help="Number of Monte Carlo paths") 37 | parser.add_argument("--n_steps", type=int, default=50, help="Number of time steps per path") 38 | parser.add_argument("--seed", type=int, default=42, help="Random seed") 39 | parser.add_argument("--q", type=float, default=0.0, help="Dividend yield") 40 | args = parser.parse_args() 41 | 42 | model = Bates( 43 | args.r, args.kappa, args.theta, args.xi, args.rho, 44 | args.v0, args.lam, args.mu_j, args.sigma_j, q=args.q 45 | ) 46 | moneyness = 0.10 47 | scenarios = [] 48 | for opt_type, payoff_cls in [("call", CallPayoff), ("put", PutPayoff)]: 49 | if opt_type == "call": 50 | S0_cases = [args.K * (1 + moneyness), args.K, args.K * (1 - moneyness)] 51 | else: 52 | S0_cases = [args.K * (1 - moneyness), args.K, args.K * (1 + moneyness)] 53 | for case, S0_case in zip(["ITM", "ATM", "OTM"], S0_cases): 54 | scenarios.append((opt_type, payoff_cls, case, S0_case)) 55 | 56 | header = f"{'Type':<6}{'Case':<6}{'MC Price':>12}{'StdErr':>12}{'Analytic':>12}{'Abs Err':>12}{'% Err':>10}{'Time(s)':>12}" 57 | print(header) 58 | print('-' * len(header)) 59 | 60 | for opt_type, payoff_cls, case, S0_case in scenarios: 61 | payoff = payoff_cls(args.K) 62 | pricer = EuropeanPricer( 63 | model, payoff, n_paths=args.n_paths, n_steps=args.n_steps, seed=args.seed 64 | ) 65 | t0 = time.time() 66 | price_mc, stderr = pricer.price(S0_case, args.T, args.r) 67 | dt = time.time() - t0 68 | 69 | price_an = bates_price( 70 | S0_case, args.K, args.T, args.r, 71 | args.kappa, args.theta, args.xi, args.rho, args.v0, 72 | args.lam, args.mu_j, args.sigma_j, 73 | q=args.q, option_type=opt_type 74 | ) 75 | 76 | abs_err = abs(price_mc - price_an) 77 | pct_err = abs_err / price_an * 100.0 if price_an != 0 else float('nan') 78 | print(f"{opt_type.capitalize():<6}{case:<6}{price_mc:12.6f}{stderr:12.6f}{price_an:12.6f}{abs_err:12.6f}{pct_err:10.2f}{dt:12.4f}") 79 | 80 | 81 | if __name__ == "__main__": 82 | main() 83 | -------------------------------------------------------------------------------- /10_options/outline.md: -------------------------------------------------------------------------------- 1 | # Outline for Monte Carlo Option Pricing Package 2 | 3 | Below is a concise, orthogonal outline for implementing a Python package to price European and American options with arbitrary payoffs via Monte Carlo simulation. 4 | 5 | ## 1. Top-level Tree 6 | 7 | ``` 8 | option_pricing/ 9 | ├── LICENSE 10 | ├── README.md 11 | ├── pyproject.toml 12 | ├── docs/ 13 | │ └── ... 14 | ├── option_pricing/ 15 | │ ├── __init__.py 16 | │ ├── models.py 17 | │ ├── payoffs.py 18 | │ ├── monte_carlo.py 19 | │ ├── pricers/ 20 | │ │ ├── __init__.py 21 | │ │ ├── european.py 22 | │ │ └── american.py 23 | │ ├── utils.py 24 | │ └── exceptions.py 25 | └── tests/ 26 | ├── conftest.py 27 | ├── test_models.py 28 | ├── test_payoffs.py 29 | ├── test_monte_carlo.py 30 | ├── test_pricers_european.py 31 | └── test_pricers_american.py 32 | ``` 33 | 34 | ## 2. Module Responsibilities 35 | 36 | | Module | Responsibility | 37 | |-------------------------|-------------------------------------------------------------------------------| 38 | | **models.py** | Define stochastic processes (e.g. GBM, Heston) and path generators. | 39 | | **payoffs.py** | Implement payoff functions (vanilla, digital, custom lambdas), vectorized. | 40 | | **monte_carlo.py** | Core Monte Carlo engine: time grid, batching, variance reduction, statistics. | 41 | | **pricers/european.py** | `EuropeanPricer`: ties Model, Payoff, MC engine to produce prices & Greeks. | 42 | | **pricers/american.py** | `AmericanPricer`: implements early exercise (LSM, dual methods). | 43 | | **utils.py** | Helpers: discounting, date math, RNG seeding, logging, progress bars. | 44 | | **exceptions.py** | Custom exceptions for clearer error handling (e.g. ModelError, PayoffError). | 45 | 46 | ## 3. Project-level Files 47 | 48 | | File | Purpose | 49 | |-----------------|-----------------------------------------------------------------------| 50 | | `pyproject.toml`| Build system & dependencies (Poetry/PEP 518 or setup.py/config). | 51 | | `README.md` | Overview, install instructions, basic usage examples. | 52 | | `docs/` | User guide, API reference (Sphinx or MkDocs). | 53 | | `tests/` | Unit tests covering all modules, edge cases, Greeks, convergence. | 54 | | `LICENSE` | (e.g. MIT, Apache-2.0) if open-sourcing. | 55 | 56 | ## 4. Suggested Development Workflow 57 | 58 | 1. **Bootstrap the project** 59 | - Initialize `pyproject.toml` or `setup.py` and docs scaffold. 60 | - Set up linters and pre-commit (black, isort, flake8). 61 | 62 | 2. **Implement core building blocks** 63 | - `models.py`: start with GBM path generator. 64 | - `payoffs.py`: vanilla calls/puts and lambda interface. 65 | - `monte_carlo.py`: generic engine accepting Model + Payoff. 66 | 67 | 3. **Write unit tests in parallel** 68 | - Compare European MC prices against known analytic results. 69 | - Test path shapes, vectorized payoffs, convergence. 70 | 71 | 4. **Add pricers** 72 | - `EuropeanPricer`: wrap MC engine, add Greeks (bump or pathwise). 73 | - `AmericanPricer`: implement Longstaff-Schwartz or dual method. 74 | 75 | 5. **Enhance features** 76 | - Additional models (local vol, multi-asset). 77 | - More variance-reduction (control variates, QMC). 78 | - Advanced payoff compositions (barriers, baskets). 79 | 80 | 6. **Documentation & examples** 81 | - Tutorial notebooks (European vs. American, Greeks, convergence). 82 | - API docs and extension guide. 83 | 84 | ## 5. Why This Layout? 85 | 86 | - **Separation of concerns**: extend payoffs without touching MC engine or pricers. 87 | - **Orthogonality**: each module has a single, well-defined responsibility. 88 | - **Testability**: fine-grained modules simplify unit testing. 89 | - **Flexibility**: users can inject custom models, payoffs, and variance-reduction techniques. -------------------------------------------------------------------------------- /10_options/mcdxa/pricers/american.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | 4 | 5 | class AmericanBinomialPricer: 6 | """ 7 | Cox-Ross-Rubinstein binomial pricer for American options. 8 | 9 | Attributes: 10 | model: Asset price model with attributes r, sigma, q. 11 | payoff: Payoff callable. 12 | n_steps: Number of binomial steps. 13 | """ 14 | def __init__(self, model, payoff, n_steps: int = 200): 15 | self.model = model 16 | self.payoff = payoff 17 | self.n_steps = n_steps 18 | 19 | def price(self, S0: float, T: float, r: float) -> float: 20 | """ 21 | Price the American option using the CRR binomial model. 22 | 23 | Args: 24 | S0 (float): Initial asset price. 25 | T (float): Time to maturity. 26 | r (float): Risk-free rate. 27 | 28 | Returns: 29 | float: American option price. 30 | """ 31 | sigma = self.model.sigma 32 | # degenerate zero-volatility: immediate exercise 33 | if sigma <= 0 or T <= 0: 34 | return float(self.payoff(S0)) 35 | q = getattr(self.model, 'q', 0.0) 36 | n = self.n_steps 37 | dt = T / n 38 | u = math.exp(sigma * math.sqrt(dt)) 39 | d = 1 / u 40 | disc = math.exp(-r * dt) 41 | p = (math.exp((r - q) * dt) - d) / (u - d) 42 | 43 | prices = [S0 * (u ** (n - j)) * (d ** j) for j in range(n + 1)] 44 | values = [float(self.payoff(price)) for price in prices] 45 | 46 | for i in range(n - 1, -1, -1): 47 | for j in range(i + 1): 48 | cont = disc * (p * values[j] + (1 - p) * values[j + 1]) 49 | exercise = float(self.payoff(S0 * (u ** (i - j)) * (d ** j))) 50 | values[j] = max(exercise, cont) 51 | return values[0] 52 | 53 | 54 | class LongstaffSchwartzPricer: 55 | """ 56 | Longstaff-Schwartz least-squares Monte Carlo pricer for American options. 57 | 58 | Attributes: 59 | model: Asset price model with simulate method. 60 | payoff: Payoff callable (vectorized). 61 | n_paths: Number of Monte Carlo paths. 62 | n_steps: Number of time steps per path. 63 | rng: numpy random generator. 64 | """ 65 | def __init__(self, model, payoff, n_paths: int = 100_000, 66 | n_steps: int = 50, seed: int = None): 67 | self.model = model 68 | self.payoff = payoff 69 | self.n_paths = n_paths 70 | self.n_steps = n_steps 71 | self.rng = None if seed is None else np.random.default_rng(seed) 72 | 73 | def price(self, S0: float, T: float, r: float) -> tuple: 74 | """ 75 | Price the American option via Least-Squares Monte Carlo. 76 | 77 | Args: 78 | S0: Initial asset price. 79 | T: Time to maturity. 80 | r: Risk-free rate. 81 | 82 | Returns: 83 | (price, stderr): discounted price and its standard error. 84 | """ 85 | dt = T / self.n_steps 86 | paths = self.model.simulate(S0, T, self.n_paths, self.n_steps, rng=self.rng) 87 | n_paths, _ = paths.shape 88 | cashflow = self.payoff(paths[:, -1]) 89 | tau = np.full(n_paths, self.n_steps, dtype=int) 90 | 91 | disc = math.exp(-r * dt) 92 | for t in range(self.n_steps - 1, 0, -1): 93 | St = paths[:, t] 94 | immediate = self.payoff(St) 95 | itm = immediate > 0 96 | if not np.any(itm): 97 | continue 98 | Y = cashflow[itm] * (disc ** (tau[itm] - t)) 99 | X = St[itm] 100 | A = np.vstack([np.ones_like(X), X, X**2]).T 101 | coeffs, *_ = np.linalg.lstsq(A, Y, rcond=None) 102 | continuation = coeffs[0] + coeffs[1] * X + coeffs[2] * X**2 103 | exercise = immediate[itm] > continuation 104 | idx = np.where(itm)[0][exercise] 105 | cashflow[idx] = immediate[idx] 106 | tau[idx] = t 107 | 108 | discounts = np.exp(-r * dt * tau) 109 | discounted = cashflow * discounts 110 | price = discounted.mean() 111 | stderr = discounted.std(ddof=1) / np.sqrt(self.n_paths) 112 | return price, stderr 113 | -------------------------------------------------------------------------------- /10_options/prompts/user_prompts_4.txt: -------------------------------------------------------------------------------- 1 | Separate the valuation function of option_pricing/analytics.py into separate Python modules and manage their import via the analytics.py file afterward. 2 | 3 | python option_pricing/analytics.py 4 | Traceback (most recent call last): 5 | File "/Users/yves/Dropbox/Program/cpf/57_ai_assistants/git/10_options/option_pricing/analytics.py", line 1, in 6 | from .bsm import norm_cdf, bsm_price 7 | ImportError: attempted relative import with no known parent package 8 | 9 | Now add a simulation function and semi-analytical valuation function according to Lewis (2001), based on the characteristic function, for the Bates (1996) pricing model. 10 | 11 | Move the simulation function to the models.py file and put it in the same format (class) as the other simulation models. 12 | 13 | python option_pricing/analytics.py 14 | Traceback (most recent call last): 15 | File "/Users/yves/Dropbox/Program/cpf/57_ai_assistants/git/10_options/option_pricing/analytics.py", line 2, in 16 | from option_pricing.bsm import norm_cdf, bsm_price 17 | ModuleNotFoundError: No module named 'option_pricing' 18 | 19 | During handling of the above exception, another exception occurred: 20 | 21 | Traceback (most recent call last): 22 | File "/Users/yves/Dropbox/Program/cpf/57_ai_assistants/git/10_options/option_pricing/analytics.py", line 11, in 23 | from bates import bates_price 24 | File "/Users/yves/Dropbox/Program/cpf/57_ai_assistants/git/10_options/option_pricing/bates.py", line 4, in 25 | from option_pricing.models import Bates 26 | ModuleNotFoundError: No module named 'option_pricing' 27 | 28 | Check why orchestrate.py shows completely wrong MCS values for MJD: Running benchmarks/benchmark_mjd.py... 29 | 30 | Type Case MC Price StdErr Analytic Abs Err % Err Time(s) 31 | ---------------------------------------------------------------------------------- 32 | Call ITM 0.000000 0.000000 18.704176 18.704176 100.00 0.1205 33 | Call ATM 0.000000 0.000000 11.503332 11.503332 100.00 0.1213 34 | Call OTM 0.000000 0.000000 5.974967 5.974967 100.00 0.1191 35 | Put ITM 95.122942 0.000000 11.097910 84.025033 757.12 0.1182 36 | Put ATM 95.122942 0.000000 6.626274 88.496668 1335.54 0.1198 37 | Put OTM 95.122942 0.000000 3.827118 91.295824 2385.50 0.1220 38 | 39 | Add a benchmark in the same way/format for Bates (1996) to the orchestrate.py file -- via a separate benchmarking file for that model. 40 | 41 | Check why the pricing errors/value differences for the Bates model according to orchestrate.py are so large. Maybe reference the semi-analytical models for Merton/Heston for the Lewis implementation. Also check correct parametrization. Type Case MC Price StdErr Analytic Abs Err % Err Time(s) 42 | ---------------------------------------------------------------------------------- 43 | Call ITM 18.325602 0.055938 18.843255 0.517654 2.75 0.2796 44 | Call ATM 10.782430 0.042944 11.573345 0.790915 6.83 0.2726 45 | Call OTM 4.944276 0.028794 6.108145 1.163870 19.05 0.2705 46 | Put ITM 10.073529 0.039637 11.231088 1.157559 10.31 0.2739 47 | Put ATM 5.912384 0.032768 6.696287 0.783903 11.71 0.2770 48 | Put OTM 3.456257 0.026030 3.966198 0.509941 12.86 0.2753 49 | 50 | Adjust the orchestrate.py file so that all relevant parameters/arguments for the single benchmark files can be passed to it directly. 51 | 52 | Update the test suite to include the added simulation/valuation functions for Bates. 53 | 54 | Change the package name to mcdxa. 55 | 56 | Update all tests to reflect the new naming/importing: ====================================================================== short test summary info ======================================================================= 57 | ERROR tests/test_bates.py 58 | ERROR tests/test_models.py 59 | ERROR tests/test_monte_carlo.py 60 | ERROR tests/test_payoffs.py 61 | ERROR tests/test_pricers_american.py 62 | ERROR tests/test_pricers_european.py 63 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 6 errors during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 64 | 6 errors in 0.18s 65 | (codex) studio:10_options yves$ python scripts/orchestrate.py 66 | 67 | Update all package references in the benchmark files. 68 | 69 | -------------------------------------------------------------------------------- /08_session/sma_browser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Streamlit web app for SMA crossover backtesting. 4 | 5 | Dependencies: 6 | pip install streamlit yfinance matplotlib numpy python-dateutil 7 | 8 | Usage: 9 | streamlit run sma_browser.py 10 | """ 11 | 12 | import streamlit as st 13 | import yfinance as yf 14 | import numpy as np 15 | import matplotlib.pyplot as plt 16 | 17 | from datetime import date 18 | from dateutil.relativedelta import relativedelta 19 | 20 | 21 | def main(): 22 | st.title("SMA Crossover Backtest") 23 | 24 | st.sidebar.header("Parameters") 25 | ticker = st.sidebar.text_input("Ticker", "SPY").upper() 26 | months = st.sidebar.number_input( 27 | "Months to retrieve", min_value=1, value=36, step=1 28 | ) 29 | fast_window = st.sidebar.number_input( 30 | "Fast SMA window (days)", min_value=1, value=20, step=1 31 | ) 32 | slow_window = st.sidebar.number_input( 33 | "Slow SMA window (days)", min_value=1, value=50, step=1 34 | ) 35 | strategy_type = st.sidebar.selectbox( 36 | "Strategy type", ["long-only", "long-short"] 37 | ) 38 | 39 | if fast_window >= slow_window: 40 | st.sidebar.error("Fast SMA window must be less than slow SMA window.") 41 | return 42 | 43 | end_date = date.today() 44 | start_date = end_date - relativedelta(months=months) 45 | st.write(f"Downloading data for {ticker} from {start_date} to {end_date}...") 46 | data = yf.download(ticker, start=start_date, end=end_date) 47 | 48 | if data.empty: 49 | st.error(f"No data found for ticker '{ticker}'.") 50 | return 51 | 52 | data[f"SMA_{fast_window}"] = data["Close"].rolling(window=fast_window).mean() 53 | data[f"SMA_{slow_window}"] = data["Close"].rolling(window=slow_window).mean() 54 | 55 | df_bt = data[["Close", f"SMA_{fast_window}", f"SMA_{slow_window}"]].dropna().copy() 56 | df_bt["return"] = df_bt["Close"].pct_change().fillna(0) 57 | 58 | if strategy_type == "long-only": 59 | df_bt["position"] = ( 60 | df_bt[f"SMA_{fast_window}"] > df_bt[f"SMA_{slow_window}"] 61 | ).astype(int) 62 | strat_label = "Crossover Strategy (long-only)" 63 | else: 64 | df_bt["position"] = np.where( 65 | df_bt[f"SMA_{fast_window}"] > df_bt[f"SMA_{slow_window}"], 1, -1 66 | ) 67 | strat_label = "Crossover Strategy (long-short)" 68 | df_bt["position"] = df_bt["position"].shift(1).fillna(0) 69 | 70 | df_bt["strategy_return"] = df_bt["return"] * df_bt["position"] 71 | df_bt["cum_benchmark"] = (1 + df_bt["return"]).cumprod() 72 | df_bt["cum_strategy"] = (1 + df_bt["strategy_return"]).cumprod() 73 | 74 | fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) 75 | cmap = plt.get_cmap("coolwarm") 76 | color_price = cmap(0.0) 77 | color_fast = cmap(0.3) 78 | color_slow = cmap(0.7) 79 | color_bh = cmap(0.0) 80 | color_strat = cmap(1.0) 81 | 82 | ax1.plot(data.index, data["Close"], label="Close", color=color_price) 83 | ax1.plot( 84 | data.index, 85 | data[f"SMA_{fast_window}"], 86 | label=f"SMA {fast_window}", 87 | color=color_fast, 88 | linestyle="--", 89 | lw=1, 90 | ) 91 | ax1.plot( 92 | data.index, 93 | data[f"SMA_{slow_window}"], 94 | label=f"SMA {slow_window}", 95 | color=color_slow, 96 | linestyle="--", 97 | lw=1, 98 | ) 99 | fast_sma = data[f"SMA_{fast_window}"] 100 | slow_sma = data[f"SMA_{slow_window}"] 101 | cross_up = (fast_sma > slow_sma) & (fast_sma.shift(1) <= slow_sma.shift(1)) 102 | cross_down = (fast_sma < slow_sma) & (fast_sma.shift(1) >= slow_sma.shift(1)) 103 | ax1.scatter( 104 | data.index[cross_up], fast_sma[cross_up], marker="^", color=color_strat, s=50, zorder=5 105 | ) 106 | ax1.scatter( 107 | data.index[cross_down], fast_sma[cross_down], marker="v", color="green", s=50, zorder=5 108 | ) 109 | ax1.set_ylabel("Price (USD)") 110 | ax1.set_title(f"{ticker} Price & SMAs") 111 | ax1.grid(True) 112 | ax1.legend() 113 | 114 | ax2.plot(df_bt.index, df_bt["cum_benchmark"], label="Buy & Hold", color=color_bh) 115 | ax2.plot(df_bt.index, df_bt["cum_strategy"], label=strat_label, color=color_strat) 116 | final_bh = df_bt["cum_benchmark"].iloc[-1] 117 | final_strat = df_bt["cum_strategy"].iloc[-1] 118 | perf_text = f"Buy & Hold: {final_bh:.2f}\n{strat_label}: {final_strat:.2f}" 119 | ax2.text( 120 | 0.02, 121 | 0.95, 122 | perf_text, 123 | transform=ax2.transAxes, 124 | va="top", 125 | ha="left", 126 | bbox=dict(facecolor="white", edgecolor="black", linewidth=1, alpha=0.8), 127 | ) 128 | ax2.set_xlabel("Date") 129 | ax2.set_ylabel("Growth of $1") 130 | ax2.set_title("Cumulative Returns") 131 | ax2.grid(True) 132 | ax2.legend(loc="lower right") 133 | 134 | plt.tight_layout() 135 | st.pyplot(fig) 136 | 137 | 138 | if __name__ == "__main__": 139 | main() -------------------------------------------------------------------------------- /01_session/codex_cli_explained.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Overview of OpenAI Codex CLI 4 | 5 | OpenAI Codex CLI is a recently released, open-source command-line tool that brings advanced AI coding capabilities directly to developers' local terminals. It is designed to streamline the software development process by allowing users to interact with code using natural language, automate repetitive tasks, and boost productivity—all while keeping sensitive code secure on their own machines[^1][^2][^4][^5]. 6 | 7 | ## Key Features 8 | 9 | **Local, Secure Operation** 10 | 11 | - Codex CLI runs entirely on your local machine, ensuring that your source code and data remain private unless you choose to share them[^2][^4][^5]. 12 | - It uses sandboxing technologies (like Apple Seatbelt on macOS or Docker on Linux) to safely execute code and commands, with network access disabled by default[^4]. 13 | 14 | **Multimodal Input and Interaction** 15 | 16 | - You can provide instructions via text, screenshots, or even simple diagrams, enabling the AI to generate, edit, or explain code accordingly[^2][^4][^5]. 17 | - The CLI acts as a code editor, AI assistant, and debugger, all within your terminal[^2]. 18 | 19 | **Flexible Approval Modes** 20 | 21 | - **Suggest (default):** The AI proposes code edits or shell commands, requiring your approval before making changes. 22 | - **Auto Edit:** The agent can read and write files automatically but still asks for permission before executing shell commands. 23 | - **Full Auto:** The agent autonomously reads, writes, and executes commands in a sandboxed, network-disabled environment limited to the current directory[^2][^4]. 24 | 25 | **Zero-Setup Installation** 26 | 27 | - Getting started is simple, typically requiring just a single NPM command to install[^2]. 28 | 29 | **Model Integration** 30 | 31 | - Codex CLI leverages OpenAI’s latest models, such as o3 and o4-mini, and now includes a smaller, faster model (codex-mini-latest) optimized for low-latency code Q\&A and editing[^1][^4]. 32 | - The CLI is regularly updated as OpenAI improves these models[^1]. 33 | 34 | 35 | ## How Codex CLI Fits Into OpenAI’s Vision 36 | 37 | Codex CLI is part of OpenAI’s broader goal to develop "agentic software engineers"—AI tools that can autonomously understand project requirements, create applications, and even perform quality assurance testing[^4][^5]. While Codex CLI doesn’t yet fully realize this vision, it represents a major step toward more intuitive, efficient, and accessible AI-powered software development[^4][^5][^6]. 38 | 39 | ## Getting Started and Access 40 | 41 | - You can sign in with your ChatGPT account, and Codex CLI will automatically configure your API key. Plus and Pro users can redeem free API credits for a limited period[^1]. 42 | - The tool is open source and hosted on GitHub, encouraging community contributions and transparency[^4][^5]. 43 | 44 | 45 | ## Practical Impact and Considerations 46 | 47 | Codex CLI has the potential to: 48 | 49 | - Accelerate feature development and bug fixing. 50 | - Help developers understand unfamiliar codebases. 51 | - Democratize coding by enabling those with less programming experience to build and modify software via natural language instructions[^2][^4][^6]. 52 | 53 | **Caveats:** AI-generated code should always be reviewed, as automated tools can sometimes introduce bugs or security vulnerabilities[^4][^5]. 54 | 55 | ## Summary Table: Codex CLI Features 56 | 57 | | Feature | Description | 58 | | :-- | :-- | 59 | | Local execution | Runs on your machine, ensuring code privacy | 60 | | Multimodal inputs | Accepts text, screenshots, and diagrams | 61 | | Approval modes | Suggest, Auto Edit, Full Auto | 62 | | Model support | Uses latest OpenAI models (o3, o4-mini, codex-mini-latest) | 63 | | Open source | Community-driven development on GitHub | 64 | | Easy setup | Simple installation and API key configuration | 65 | | Security | Sandboxed execution, network disabled by default | 66 | 67 | ## Conclusion 68 | 69 | OpenAI Codex CLI is a lightweight, open-source coding agent for the terminal, designed to make AI-assisted software development faster, safer, and more accessible. It allows developers to pair with state-of-the-art AI models for code generation, editing, and debugging—all while maintaining control and privacy over their codebase[^1][^2][^4][^5]. 70 | 71 |
72 | 73 | [^1]: https://openai.com/index/introducing-codex/ 74 | 75 | [^2]: https://www.datacamp.com/tutorial/open-ai-codex-cli-tutorial 76 | 77 | [^3]: https://techcrunch.com/2025/05/16/openai-launches-codex-an-ai-coding-agent-in-chatgpt/ 78 | 79 | [^4]: https://ai2sql.io/openai-codex-cli-announcement 80 | 81 | [^5]: https://techcrunch.com/2025/04/16/openai-debuts-codex-cli-an-open-source-coding-tool-for-terminals/ 82 | 83 | [^6]: https://tech-now.io/en/blogs/openai-codex-in-2025-new-era-of-ai-powered-software-development 84 | 85 | [^7]: https://www.blott.studio/blog/post/openai-codex-cli-build-faster-code-right-from-your-terminal 86 | 87 | [^8]: https://www.mind-verse.de/news/wichtige-aktualisierungen-codex-cli-anmeldung-modell-code-interaktionen 88 | 89 | [^9]: https://github.com/openai/codex 90 | 91 | [^10]: https://tech-now.io/blog/openai-codex-im-jahr-2025-neue-ara-der-ki-gestutzten-softwareentwicklung 92 | 93 | -------------------------------------------------------------------------------- /15_session/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Free Bootcamp: Algorithmic Trading with Python & GenAI 7 | 8 | 107 | 108 | 109 |
110 | 111 |

Algorithmic Trading Bootcamp

112 |
113 |
114 | GenAI Bootcamp Visual 115 |

Free Online Bootcamp

116 |

Master algorithmic trading with Python & GenAI. Join us to learn strategy design, backtesting, and deployment of live trading bots. No prior experience required!

117 |
Loading countdown...
118 |
119 | 120 | 121 | 122 | 123 |
124 |
125 |
126 |

What you'll learn:

127 |
    128 |
  • Python for data analysis and backtesting
  • 129 |
  • Building intelligent trading strategies with GenAI
  • 130 |
  • Deploying strategies to live markets
  • 131 |
132 |
133 |
134 | © 2025 The Python Quants GmbH. All rights reserved. 135 |
136 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /08_session/sma_backtest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Fetch and visualize historical stock data using yfinance. 3 | 4 | Prompts the user for: 5 | - a ticker symbol 6 | - number of months to retrieve 7 | - two SMA window values (fast and slow) 8 | - a strategy type ('long-only' or 'long-short') 9 | 10 | Retrieves data for that period, visualizes closing price and SMAs, marks crossovers, 11 | and backtests the specified crossover strategy vs. buy & hold. 12 | """ 13 | 14 | import datetime 15 | from dateutil.relativedelta import relativedelta 16 | import sys 17 | 18 | import matplotlib.pyplot as plt 19 | import yfinance as yf 20 | import numpy as np 21 | 22 | 23 | def main(): 24 | ticker = input("Enter ticker symbol (e.g. SPY): ").strip().upper() 25 | 26 | try: 27 | months = int(input( 28 | "Enter number of months to retrieve data for (e.g. 36 for 3 years): " 29 | ).strip()) 30 | except ValueError: 31 | print("Invalid input: number of months must be an integer. Exiting.") 32 | sys.exit(1) 33 | 34 | if months <= 0: 35 | print("Number of months must be positive. Exiting.") 36 | sys.exit(1) 37 | 38 | try: 39 | fast_window = int(input("Enter fast SMA window (days, e.g. 20): ").strip()) 40 | slow_window = int(input("Enter slow SMA window (days, e.g. 50): ").strip()) 41 | except ValueError: 42 | print("Invalid input: SMA windows must be integers. Exiting.") 43 | sys.exit(1) 44 | 45 | if fast_window <= 0 or slow_window <= 0: 46 | print("SMA windows must be positive integers. Exiting.") 47 | sys.exit(1) 48 | 49 | strategy_type = input("Enter strategy type ('long-only' or 'long-short'): ").strip().lower() 50 | if strategy_type not in ('long-only', 'long-short'): 51 | print("Invalid strategy type. Exiting.") 52 | sys.exit(1) 53 | 54 | end_date = datetime.date.today() 55 | start_date = end_date - relativedelta(months=months) 56 | 57 | print(f"Downloading data for {ticker} from {start_date} to {end_date}...") 58 | data = yf.download(ticker, start=start_date, end=end_date) 59 | 60 | if data.empty: 61 | print(f"No data found for ticker symbol '{ticker}'. Exiting.") 62 | sys.exit(1) 63 | 64 | data[f"SMA_{fast_window}"] = data["Close"].rolling(window=fast_window).mean() 65 | data[f"SMA_{slow_window}"] = data["Close"].rolling(window=slow_window).mean() 66 | 67 | # Backtest setup: only keep entries where both SMAs exist 68 | df_bt = data[["Close", f"SMA_{fast_window}", f"SMA_{slow_window}"]].dropna().copy() 69 | # Compute daily returns 70 | df_bt["return"] = df_bt["Close"].pct_change().fillna(0) 71 | # Determine positions based on strategy type 72 | if strategy_type == 'long-only': 73 | df_bt["position"] = ( 74 | df_bt[f"SMA_{fast_window}"] > df_bt[f"SMA_{slow_window}"] 75 | ).astype(int) 76 | else: 77 | # long-short: long when fast > slow, short when fast < slow 78 | df_bt["position"] = np.where( 79 | df_bt[f"SMA_{fast_window}"] > df_bt[f"SMA_{slow_window}"], 1, -1 80 | ) 81 | df_bt["position"] = df_bt["position"].shift(1).fillna(0) 82 | df_bt["strategy_return"] = df_bt["return"] * df_bt["position"] 83 | df_bt["cum_benchmark"] = (1 + df_bt["return"]).cumprod() 84 | df_bt["cum_strategy"] = (1 + df_bt["strategy_return"]).cumprod() 85 | strategy_label = ( 86 | "Crossover Strategy (long-only)" 87 | if strategy_type == 'long-only' 88 | else "Crossover Strategy (long-short)" 89 | ) 90 | 91 | # Plot price + SMAs with crossover markers, and performance 92 | fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) 93 | 94 | # Define colormap colors 95 | cmap = plt.get_cmap("coolwarm") 96 | color_price = cmap(0.0) 97 | color_fast = cmap(0.3) 98 | color_slow = cmap(0.7) 99 | color_bh = cmap(0.0) 100 | color_strat = cmap(1.0) 101 | 102 | ax1.plot(data.index, data["Close"], label="Close", color=color_price) 103 | ax1.plot( 104 | data.index, 105 | data[f"SMA_{fast_window}"], 106 | color=color_fast, 107 | linestyle="--", 108 | lw=1, 109 | label=f"SMA {fast_window}", 110 | ) 111 | ax1.plot( 112 | data.index, 113 | data[f"SMA_{slow_window}"], 114 | color=color_slow, 115 | linestyle="--", 116 | lw=1, 117 | label=f"SMA {slow_window}", 118 | ) 119 | # Mark crossovers 120 | fast_sma = data[f"SMA_{fast_window}"] 121 | slow_sma = data[f"SMA_{slow_window}"] 122 | cross_up = (fast_sma > slow_sma) & (fast_sma.shift(1) <= slow_sma.shift(1)) 123 | cross_down = (fast_sma < slow_sma) & (fast_sma.shift(1) >= slow_sma.shift(1)) 124 | ax1.scatter( 125 | data.index[cross_up], fast_sma[cross_up], marker="^", color=color_strat, s=50, zorder=5 126 | ) 127 | ax1.scatter( 128 | data.index[cross_down], fast_sma[cross_down], marker="v", color="green", s=50, zorder=5 129 | ) 130 | ax1.set_title(f"{ticker} Price & SMAs ({start_date} to {end_date})") 131 | ax1.set_ylabel("Price (USD)") 132 | ax1.grid(True) 133 | ax1.legend() 134 | 135 | ax2.plot(df_bt.index, df_bt["cum_benchmark"], label="Buy & Hold", color=color_bh) 136 | ax2.plot(df_bt.index, df_bt["cum_strategy"], label=strategy_label, color=color_strat) 137 | ax2.set_title("Cumulative Returns") 138 | ax2.set_xlabel("Date") 139 | ax2.set_ylabel("Growth of $1") 140 | # Annotate final performance in upper left, avoiding overlap 141 | final_bh = df_bt["cum_benchmark"].iloc[-1] 142 | final_strat = df_bt["cum_strategy"].iloc[-1] 143 | perf_text = ( 144 | f"Buy & Hold: {final_bh:.2f}\n" 145 | f"{strategy_label}: {final_strat:.2f}" 146 | ) 147 | ax2.text( 148 | 0.02, 149 | 0.95, 150 | perf_text, 151 | transform=ax2.transAxes, 152 | va="top", 153 | ha="left", 154 | bbox=dict(facecolor="white", edgecolor="black", linewidth=1, alpha=0.8), 155 | ) 156 | ax2.grid(True) 157 | ax2.legend(loc="lower right") 158 | 159 | plt.tight_layout() 160 | plt.show() 161 | 162 | 163 | if __name__ == "__main__": 164 | main() -------------------------------------------------------------------------------- /01_session/breakout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Breakout Game 7 | 11 | 12 | 13 | 14 | 201 | 202 | -------------------------------------------------------------------------------- /10_options/scripts/mcdxa.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "e32fba29-1962-446c-97fd-476e05aa155a", 6 | "metadata": {}, 7 | "source": [ 8 | "\"The
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "2f1c8a9f", 14 | "metadata": {}, 15 | "source": [ 16 | "# mcdxa Package Tutorial\n", 17 | "\n", 18 | "This notebook demonstrates how to use the **mcdxa** package for option pricing.\n" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "1e4ad6e6-89f2-4750-b5c6-2d1797ed67ad", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "import numpy as np\n", 29 | "import matplotlib.pyplot as plt\n", 30 | "plt.style.use('seaborn-v0_8')\n", 31 | "%config InlineBackend.figure_format = 'svg'" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "ad9403b3-23da-44fa-95f2-b9a9f50b1e74", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "import sys\n", 42 | "sys.path.append('..')" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "id": "24f3cb5a-6343-4785-9ccd-9f14731d2df0", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "from mcdxa.models import BSM, Heston, Merton, Bates\n", 53 | "from mcdxa.payoffs import CallPayoff, PutPayoff, AsianCallPayoff, CustomPayoff\n", 54 | "from mcdxa.pricers.european import EuropeanPricer\n", 55 | "from mcdxa.pricers.american import AmericanBinomialPricer, LongstaffSchwartzPricer\n", 56 | "from mcdxa.analytics import bsm_price" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "id": "b828bccd", 62 | "metadata": {}, 63 | "source": [ 64 | "## Payoff Examples\n" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "id": "52204e91", 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "K = 100\n", 75 | "spots = [80, 100, 120]\n", 76 | "call = CallPayoff(K)\n", 77 | "put = PutPayoff(K)\n", 78 | "print('Call payoff:', call(spots))\n", 79 | "print('Put payoff :', put(spots))" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "id": "7f71ebef", 85 | "metadata": {}, 86 | "source": [ 87 | "## Custom Payoff\n" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "id": "01cf75cf", 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "sqrt_call = CustomPayoff(lambda s: np.maximum(np.sqrt(s) - 9, 0))\n", 98 | "spots = [81, 100, 121]\n", 99 | "print('Custom sqrt call payoff:', sqrt_call(spots))" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "id": "133e2577", 105 | "metadata": {}, 106 | "source": [ 107 | "## Path-dependent Payoff (Asian)\n" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": null, 113 | "id": "ca67cbb6", 114 | "metadata": {}, 115 | "outputs": [], 116 | "source": [ 117 | "paths = np.array([[90, 110, 130], [120, 100, 80]])\n", 118 | "asian_call = AsianCallPayoff(100)\n", 119 | "print('Asian call payoff on sample paths:', asian_call(paths))" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "id": "bdb6355e", 125 | "metadata": {}, 126 | "source": [ 127 | "## Simulate and Plot BSM Paths\n" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "id": "5bcc0e6d", 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "model = BSM(r=0.05, sigma=0.2)\n", 138 | "paths = model.simulate(100, 1, n_paths=5, n_steps=50)\n", 139 | "plt.figure(figsize=(8,4))\n", 140 | "for i, path in enumerate(paths):\n", 141 | " plt.plot(path, label=f'Path {i}')\n", 142 | "plt.legend(); plt.title('BSM Sample Paths'); plt.xlabel('Step'); plt.ylabel('Price')\n", 143 | "plt.show()" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "id": "935790bc", 149 | "metadata": {}, 150 | "source": [ 151 | "## European Option Pricing via Monte Carlo\n" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "id": "848d3e75", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "model = BSM(r=0.05, sigma=0.2)\n", 162 | "payoff = CallPayoff(100)\n", 163 | "pricer = EuropeanPricer(model, payoff, n_paths=5000, n_steps=50, seed=42)\n", 164 | "price_mc, stderr = pricer.price(100, 1, 0.05)\n", 165 | "price_bs = bsm_price(100, 100, 1, 0.05, 0.2, option_type='call')\n", 166 | "print(f'MC price: {price_mc:.4f} ± {stderr:.4f}, BSM price: {price_bs:.4f}')" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "id": "e06c5c8a", 172 | "metadata": {}, 173 | "source": [ 174 | "## American Option Pricing\n" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": null, 180 | "id": "9aa98105", 181 | "metadata": {}, 182 | "outputs": [], 183 | "source": [ 184 | "amer = AmericanBinomialPricer(model, CallPayoff(100), n_steps=100)\n", 185 | "price_amer = amer.price(100, 1, 0.05)\n", 186 | "print('CRR American call price:', price_amer)\n", 187 | "lsm = LongstaffSchwartzPricer(model, PutPayoff(100), n_paths=5000, n_steps=50, seed=42)\n", 188 | "price_lsm, stderr_lsm = lsm.price(90, 1, 0.05)\n", 189 | "print(f'LSM American put price: {price_lsm:.4f} ± {stderr_lsm:.4f}')" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "id": "29b7acf4-7d13-4a6c-8932-b9d6841ac3ce", 195 | "metadata": {}, 196 | "source": [ 197 | "\"The
" 198 | ] 199 | } 200 | ], 201 | "metadata": { 202 | "kernelspec": { 203 | "display_name": "Python 3 (ipykernel)", 204 | "language": "python", 205 | "name": "python3" 206 | }, 207 | "language_info": { 208 | "codemirror_mode": { 209 | "name": "ipython", 210 | "version": 3 211 | }, 212 | "file_extension": ".py", 213 | "mimetype": "text/x-python", 214 | "name": "python", 215 | "nbconvert_exporter": "python", 216 | "pygments_lexer": "ipython3", 217 | "version": "3.12.2" 218 | } 219 | }, 220 | "nbformat": 4, 221 | "nbformat_minor": 5 222 | } 223 | -------------------------------------------------------------------------------- /09_session/streamlit_portfolio_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Streamlit application for fetching EOD stock data, computing optimal portfolios, 4 | and visualizing prices, allocations, and Monte Carlo risk-return simulations. 5 | """ 6 | 7 | import datetime as dt 8 | 9 | import numpy as np 10 | import pandas as pd 11 | import streamlit as st 12 | import yfinance as yf 13 | import matplotlib.pyplot as plt 14 | from scipy.optimize import minimize 15 | 16 | 17 | @st.cache_data 18 | def load_data(tickers, period, start_date, end_date): 19 | """ 20 | Fetch EOD 'Close' prices for given tickers using yfinance. 21 | Caches results to speed up repeated queries. 22 | """ 23 | if period: 24 | df = yf.download(tickers, period=period, group_by="ticker") 25 | else: 26 | df = yf.download( 27 | tickers, 28 | start=start_date, 29 | end=end_date, 30 | group_by="ticker", 31 | ) 32 | # Extract close prices 33 | if isinstance(df.columns, pd.MultiIndex): 34 | prices = df.xs("Close", level=1, axis=1) 35 | else: 36 | prices = df["Close"].to_frame() 37 | prices.columns = tickers 38 | return prices 39 | 40 | 41 | # Sidebar - user inputs 42 | st.sidebar.title("Portfolio Optimizer Settings") 43 | tickers_input = st.sidebar.text_input("Tickers (comma-separated)", "AAPL, MSFT, NFLX") 44 | tickers = [t.strip().upper() for t in tickers_input.split(",") if t.strip()] 45 | 46 | date_mode = st.sidebar.radio("Date range mode", ("Period", "Dates")) 47 | if date_mode == "Period": 48 | period = st.sidebar.text_input("Period (e.g. 1y,6mo)", "1y") 49 | start_date = end_date = None 50 | else: 51 | period = None 52 | start_date = st.sidebar.date_input("Start date", dt.date.today() - dt.timedelta(days=365)) 53 | end_date = st.sidebar.date_input("End date", dt.date.today()) 54 | 55 | normalize = st.sidebar.checkbox("Normalize prices", value=True) 56 | allow_shorts = st.sidebar.checkbox("Allow short sales", value=False) 57 | num_portfolios = st.sidebar.slider("Number of simulations", 100, 5000, 500, step=100) 58 | risk_free_rate = st.sidebar.number_input("Annual risk-free rate", min_value=0.0, max_value=1.0, value=0.0) 59 | annualize_days = st.sidebar.number_input("Trading days/year", min_value=1, max_value=365, value=252) 60 | 61 | st.title("📈 Portfolio Optimization Dashboard") 62 | 63 | # Load data 64 | prices = load_data(tickers, period, start_date, end_date) 65 | st.write("### Closing Price Data", prices.round(4).tail()) 66 | 67 | # Compute returns and annualized metrics 68 | returns = prices.pct_change().dropna() 69 | ann_returns = returns.mean() * annualize_days 70 | ann_cov = returns.cov() * annualize_days 71 | 72 | # Portfolio optimization 73 | ones = np.ones(len(ann_returns)) 74 | inv_cov = np.linalg.inv(ann_cov.values) 75 | if allow_shorts: 76 | w_mvp = inv_cov.dot(ones) / ones.dot(inv_cov).dot(ones) 77 | excess = ann_returns.values - risk_free_rate 78 | w_tan = inv_cov.dot(excess) / ones.dot(inv_cov).dot(excess) 79 | else: 80 | bounds = tuple((0.0, 1.0) for _ in range(len(ones))) 81 | constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1},) 82 | init = ones / len(ones) 83 | res_mvp = minimize(lambda w: w.dot(ann_cov.values).dot(w), init, bounds=bounds, constraints=constraints) 84 | w_mvp = res_mvp.x 85 | res_tan = minimize( 86 | lambda w: -((w.dot(ann_returns.values) - risk_free_rate) / np.sqrt(w.dot(ann_cov.values).dot(w))), 87 | init, 88 | bounds=bounds, 89 | constraints=constraints, 90 | ) 91 | w_tan = res_tan.x 92 | 93 | def port_stats(w): 94 | ret = w.dot(ann_returns.values) 95 | vol = np.sqrt(w.dot(ann_cov.values).dot(w)) 96 | sharpe = (ret - risk_free_rate) / vol 97 | return ret, vol, sharpe 98 | 99 | ret_mvp, vol_mvp, sharpe_mvp = port_stats(w_mvp) 100 | ret_tan, vol_tan, sharpe_tan = port_stats(w_tan) 101 | 102 | weights_df = pd.DataFrame({"Min Variance": w_mvp, "Max Sharpe": w_tan}, index=ann_returns.index) 103 | stats_df = pd.DataFrame( 104 | {"Return": [ret_mvp, ret_tan], "Volatility": [vol_mvp, vol_tan], "Sharpe": [sharpe_mvp, sharpe_tan]}, 105 | index=["Min Variance", "Max Sharpe"], 106 | ) 107 | 108 | st.write("## Portfolio Weights", weights_df.round(4)) 109 | st.write("## Performance Metrics", stats_df.round(4)) 110 | 111 | # Visualization tabs 112 | tab1, tab2, tab3 = st.tabs(["Price Series", "Allocations", "Monte Carlo"]) 113 | 114 | with tab1: 115 | st.header("Price Series") 116 | fig, ax = plt.subplots(figsize=(10, 5)) 117 | data_to_plot = prices / prices.iloc[0] if normalize else prices 118 | for col in data_to_plot.columns: 119 | ax.plot(data_to_plot.index, data_to_plot[col], label=col) 120 | ax.set_xlabel("Date") 121 | ax.set_ylabel("Normalized Price" if normalize else "Price") 122 | ax.legend() 123 | ax.grid(True) 124 | st.pyplot(fig) 125 | 126 | with tab2: 127 | st.header("Optimal Portfolio Allocations") 128 | if allow_shorts: 129 | # Grouped bar chart for allocations when shorts allowed 130 | fig, ax = plt.subplots(figsize=(8, 4)) 131 | weights_df.plot.bar(ax=ax) 132 | ax.set_title("Portfolio Weights: Min Variance vs Max Sharpe") 133 | ax.set_ylabel("Weight") 134 | ax.grid(True, axis="y") 135 | st.pyplot(fig) 136 | else: 137 | fig, axes = plt.subplots(1, 2, figsize=(10, 4)) 138 | weights_df.iloc[:, 0].plot(kind="pie", ax=axes[0], autopct="%.1f%%", startangle=90) 139 | axes[0].set_title("Min Variance") 140 | axes[0].set_ylabel("") 141 | weights_df.iloc[:, 1].plot(kind="pie", ax=axes[1], autopct="%.1f%%", startangle=90) 142 | axes[1].set_title("Max Sharpe") 143 | axes[1].set_ylabel("") 144 | st.pyplot(fig) 145 | 146 | with tab3: 147 | st.header("Monte Carlo Simulation") 148 | mc_n = num_portfolios 149 | mc = np.zeros((mc_n, 2)) 150 | for i in range(mc_n): 151 | if allow_shorts: 152 | w = np.random.randn(len(ann_returns)) 153 | w /= np.sum(w) 154 | else: 155 | w = np.random.rand(len(ann_returns)) 156 | w /= np.sum(w) 157 | mc[i, 0] = np.sqrt(w.dot(ann_cov.values).dot(w)) 158 | mc[i, 1] = w.dot(ann_returns.values) 159 | fig, ax = plt.subplots(figsize=(8, 6)) 160 | ax.scatter(mc[:, 0], mc[:, 1], color='gray', alpha=0.5) 161 | ax.scatter([vol_mvp], [ret_mvp], color='red', marker='*', s=200, label='Min Variance') 162 | ax.scatter([vol_tan], [ret_tan], color='blue', marker='*', s=200, label='Max Sharpe') 163 | ax.set_xlabel('Volatility') 164 | ax.set_ylabel('Return') 165 | ax.set_title(f'Monte Carlo Portfolios (n={mc_n})') 166 | ax.legend() 167 | ax.grid(True) 168 | st.pyplot(fig) -------------------------------------------------------------------------------- /04_session/backtest_gemini_25_flash.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | # --- Part 1: Fundamental Data Analysis (for context and rationale, not directly used in backtest script logic) --- 6 | 7 | # Load the aia_fundamentals.csv file 8 | df_fundamentals = pd.read_csv('aia_fundamentals.csv') 9 | 10 | # Transpose the DataFrame 11 | df_fundamentals_transposed = df_fundamentals.set_index('Unnamed: 0').T 12 | df_fundamentals_transposed.index.name = 'Ticker' 13 | 14 | # Corrected list of columns to convert to numeric based on available data 15 | numeric_cols = [ 16 | 'MarketCapitalization', 'MarketCapitalizationMln', 'EBITDA', 'PERatio', 17 | 'PEGRatio', 'WallStreetTargetPrice', 'BookValue', 'DividendShare', 18 | 'DividendYield', 'EarningsShare', 'EPSEstimateCurrentYear', 19 | 'EPSEstimateNextYear', 'EPSEstimateNextQuarter', 20 | 'EPSEstimateCurrentQuarter', 'ProfitMargin', 21 | 'OperatingMarginTTM', 'ReturnOnAssetsTTM', 'ReturnOnEquityTTM', 22 | 'RevenueTTM', 'RevenuePerShareTTM', 'QuarterlyRevenueGrowthYOY', 23 | 'GrossProfitTTM', 'DilutedEpsTTM', 'QuarterlyEarningsGrowthYOY' 24 | ] 25 | 26 | for col in numeric_cols: 27 | df_fundamentals_transposed[col] = pd.to_numeric(df_fundamentals_transposed[col], errors='coerce') 28 | 29 | # (The detailed print statements for fundamental analysis are omitted here to keep the final script concise, 30 | # as the user specifically asked for the "complete Python code for the new portfolio and the benchmarking". 31 | # These were used in previous steps to inform portfolio selection.) 32 | 33 | # --- Part 2: Portfolio Construction (as decided in the previous step) --- 34 | # Long Positions: MSFT, NVDA, GOOG 35 | # Short Positions: INTC, GE, MS 36 | 37 | # --- Part 3: Backtest Implementation --- 38 | 39 | # Load the EOD data 40 | df_eod_data = pd.read_csv('aia_eod_data.csv') 41 | 42 | # Load SPY prices for benchmark 43 | df_spy_prices = pd.read_csv('aia_spy_prices.csv') 44 | 45 | # Convert 'Date' columns to datetime and set as index 46 | df_eod_data['Date'] = pd.to_datetime(df_eod_data['Date']) 47 | df_eod_data = df_eod_data.set_index('Date').sort_index() 48 | 49 | df_spy_prices['Date'] = pd.to_datetime(df_spy_prices['Date']) 50 | df_spy_prices = df_spy_prices.set_index('Date').sort_index() 51 | 52 | # Define the NEW long and short portfolio stocks 53 | long_stocks_new = ['MSFT', 'NVDA', 'GOOG'] 54 | short_stocks_new = ['INTC', 'GE', 'MS'] 55 | all_portfolio_stocks_new = long_stocks_new + short_stocks_new 56 | 57 | # Select only the relevant stock columns from EOD data and SPY 'SPY.US' price 58 | df_portfolio_prices_new = df_eod_data[all_portfolio_stocks_new] 59 | df_spy_close_new = df_spy_prices['SPY.US'].rename('SPY') 60 | 61 | # Combine portfolio prices and SPY prices for date alignment and calculations 62 | df_combined_prices_new = pd.concat([df_portfolio_prices_new, df_spy_close_new], axis=1) 63 | 64 | # Drop rows with any missing values, as prices must be present for return calculations 65 | df_combined_prices_new.dropna(inplace=True) 66 | 67 | # Calculate daily returns for all stocks and SPY 68 | df_daily_returns_new = df_combined_prices_new.pct_change().dropna() 69 | 70 | # Define weights for the NEW portfolio 71 | # Assuming equal weights for simplicity and a dollar-neutral portfolio 72 | long_weight_new = 1 / len(long_stocks_new) 73 | short_weight_new = -1 / len(short_stocks_new) # Negative for short positions 74 | 75 | weights_new = {} 76 | for stock in long_stocks_new: 77 | weights_new[stock] = long_weight_new 78 | for stock in short_stocks_new: 79 | weights_new[stock] = short_weight_new 80 | 81 | # Convert weights to a pandas Series for easy multiplication with returns 82 | weights_series_new = pd.Series(weights_new) 83 | 84 | # Calculate portfolio daily returns 85 | portfolio_daily_returns_new = df_daily_returns_new[weights_series_new.index].dot(weights_series_new) 86 | 87 | # --- Performance Metrics Calculation --- 88 | 89 | # Cumulative Returns 90 | cumulative_portfolio_returns_new = (1 + portfolio_daily_returns_new).cumprod() - 1 91 | cumulative_spy_returns_new = (1 + df_daily_returns_new['SPY']).cumprod() - 1 92 | 93 | # Total Returns (simply the last value of cumulative returns) 94 | total_returns_portfolio_new = cumulative_portfolio_returns_new.iloc[-1] 95 | total_returns_spy_new = cumulative_spy_returns_new.iloc[-1] 96 | 97 | # Max Drawdown function 98 | def calculate_max_drawdown(returns_series): 99 | cumulative_returns = (1 + returns_series).cumprod() 100 | peak = cumulative_returns.expanding(min_periods=1).max() 101 | drawdown = (cumulative_returns / peak) - 1 102 | return drawdown.min() 103 | 104 | max_drawdown_portfolio_new = calculate_max_drawdown(portfolio_daily_returns_new) 105 | max_drawdown_spy_new = calculate_max_drawdown(df_daily_returns_new['SPY']) 106 | 107 | # Annualized Volatility (assuming 252 trading days in a year) 108 | annualized_volatility_portfolio_new = portfolio_daily_returns_new.std() * np.sqrt(252) 109 | annualized_volatility_spy_new = df_daily_returns_new['SPY'].std() * np.sqrt(252) 110 | 111 | # Annualized Returns 112 | annualized_returns_portfolio_new = (1 + portfolio_daily_returns_new).prod()**(252/len(portfolio_daily_returns_new)) - 1 113 | annualized_returns_spy_new = (1 + df_daily_returns_new['SPY']).prod()**(252/len(df_daily_returns_new['SPY'])) - 1 114 | 115 | # Sharpe Ratio (assuming risk-free rate = 0 for simplicity) 116 | risk_free_rate = 0 117 | sharpe_ratio_portfolio_new = (annualized_returns_portfolio_new - risk_free_rate) / annualized_volatility_portfolio_new 118 | sharpe_ratio_spy_new = (annualized_returns_spy_new - risk_free_rate) / annualized_volatility_spy_new 119 | 120 | # Beta and Alpha (relative to SPY) 121 | X_new = df_daily_returns_new['SPY'] 122 | Y_new = portfolio_daily_returns_new 123 | 124 | clean_data_new = pd.concat([X_new, Y_new], axis=1).dropna() 125 | X_clean_new = clean_data_new['SPY'] 126 | Y_clean_new = clean_data_new.iloc[:, 1] 127 | 128 | if not X_clean_new.empty and not Y_clean_new.empty: 129 | beta_new = X_clean_new.cov(Y_clean_new) / X_clean_new.var() 130 | alpha_new = annualized_returns_portfolio_new - (risk_free_rate + beta_new * (annualized_returns_spy_new - risk_free_rate)) 131 | else: 132 | beta_new = np.nan 133 | alpha_new = np.nan 134 | 135 | # Summary of Performance Statistics 136 | performance_summary_new = pd.DataFrame({ 137 | 'Metric': ['Annualized Returns', 'Total Returns', 'Annualized Volatility', 'Sharpe Ratio', 'Max Drawdown', 'Beta (vs SPY)', 'Alpha (vs SPY)'], 138 | 'New Portfolio': [ 139 | f'{annualized_returns_portfolio_new:.2%}', 140 | f'{total_returns_portfolio_new:.2%}', # Added Total Returns 141 | f'{annualized_volatility_portfolio_new:.2%}', 142 | f'{sharpe_ratio_portfolio_new:.2f}', 143 | f'{max_drawdown_portfolio_new:.2%}', 144 | f'{beta_new:.2f}', 145 | f'{alpha_new:.2%}' 146 | ], 147 | 'SPY Benchmark': [ 148 | f'{annualized_returns_spy_new:.2%}', 149 | f'{total_returns_spy_new:.2%}', # Added Total Returns 150 | f'{annualized_volatility_spy_new:.2%}', 151 | f'{sharpe_ratio_spy_new:.2f}', 152 | f'{max_drawdown_spy_new:.2%}', 153 | '1.00', 154 | '0.00%' 155 | ] 156 | }) 157 | 158 | print("\n--- NEW Portfolio Performance Statistics ---") 159 | print(performance_summary_new.to_string(index=False)) 160 | 161 | # Plotting cumulative returns 162 | plt.figure(figsize=(12, 6)) 163 | plt.plot(cumulative_portfolio_returns_new, label='NEW L/S Portfolio') 164 | plt.plot(cumulative_spy_returns_new, label='SPY Benchmark') 165 | plt.title('Cumulative Returns of NEW L/S Portfolio vs. SPY Benchmark') 166 | plt.xlabel('Date') 167 | plt.ylabel('Cumulative Return') 168 | plt.legend() 169 | plt.grid(True) 170 | plt.show() -------------------------------------------------------------------------------- /07_session/dataviz.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Time Series Data Visualization 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Time Series Data Visualization

15 |
16 |
17 |
18 | 19 | 22 |
23 |
24 | 25 |
26 | 27 | to 28 | 29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 | 187 | 188 | -------------------------------------------------------------------------------- /06_session/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Merton Jump Diffusion Simulator 7 | 8 | 30 | 31 | 32 |
33 |

Merton Jump Diffusion Simulator

34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 | 50 |
51 |

Call Price: -

52 |

Put Price: -

53 |
54 |
55 | 149 | 150 | -------------------------------------------------------------------------------- /07_session/spaceinvaders.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Space Invaders 6 | 10 | 11 | 12 | 13 | 249 | 250 | --------------------------------------------------------------------------------