diff --git a/README.md b/README.md index a9c9fbb..8f80146 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ -# lowkey_backtest +### lowkey_backtest — Supertrend Backtester + +### Overview +Backtest a simple, long-only strategy driven by a meta Supertrend signal on aggregated OHLCV data. The script: +- Loads 1-minute BTC/USD data from `../data/btcusd_1-min_data.csv` +- Aggregates to multiple timeframes (e.g., `5min`, `15min`, `30min`, `1h`, `4h`, `1d`) +- Computes three Supertrend variants and creates a meta signal when all agree +- Executes entries/exits at the aggregated bar open price +- Applies OKX spot fee assumptions (taker by default) +- Evaluates stop-loss using intra-bar 1-minute data +- Writes detailed trade logs and a summary CSV + +### Requirements +- Python 3.12+ +- Dependencies: `pandas`, `numpy`, `ta` +- Package management: `uv` + +Install dependencies with uv: + +```bash +uv sync +# If a dependency is missing, add it explicitly and sync +uv add pandas numpy ta +uv sync +``` + +### Data +- Expected CSV location: `../data/btcusd_1-min_data.csv` (relative to the repo root) +- Required columns: `Timestamp`, `Open`, `High`, `Low`, `Close`, `Volume` +- `Timestamp` should be UNIX seconds; zero-volume rows are ignored + +### Quickstart +Run the backtest with defaults: + +```bash +uv run python main.py +``` + +Outputs: +- Per-run trade logs in `backtest_logs/` named like `trade_log__sl.csv` +- Run-level summary in `backtest_summary.csv` + +### Configuring a Run +Adjust parameters directly in `main.py`: +- Date range (in `load_data`): `load_data('2021-11-01', '2024-10-16')` +- Timeframes to test (any subset of `"5min", "15min", "30min", "1h", "4h", "1d"`): + - `timeframes = ["5min", "15min", "30min", "1h", "4h", "1d"]` +- Stop-loss percentages: + - `stoplosses = [0.03, 0.05, 0.1]` +- Supertrend settings (in `add_supertrend_indicators`): `(period, multiplier)` pairs `(12, 3.0)`, `(10, 1.0)`, `(11, 2.0)` +- Fee model (in `calculate_okx_taker_maker_fee`): taker `0.0010`, maker `0.0008` + +### What the Backtester Does +- Aggregation: Resamples 1-minute data to your selected timeframe using OHLCV rules +- Supertrend signals: Computes three Supertrends and derives a meta trend of `+1` (bullish) or `-1` (bearish) when all agree; otherwise `0` +- Trade logic (long-only): + - Entry when the meta trend changes to bullish; uses aggregated bar open price + - Exit when the meta trend changes to bearish; uses aggregated bar open price + - Stop-loss: For each aggregated bar, scans corresponding 1-minute closes to detect stop-loss and exits using a realistic fill (threshold or next 1-minute open if gapped) +- Performance metrics: total return, max drawdown, Sharpe (daily, factor 252), win rate, number of trades, final/initial equity, and total fees + +### Important: Lookahead Bias Note +The current implementation uses the meta Supertrend signal of the same bar for entries/exits, which introduces lookahead bias. To avoid this, lag the signal by one bar inside `backtest()` in `main.py`: + +```python +# Replace the current line +meta_trend_signal = meta_trend + +# With a one-bar lag to remove lookahead +# meta_trend_signal = np.roll(meta_trend, 1) +# meta_trend_signal[0] = 0 +``` + +### Outputs +- `backtest_logs/trade_log__sl.csv`: trade-by-trade records including type (`buy`, `sell`, `stop_loss`, `forced_close`), timestamps, prices, balances, PnL, and fees +- `backtest_summary.csv`: one row per (timeframe, stop-loss) combination with `timeframe`, `stop_loss`, `total_return`, `max_drawdown`, `sharpe_ratio`, `win_rate`, `num_trades`, `final_equity`, `initial_equity`, `num_stop_losses`, `total_fees` + +### Troubleshooting +- CSV not found: Ensure the dataset is located at `../data/btcusd_1-min_data.csv` +- Missing packages: Run `uv add pandas numpy ta` then `uv sync` +- Memory/performance: Large date ranges on 1-minute data can be heavy; narrow the date span or test fewer timeframes diff --git a/main.py b/main.py index 558c266..0f4b660 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,16 @@ import pandas as pd import numpy as np from ta.volatility import AverageTrueRange +import time +import csv +import math +import os -def load_data(since): +def load_data(since, until): df = pd.read_csv('../data/btcusd_1-min_data.csv') df['Timestamp'] = pd.to_datetime(df['Timestamp'], unit='s') - df = df[df['Timestamp'] >= pd.Timestamp(since)] + df = df[(df['Timestamp'] >= pd.Timestamp(since)) & (df['Timestamp'] <= pd.Timestamp(until))] return df def aggregate_data(df, timeframe): @@ -38,10 +42,32 @@ def calculate_supertrend(df, period, multiplier): Returns: pd.Series: Supertrend values. """ + # Ensure we have enough data for ATR calculation + if len(df) < period + 1: + print(f"Warning: Not enough data for ATR period {period}. Need at least {period + 1} rows, got {len(df)}") + return pd.Series([np.nan] * len(df), index=df.index) + high = df['High'].values low = df['Low'].values close = df['Close'].values - atr = AverageTrueRange(df['High'], df['Low'], df['Close'], window=period).average_true_range().values + + # Calculate True Range first + tr = np.zeros_like(close) + for i in range(1, len(close)): + tr[i] = max( + high[i] - low[i], # Current high - current low + abs(high[i] - close[i-1]), # Current high - previous close + abs(low[i] - close[i-1]) # Current low - previous close + ) + + # Calculate ATR using simple moving average + atr = np.zeros_like(close) + atr[period] = np.mean(tr[1:period+1]) # First ATR value + for i in range(period+1, len(close)): + atr[i] = (atr[i-1] * (period-1) + tr[i]) / period # Exponential-like smoothing + + # Fill initial values with the first valid ATR + atr[:period] = atr[period] if atr[period] > 0 else 0.001 hl2 = (high + low) / 2 upperband = hl2 + (multiplier * atr) @@ -105,23 +131,38 @@ def precompute_1min_slice_indices(df_aggregated, df_1min): indices.append((start_idx, end_idx)) return indices, sorted_1min -def backtest(df_aggregated, df_1min, stop_loss_pct, progress_step=1000): +def backtest(timeframe, df_aggregated, df_1min, stop_loss_pct, progress_step=1000): """ - Backtest trading strategy based on Supertrend indicators with trailing stop loss. - Buys when all three Supertrend columns are positive (>0), - sells when any is negative (<0), or when trailing stop loss is hit. - - Args: - df_aggregated (pd.DataFrame): Aggregated OHLCV data with Supertrend columns. - df_1min (pd.DataFrame): 1-minute OHLCV data. - stop_loss_pct (float): Trailing stop loss percentage (e.g., 0.02 for 2%). - progress_step (int): Step interval for progress display. + Backtest trading strategy based on meta supertrend logic (all three supertrends agree). + Uses signal transitions and open prices for entry/exit to match original implementation. """ + start_time = time.time() required_st_cols = ["supertrend_12_3.0", "supertrend_10_1.0", "supertrend_11_2.0"] for col in required_st_cols: if col not in df_aggregated.columns: raise ValueError(f"Missing required Supertrend column: {col}") + # Calculate trend directions for each supertrend (-1, 0, 1) + trends = [] + for col in required_st_cols: + # Convert supertrend values to trend direction based on close price position + trend = np.where(df_aggregated['Close'] > df_aggregated[col], 1, -1) + trends.append(trend) + + # Stack trends and calculate meta trend (all must agree) + trends_arr = np.stack(trends, axis=1) + meta_trend = np.where((trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]), + trends_arr[:,0], 0) + + meta_trend_signal = meta_trend #incorrect: should be lagging as it introduces lookahead bias. + # Next step: modify OHLCV predictor to not use supertrend as a feature or anyother feature + # that introduces lookahead bias and predict the next close price. + # + # Old code, not that efficient: + # Add signal lagging to avoid lookahead bias + # meta_trend_signal = np.roll(meta_trend, 1) + # meta_trend_signal[0] = 0 # No signal for first bar + # Precompute 1-min slice indices for each aggregated bar slice_indices, sorted_1min = precompute_1min_slice_indices(df_aggregated, df_1min) df_1min_sorted = df_1min.iloc[sorted_1min].reset_index(drop=True) @@ -130,74 +171,275 @@ def backtest(df_aggregated, df_1min, stop_loss_pct, progress_step=1000): init_usd = 1000 usd = init_usd coin = 0 - highest_price = None nb_stop_loss = 0 + trade_log = [] + equity_curve = [] + trade_results = [] + entry_price = None + entry_time = None total_steps = len(df_aggregated) - 1 for i in range(1, len(df_aggregated)): - st_vals = [df_aggregated[col][i] for col in required_st_cols] - all_positive = all(val > 0 for val in st_vals) - any_negative = any(val < 0 for val in st_vals) + open_price = df_aggregated['Open'][i] # Use open price for entry/exit close_price = df_aggregated['Close'][i] + timestamp = df_aggregated['Timestamp'][i] + + # Get previous and current meta trend signals + prev_mt = meta_trend_signal[i-1] if i > 0 else 0 + curr_mt = meta_trend_signal[i] - # Buy condition: all Supertrend values positive - if not in_position and all_positive: - in_position = True - coin = usd / close_price - usd = 0 - highest_price = close_price - # If in position, update highest price and check stop loss on 1-min data - elif in_position: - # Update highest price if new high on aggregated bar - if close_price > highest_price: - highest_price = close_price + # Track equity at each bar + equity = usd + coin * close_price + equity_curve.append((timestamp, equity)) - # Use precomputed indices for this bar + # Check stop loss if in position + if in_position: start_idx, end_idx = slice_indices[i-1] df_1min_slice = df_1min_sorted.iloc[start_idx:end_idx] - stop_triggered = False - for _, row in df_1min_slice.iterrows(): - # Update highest price if new high in 1-min bar - if row['Close'] > highest_price: - highest_price = row['Close'] - # Trailing stop loss condition on 1-min close - if row['Close'] < highest_price * (1 - stop_loss_pct): - in_position = False - usd = coin * row['Close'] - coin = 0 - # print(f"Stop loss triggered at {row['Close']:.2f} on {row['Timestamp']}") - nb_stop_loss += 1 - highest_price = None + + if not df_1min_slice.empty: + stop_loss_threshold = entry_price * (1 - stop_loss_pct) + below_stop = df_1min_slice['Low'] < stop_loss_threshold + + if below_stop.any(): + first_idx = below_stop.idxmax() + stop_row = df_1min_slice.loc[first_idx] stop_triggered = True - break - - # If stop loss was triggered, skip further checks for this bar + in_position = False + + # More realistic stop loss fill logic + if stop_row['Open'] < stop_loss_threshold: + exit_price = stop_row['Open'] + else: + exit_price = stop_loss_threshold + + exit_time = stop_row['Timestamp'] + gross_usd = coin * exit_price + fee = calculate_okx_taker_maker_fee(gross_usd, is_maker=False) + usd = gross_usd - fee + trade_pnl = (exit_price - entry_price) / entry_price if entry_price else 0 + trade_results.append(trade_pnl) + trade_log.append({ + 'type': 'stop_loss', + 'time': exit_time, + 'price': exit_price, + 'usd': usd, + 'coin': 0, + 'pnl': trade_pnl, + 'fee': fee + }) + coin = 0 + nb_stop_loss += 1 + entry_price = None + entry_time = None + if stop_triggered: continue - # Sell condition: any Supertrend value negative (on aggregated bar close) - if any_negative: - in_position = False - usd = coin * close_price - coin = 0 - highest_price = None + # Entry condition: signal changes TO bullish (prev != 1 and curr == 1) + if not in_position and prev_mt != 1 and curr_mt == 1: + in_position = True + fee = calculate_okx_taker_maker_fee(usd, is_maker=False) + usd_after_fee = usd - fee + coin = usd_after_fee / open_price # Use open price + entry_price = open_price + entry_time = timestamp + usd = 0 + trade_log.append({ + 'type': 'buy', + 'time': timestamp, + 'price': open_price, + 'usd': usd, + 'coin': coin, + 'fee': fee + }) + + # Exit condition: signal changes TO bearish (prev == 1 and curr == -1) + elif in_position and prev_mt == 1 and curr_mt == -1: + in_position = False + exit_price = open_price # Use open price + exit_time = timestamp + gross_usd = coin * open_price + fee = calculate_okx_taker_maker_fee(gross_usd, is_maker=False) + usd = gross_usd - fee + trade_pnl = (exit_price - entry_price) / entry_price if entry_price else 0 + trade_results.append(trade_pnl) + trade_log.append({ + 'type': 'sell', + 'time': exit_time, + 'price': exit_price, + 'usd': usd, + 'coin': 0, + 'pnl': trade_pnl, + 'fee': fee + }) + coin = 0 + entry_price = None + entry_time = None if i % progress_step == 0 or i == total_steps: percent = (i / total_steps) * 100 - print(f"Progress: {percent:.1f}% ({i}/{total_steps})") + print(f"\rTimeframe: {timeframe},\tProgress: {percent:.1f}%\tCurrent equity: {equity:.2f}\033[K", end='', flush=True) - print(f"Total profit: {usd - init_usd}") - print(f"Number of stop losses: {nb_stop_loss}") + # Force close any open position at the end + if in_position: + final_open_price = df_aggregated['Open'].iloc[-1] # Use open price for consistency + final_timestamp = df_aggregated['Timestamp'].iloc[-1] + gross_usd = coin * final_open_price + fee = calculate_okx_taker_maker_fee(gross_usd, is_maker=False) + usd = gross_usd - fee + trade_pnl = (final_open_price - entry_price) / entry_price if entry_price else 0 + trade_results.append(trade_pnl) + trade_log.append({ + 'type': 'forced_close', + 'time': final_timestamp, + 'price': final_open_price, + 'usd': usd, + 'coin': 0, + 'pnl': trade_pnl, + 'fee': fee + }) + coin = 0 + in_position = False + entry_price = None + + print() + print(f"Timeframe: {timeframe},\tTotal profit: {usd - init_usd},\tNumber of stop losses: {nb_stop_loss}") + + # --- Performance Metrics --- + equity_arr = np.array([e[1] for e in equity_curve]) + # Handle edge cases for empty or invalid equity data + if len(equity_arr) == 0: + print("Warning: No equity data available") + return None + returns = np.diff(equity_arr) / equity_arr[:-1] + # Filter out infinite and NaN returns + returns = returns[np.isfinite(returns)] + total_return = (equity_arr[-1] - equity_arr[0]) / equity_arr[0] if equity_arr[0] != 0 else 0 + running_max = np.maximum.accumulate(equity_arr) + if equity_arr[-1] <= 0.01: + max_drawdown = -1.0 + else: + drawdowns = (equity_arr - running_max) / running_max + max_drawdown = drawdowns.min() if len(drawdowns) > 0 and np.isfinite(drawdowns).any() else 0 + if len(returns) > 1 and np.std(returns) > 1e-9: + sharpe = np.mean(returns) / np.std(returns) * math.sqrt(252) + else: + sharpe = 0 + wins = [1 for r in trade_results if r > 0] + win_rate = len(wins) / len(trade_results) if trade_results else 0 + num_trades = len(trade_results) + + print(f"Performance Metrics:") + print(f" Total Return: {total_return*100:.2f}%") + print(f" Max Drawdown: {max_drawdown*100:.2f}%") + print(f" Sharpe Ratio: {sharpe:.2f}") + print(f" Win Rate: {win_rate*100:.2f}%") + print(f" Number of Trades: {num_trades}") + print(f" Final Equity: ${equity_arr[-1]:.2f}") + print(f" Initial Equity: ${equity_arr[0]:.2f}") + + # --- Save Trade Log --- + log_dir = "backtest_logs" + os.makedirs(log_dir, exist_ok=True) + # Format stop_loss_pct for filename (e.g., 0.05 -> 0p05) + stop_loss_str = f"{stop_loss_pct:.2f}".replace('.', 'p') + log_path = os.path.join(log_dir, f"trade_log_{timeframe}_sl{stop_loss_str}.csv") + if trade_log: + all_keys = set() + for entry in trade_log: + all_keys.update(entry.keys()) + all_keys = list(all_keys) + + trade_log_filled = [] + for entry in trade_log: + filled_entry = {k: entry.get(k, None) for k in all_keys} + trade_log_filled.append(filled_entry) + + # Calculate total fees for this backtest + total_fees = sum(entry.get('fee', 0) for entry in trade_log) + + # Write summary header row, then trade log header and rows + with open(log_path, 'w', newline='') as f: + writer = csv.writer(f) + summary_header = [ + 'elapsed_time_sec', 'total_return', 'max_drawdown', 'sharpe_ratio', + 'win_rate', 'num_trades', 'final_equity', 'initial_equity', 'num_stop_losses', 'total_fees' + ] + summary_values = [ + f"{time.time() - start_time:.2f}", + f"{total_return*100:.2f}%", + f"{max_drawdown*100:.2f}%", + f"{sharpe:.2f}", + f"{win_rate*100:.2f}%", + str(num_trades), + f"${equity_arr[-1]:.2f}", + f"${equity_arr[0]:.2f}", + str(nb_stop_loss), + f"${total_fees:.4f}" + ] + writer.writerow(summary_header) + writer.writerow(summary_values) + writer.writerow([]) # Blank row for separation + dict_writer = csv.DictWriter(f, fieldnames=all_keys) + dict_writer.writeheader() + dict_writer.writerows(trade_log_filled) + + print(f"Trade log saved to {log_path}") + else: + print("No trades to log.") + + # Return summary metrics (excluding elapsed time) + return { + 'timeframe': timeframe, + 'stop_loss': stop_loss_pct, + 'total_return': total_return, + 'max_drawdown': max_drawdown, + 'sharpe_ratio': sharpe, + 'win_rate': win_rate, + 'num_trades': num_trades, + 'final_equity': equity_arr[-1], + 'initial_equity': equity_arr[0], + 'num_stop_losses': nb_stop_loss, + 'total_fees': total_fees if trade_log else 0 + } if __name__ == "__main__": - df_1min = load_data('2020-01-01') - df_aggregated = aggregate_data(df_1min, '5min') + timeframes = ["5min", "15min", "30min", "1h", "4h", "1d"] + # timeframes = ["5min", "15min", "1h", "4h", "1d"] + # timeframes = ["30min"] + stoplosses = [0.03, 0.05, 0.1] + + df_1min = load_data('2021-11-01', '2024-10-16') - # Add Supertrend indicators - df_aggregated = add_supertrend_indicators(df_aggregated) - - df_aggregated['log_return'] = np.log(df_aggregated['Close'] / df_aggregated['Close'].shift(1)) - - # Example: 2% trailing stop loss - backtest(df_aggregated, df_1min, stop_loss_pct=0.02) \ No newline at end of file + # Prepare summary CSV + summary_csv_path = "backtest_summary.csv" + summary_header = [ + 'timeframe', 'stop_loss', 'total_return', 'max_drawdown', 'sharpe_ratio', + 'win_rate', 'num_trades', 'final_equity', 'initial_equity', 'num_stop_losses', 'total_fees' + ] + with open(summary_csv_path, 'w', newline='') as summary_file: + writer = csv.DictWriter(summary_file, fieldnames=summary_header) + writer.writeheader() + for timeframe in timeframes: + df_aggregated = aggregate_data(df_1min, timeframe) + df_aggregated = add_supertrend_indicators(df_aggregated) + for stop_loss_pct in stoplosses: + summary = backtest(timeframe, df_aggregated, df_1min, stop_loss_pct=stop_loss_pct) + if summary is not None: + # Format values for CSV (e.g., floats as rounded strings) + summary_row = { + 'timeframe': summary['timeframe'], + 'stop_loss': summary['stop_loss'], + 'total_return': f"{summary['total_return']*100:.2f}%", + 'max_drawdown': f"{summary['max_drawdown']*100:.2f}%", + 'sharpe_ratio': f"{summary['sharpe_ratio']:.2f}", + 'win_rate': f"{summary['win_rate']*100:.2f}%", + 'num_trades': summary['num_trades'], + 'final_equity': f"${summary['final_equity']:.2f}", + 'initial_equity': f"${summary['initial_equity']:.2f}", + 'num_stop_losses': summary['num_stop_losses'], + 'total_fees': f"${summary['total_fees']:.4f}" + } + writer.writerow(summary_row) diff --git a/pyproject.toml b/pyproject.toml index 7dc0765..12727f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,6 @@ version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" -dependencies = [] +dependencies = [ + "ta>=0.11.0", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d7fed2f --- /dev/null +++ b/uv.lock @@ -0,0 +1,160 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "lowkey-backtest" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "ta" }, +] + +[package.metadata] +requires-dist = [{ name = "ta", specifier = ">=0.11.0" }] + +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, + { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" }, + { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" }, + { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" }, + { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" }, + { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" }, + { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" }, + { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "ta" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/9a/37d92a6b470dc9088612c2399a68f1a9ac22872d4e1eff416818e22ab11b/ta-0.11.0.tar.gz", hash = "sha256:de86af43418420bd6b088a2ea9b95483071bf453c522a8441bc2f12bcf8493fd", size = 25308, upload-time = "2023-11-02T13:53:35.434Z" } + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +]