diff --git a/.vscode/launch.json b/.vscode/launch.json index 237a81e..9f6cc6d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "console": "integratedTerminal", "cwd": "${workspaceFolder}", "args": [ - "2025-03-01", "2025-09-22", + "2025-01-01", "2025-12-31", "--timeframes-minutes", "60", "240", "480", "--stop-loss", "0.02", "0.05", "--exit-on-bearish-flip", diff --git a/INSTRUCTIONS_MVRV.md b/INSTRUCTIONS_MVRV.md new file mode 100644 index 0000000..fcece2d --- /dev/null +++ b/INSTRUCTIONS_MVRV.md @@ -0,0 +1,42 @@ +# MVRV Strategy Port Instructions + +This document explains how to run the ported MVRV/NUPL strategy. + +## Prerequisites +1. **Dependencies:** Ensure you have installed the required packages: + ```bash + uv add requests xgboost scikit-learn numba python-dotenv + ``` +2. **API Key:** Your CryptoQuant API key is set in `.env` (`CRYPTOQUANT_API_KEY`). +3. **Data:** You need a high-frequency (1m or 15m) OHLCV CSV file for BTC/USDT. + +## Workflow + +### 1. Prepare Data +Fetch on-chain data from CryptoQuant, merge it with your price data, and generate features. +```bash +python prepare_data.py --csv --days 730 +``` +*Output:* `data/features.csv` + +### 2. Train Model +Train the XGBoost model using the generated features. +```bash +python train_model.py +``` +*Output:* `data/model.pkl` + +### 3. Run Backtest +Run the strategy backtest using the trained model and your price data. +```bash +python backtest_mvrv.py --csv +``` +*Output:* Logs in `logs/mvrv_trade_log.csv` and summary printed to console. + +## Configuration +You can adjust strategy parameters in `strategy_config.py`: +- `PROB_THRESHOLD`: ML probability threshold for entry (default 0.55). +- `SL_ATR_MULT`: Stop Loss ATR multiplier (default 0.8). +- `TP_ATR_MULT`: Take Profit ATR multiplier (default 1.5). +- `MVRV_Z_THRESH`: Overheated MVRV Z-score threshold (default 1.5). +- `NUPL_THRESH`: Overheated NUPL threshold (default 0.6). diff --git a/backtest.py b/backtest.py index d4300cd..3514fa3 100644 --- a/backtest.py +++ b/backtest.py @@ -66,5 +66,8 @@ def backtest( equity_curve = pd.Series(equity, index=df["Timestamp"]) if log_path: write_trade_log(trades, log_path) - perf = compute_metrics(equity_curve, trades) + + # Calculate correct annualization factor based on timeframe + periods_per_year = int(252 * 24 * 60 / timeframe_minutes) + perf = compute_metrics(equity_curve, trades, periods_per_year=periods_per_year) return perf, equity_curve, trades diff --git a/backtest_mvrv.py b/backtest_mvrv.py new file mode 100644 index 0000000..713bd03 --- /dev/null +++ b/backtest_mvrv.py @@ -0,0 +1,270 @@ +import pandas as pd +import numpy as np +import pickle +import os +import strategy_config as config +from trade import TradeState, enter_long, exit_long +from logging_utils import write_trade_log +from metrics import compute_metrics +from pathlib import Path + +def backtest_mvrv( + df_features: pd.DataFrame, + df_1min: pd.DataFrame, + initial_cash: float = 10000.0, + log_path: Path | None = None, + test_only: bool = True # NEW: Only backtest on test set to avoid train/test leakage +): + print("--- Starting MVRV Strategy Backtest ---") + + # 1. Load Model and Generate Predictions + print(f"Loading model from {config.MODEL_PATH}...") + with open(config.MODEL_PATH, 'rb') as f: + model = pickle.load(f) + + # Load split info to identify test set boundary + split_info_path = config.MODEL_PATH.replace('.pkl', '_split.pkl') + if test_only and os.path.exists(split_info_path): + with open(split_info_path, 'rb') as f: + split_info = pickle.load(f) + test_start_idx = split_info['test_start_idx'] + print(f"Filtering to TEST SET ONLY (starting at index {test_start_idx})") + print(f" Train size was: {split_info['train_size']}, Test size: {split_info['test_size']}") + + # Filter features to test set only + df_features = df_features.iloc[test_start_idx:].copy() + + # Filter 1min data to match the test period + test_start_ts = df_features.index[0] + df_1min = df_1min[df_1min['Timestamp'] >= test_start_ts].copy() + + print(f"Backtest period: {df_features.index[0]} to {df_features.index[-1]}") + elif test_only: + print("WARNING: Split info not found. Running on FULL dataset (includes training data!).") + + # Prepare features for prediction + # Only use columns that were used in training + # We rely on config.FEATURE_NAMES, but we must check what's in df_features + # The model expects specific columns. + X = df_features[config.FEATURE_NAMES] + + print("Generating predictions...") + probs = model.predict_proba(X)[:, 1] + df_features['signal_prob'] = probs + + # 2. Setup Backtest Loop + state = TradeState( + cash=initial_cash, + fee_bps=config.FEES_PERCENT * 10000, # Convert to bps + slippage_bps=config.SLIPPAGE_PERCENT * 10000 + ) + + equity = [] + trades = [] + + # Track dynamic SL/TP + current_sl_price = 0.0 + current_tp_price = 0.0 + + # Pre-calculate entry signals to speed up loop + # Logic: Prob > Thresh AND Funding > Filter AND (MVRV < Thresh AND NUPL < Thresh) + # Note: features.py handles MVRV/NUPL Z-scores. + # The strategy uses raw NUPL/MVRV for regime filter, or Z-scores? + # Source: (mvrv_z > MVRV_Z_THRESH) | (nupl > NUPL_THRESH) -> is_overheated + # Check if we have 'mvrv_z' and 'nupl' columns in df_features. + + # Apply filters + # Defaults if cols missing (safe fallback) + s_prob = df_features['signal_prob'] + + funding = df_features['funding_rate'] if 'funding_rate' in df_features.columns else pd.Series(0, index=df_features.index) + + # Use 'mvrv_z' if available, else 'mvrv' (but Z-score is preferred for normalization) + # The source strategy used 'mvrv_z' > 1.5 for overheated. + mvrv_z = df_features['mvrv_z'] if 'mvrv_z' in df_features.columns else pd.Series(0, index=df_features.index) + + # Source used raw 'nupl' > 0.6 for overheated + nupl = df_features['nupl'] if 'nupl' in df_features.columns else pd.Series(0, index=df_features.index) + + # Regime Filter: True if NOT overheated + is_overheated = (mvrv_z > config.MVRV_Z_THRESH) | (nupl > config.NUPL_THRESH) + regime_can_trade = ~is_overheated + + # Entry Signal + entry_signals = ( + (s_prob > config.PROB_THRESHOLD) & + (funding > config.FUNDING_FILTER) & + regime_can_trade + ) + + df_features['entry_signal'] = entry_signals + + print(f"Total Entry Signals: {entry_signals.sum()}") + + # Loop + # df_features is 1H. df_1min is 1m. + # We iterate through df_features (hourly steps). + # If in a trade, we check df_1min for SL/TP within that hour. + # If not in a trade, we check for Entry Signal at the close of the hour (or open of next). + # Standard backtesting: Signals calculated on 'Close' are executable at 'Open' of next candle. + # But df_1min covers the interval. + # Let's align carefully. + + for i in range(len(df_features) - 1): + # Current 1H candle (completed) + row = df_features.iloc[i] + next_row = df_features.iloc[i+1] + + ts_start = row.name # Timestamp of the row (e.g. 10:00) + ts_end = next_row.name # Timestamp of next row (e.g. 11:00) + + # Get 1m data for this interval [ts_start, ts_end) + # Note: df_1min['Timestamp'] needs to be datetime + mask = (df_1min['Timestamp'] >= ts_start) & (df_1min['Timestamp'] < ts_end) + chunk_1min = df_1min.loc[mask] + + # 1. Manage Existing Position (Exit Logic) + # Store initial qty state to prevent re-entry in same candle if we exited + started_with_position = state.qty > 0 + + if state.qty > 0: + # Check for SL/TP hits in 1m data + for _, m_row in chunk_1min.iterrows(): + m_high = m_row['High'] + m_low = m_row['Low'] + m_ts = m_row['Timestamp'] + + # Check SL + if m_low <= current_sl_price: + evt = exit_long(state, current_sl_price) # Exec at SL price + if evt: + prev = trades[-1] + pnl = (evt["price"] - prev["price"]) * prev["qty"] + evt.update({"t": m_ts.isoformat(), "reason": "stop_loss", "pnl": pnl}) + trades.append(evt) + break # Exit loop + + # Check TP + if m_high >= current_tp_price: + evt = exit_long(state, current_tp_price) # Exec at TP price + if evt: + prev = trades[-1] + pnl = (evt["price"] - prev["price"]) * prev["qty"] + evt.update({"t": m_ts.isoformat(), "reason": "take_profit", "pnl": pnl}) + trades.append(evt) + break # Exit loop + + # 2. Check for New Entry (if no position) + # Logic: If signal was True at 'row' (completed candle), we enter at Open of 'next_row' (or first 1m candle of next hour) + # Actually, we can enter immediately at the start of the interval if the signal was from the *previous* completed candle. + # Here 'row' is the current interval processing. + # If 'entry_signal' is True for 'row', it means at the end of 10:00 we have a signal. + # We should enter at 11:00 (which is start of next interval). + # So we check entry_signal of 'row', and if True, we enter at first available price in 'chunk_1min'?? + # WAIT. 'chunk_1min' is [ts_start, ts_end). + # If row is 10:00 (meaning data for 09:00-10:00?), standard pandas resample labels left or right? + # Usually 10:00 label means 10:00-11:00 or 09:00-10:00? + # prepare_data used resample('1h'). + # Pandas default for 1h is usually start of bin (left). + # So 10:00 row contains data from 10:00 to 11:00. + # We can only know the signal at 11:00 (Close of the candle). + # So we can execute at 11:00 (start of next bin). + + # So: processing interval i (10:00-11:00). + # We check signal from i-1 (09:00-10:00). + # If i-1 had signal, we enter at start of i. + + if state.qty <= 0 and not started_with_position: + # Check previous row signal + if i > 0: + prev_row = df_features.iloc[i-1] + if prev_row['entry_signal']: + # Enter Long + # Price = Open of current interval (or first 1m open) + entry_price = row['open'] + if not chunk_1min.empty: + entry_price = chunk_1min.iloc[0]['Open'] + + # Calculate ATR-based SL/TP + atr = prev_row['atr'] + if pd.isna(atr) or atr == 0: + atr = row['open'] * 0.01 # Fallback 1% + + sl_dist = atr * config.SL_ATR_MULT + tp_dist = atr * config.TP_ATR_MULT + + current_sl_price = entry_price - sl_dist + current_tp_price = entry_price + tp_dist + + evt = enter_long(state, entry_price) + if evt: + evt.update({ + "t": ts_start.isoformat(), + "reason": "signal_entry", + "sl": current_sl_price, + "tp": current_tp_price + }) + trades.append(evt) + + # Update Equity Curve (mark-to-market at close of hour) + current_price = row['close'] + val = state.cash + (state.qty * current_price) + equity.append({'timestamp': ts_start, 'equity': val}) + + # Create Equity Series + equity_df = pd.DataFrame(equity).set_index('timestamp') + equity_curve = equity_df['equity'] + + # Save Logs + if log_path: + write_trade_log(trades, log_path) + + # Metrics (hourly bars: 252 trading days * 24 hours = 6048 periods/year) + perf = compute_metrics(equity_curve, trades, periods_per_year=252 * 24) + + # Print Summary + print("\n--- Backtest Summary ---") + print(f"Total Return: {perf.total_return * 100:.2f}%") + print(f"Sharpe Ratio: {perf.sharpe_ratio:.2f}") + print(f"Max Drawdown: {perf.max_drawdown * 100:.2f}%") + print(f"Total Trades: {perf.num_trades}") + + return perf, equity_curve, trades + +import argparse +def run(): + parser = argparse.ArgumentParser() + parser.add_argument("--csv", required=True, help="Path to 1m/15m OHLCV CSV") + args = parser.parse_args() + + # Load 1M Data + print(f"Loading 1m/15m data from {args.csv}...") + df_1min = pd.read_csv(args.csv) + # Ensure Timestamp + if 'Timestamp' in df_1min.columns: + ts_max = df_1min['Timestamp'].max() + if ts_max < 3000000000: + unit = 's' + elif ts_max < 3000000000000: + unit = 'ms' + else: + unit = None + df_1min['Timestamp'] = pd.to_datetime(df_1min['Timestamp'], unit=unit) + elif 'Date' in df_1min.columns: + df_1min['Timestamp'] = pd.to_datetime(df_1min['Date']) + + df_1min = df_1min.sort_values('Timestamp') + + # Load Features (1H) + print(f"Loading features from {config.FEATURES_PATH}...") + if not os.path.exists(config.FEATURES_PATH): + print("Error: features.csv not found. Run prepare_data.py first.") + return + + df_features = pd.read_csv(config.FEATURES_PATH, parse_dates=['timestamp'], index_col='timestamp') + + # Run Backtest + backtest_mvrv(df_features, df_1min, log_path=Path("logs/mvrv_trade_log.csv")) + +if __name__ == "__main__": + run() diff --git a/cryptoquant_client.py b/cryptoquant_client.py new file mode 100644 index 0000000..13e24f3 --- /dev/null +++ b/cryptoquant_client.py @@ -0,0 +1,263 @@ +import ssl +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry +import os +import time +import random +from datetime import datetime, timedelta +from typing import Dict, List, Optional +import logging +import strategy_config as config + +# Set up logging +logger = logging.getLogger(__name__) + +class RateLimiter: + """Simple rate limiter for CryptoQuant's API.""" + + def __init__(self, max_requests: int = None, window_seconds: int = 60): + # Default to 20 if not specified, or read from env + if max_requests is None: + max_requests = int(os.getenv('CRYPTOQUANT_RATE_LIMIT', 20)) + + self.max_requests = max_requests + self.window_seconds = window_seconds + self.requests = [] # List of timestamps + + def wait_if_needed(self) -> None: + """Sleep if we've hit the rate limit in current window.""" + now = time.time() + + # Remove requests outside the window + self.requests = [ts for ts in self.requests if now - + ts < self.window_seconds] + + # If at limit, sleep until oldest request expires + if len(self.requests) >= self.max_requests: + sleep_time = self.window_seconds - (now - self.requests[0]) + if sleep_time > 0: + logger.warning( + f"Rate limit hit. Sleeping {sleep_time:.1f}s...") + time.sleep(sleep_time) + + # Record this request + self.requests.append(now) + + def get_remaining(self) -> int: + """Get requests remaining in current window.""" + now = time.time() + recent_requests = [ + ts for ts in self.requests if now - ts < self.window_seconds] + return self.max_requests - len(recent_requests) + + +class CryptoQuantClient: + def __init__(self, api_key: str = None): + self.api_key = api_key or os.getenv('CRYPTOQUANT_API_KEY') + if not self.api_key: + raise ValueError("API key required - CRYPTOQUANT_API_KEY env var not set.") + + self.base_url = 'https://api.cryptoquant.com/v1' + headers = { + 'Authorization': f'Bearer {self.api_key}', + 'User-Agent': f'CryptoQuantBot/{random.uniform(1.0, 2.0):.1f}' + } + self.logger = logger + self.rate_limiter = RateLimiter() + + # Create a robust session with retries + self.session = requests.Session() + ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + ssl_context.options |= 0x80000000 + retry_strategy = Retry( + total=5, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["HEAD", "GET", "OPTIONS"], + backoff_factor=1 + ) + + class SSLAdapter(HTTPAdapter): + def init_poolmanager(self, *args, **kwargs): + kwargs['ssl_context'] = ssl_context + return super().init_poolmanager(*args, **kwargs) + + adapter = SSLAdapter(max_retries=retry_strategy) + self.session.mount("https://", adapter) + self.session.headers.update(headers) + + def _make_request(self, method: str, url: str, **kwargs) -> requests.Response: + """Internal method with robust error handling.""" + try: + if method.upper() == 'GET': + response = self.session.get(url, **kwargs, timeout=30) + elif method.upper() == 'POST': + response = self.session.post(url, **kwargs, timeout=30) + else: + raise ValueError(f"Unsupported method: {method}") + + response.raise_for_status() + return response + except requests.exceptions.RequestException as e: + self.logger.error(f"Request failed after all retries: {e}") + raise + + def _fetch_metric_chunked(self, url_suffix: str, params: Dict, days_back: int, chunk_days: int = 90) -> List[Dict]: + """Fetch data in chunks to avoid API limits.""" + all_data = [] + end_date = datetime.now() + start_date = end_date - timedelta(days=days_back) + + current_start = start_date + while current_start < end_date: + current_end = min(current_start + timedelta(days=chunk_days), end_date) + + # Format dates for API + chunk_params = params.copy() + chunk_params.update({ + 'from': current_start.strftime('%Y%m%d'), + 'to': current_end.strftime('%Y%m%d'), + 'limit': 10000 # Request max limit just in case + }) + + self.rate_limiter.wait_if_needed() + url = f"{self.base_url}/{url_suffix}" + + try: + self.logger.info(f"Fetching chunk {chunk_params['from']} to {chunk_params['to']}") + response = self._make_request('GET', url, params=chunk_params) + + # Check for specific "Out of allowed range" error to stop early if needed? + # Actually, if we iterate Old -> New, the early chunks fail, later succeed. + # If we want to optimize, we could start New -> Old and stop on failure. + # But current Old -> New approach ensures we get the most recent valid data eventually. + + data = response.json() + result = data.get('result', {}).get('data', []) + all_data.extend(result) + + except requests.exceptions.HTTPError as e: + # Handle 400 specially if it's "Out of allowed request range" + if e.response.status_code == 400: + self.logger.warning(f"Chunk failed (likely data limit): {e}. Continuing to next chunk...") + else: + self.logger.error(f"HTTP Error fetching chunk: {e}") + except Exception as e: + self.logger.error(f"Error fetching chunk: {e}") + + current_start = current_end + timedelta(days=1) # Move to next day + time.sleep(0.5) # Gentle pace + + # Remove duplicates based on date if any? usually API is clean. + return all_data + + def get_nupl(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 364) -> List[Dict]: + logger.info(f"Fetching {asset} NUPL ({days_back} days)") + params = {'window': window} + return self._fetch_metric_chunked(f"{asset.lower()}/network-indicator/nupl", params, days_back) + + def get_mvrv(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 364) -> List[Dict]: + logger.info(f"Fetching {asset} MVRV ({days_back} days)") + params = {'window': window} + return self._fetch_metric_chunked(f"{asset.lower()}/market-indicator/mvrv", params, days_back) + + def get_lth_sopr(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 364) -> List[Dict]: + logger.info(f"Fetching {asset} LTH-SOPR ({days_back} days)") + params = {'window': window} + return self._fetch_metric_chunked(f"{asset.lower()}/market-indicator/sopr", params, days_back) + + def get_puell_multiple(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 364) -> List[Dict]: + logger.info(f"Fetching {asset} Puell Multiple ({days_back} days)") + params = {'window': window} + return self._fetch_metric_chunked(f"{asset.lower()}/network-indicator/puell-multiple", params, days_back) + + def get_fund_flow_ratio(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 364) -> List[Dict]: + logger.info(f"Fetching {asset} Fund Flow Ratio ({days_back} days)") + params = {'window': window, 'exchange': 'all_exchange'} + return self._fetch_metric_chunked(f"{asset.lower()}/flow-indicator/fund-flow-ratio", params, days_back) + + def get_funding_rates(self, asset: str = config.ASSET, exchange: str = 'all_exchange', window: str = 'day', days_back: int = 90) -> List[Dict]: + self.logger.info(f"Fetching {asset} funding rates ({days_back} days)") + params = {'window': window, 'exchange': exchange} + return self._fetch_metric_chunked(f"{asset.lower()}/market-data/funding-rates", params, days_back) + + def get_exchange_net_flow(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 90) -> List[Dict]: + self.logger.info(f"Fetching {asset} exchange netflow ({days_back} days)") + params = {'window': window, 'exchange': 'all_exchange'} + return self._fetch_metric_chunked(f"{asset.lower()}/exchange-flows/netflow", params, days_back) + + def get_sopr_ratio(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 90) -> List[Dict]: + self.logger.info(f"Fetching {asset} SOPR Ratio ({days_back} days)") + params = {'window': window} + return self._fetch_metric_chunked(f"{asset.lower()}/market-indicator/sopr-ratio", params, days_back) + + def get_active_addresses(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 90) -> List[Dict]: + self.logger.info(f"Fetching {asset} active addresses ({days_back} days)") + params = {'window': window} + if asset.lower() == 'eth': + suffix = f"{asset.lower()}/network-data/addresses-count-all" + else: + suffix = f"{asset.lower()}/network-data/addresses-count" + return self._fetch_metric_chunked(suffix, params, days_back) + + def get_leverage_ratio(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 90) -> List[Dict]: + """Get Estimated Leverage Ratio.""" + self.logger.info(f"Fetching {asset} Estimated Leverage Ratio ({days_back} days)") + params = {'window': window, 'exchange': 'all_exchange'} + return self._fetch_metric_chunked(f"{asset.lower()}/market-indicator/estimated-leverage-ratio", params, days_back) + + def get_exchange_whale_ratio(self, asset: str = config.ASSET, window: str = 'day', days_back: int = 90) -> List[Dict]: + """Get the ratio of whale-sized deposits to total exchange deposits.""" + self.logger.info(f"Fetching {asset} Exchange Whale Ratio ({days_back} days)") + params = {'window': window, 'exchange': 'all_exchange'} + return self._fetch_metric_chunked(f"{asset.lower()}/flow-indicator/exchange-whale-ratio", params, days_back) + + def fetch_all_onchain(self, asset: str = config.ASSET, days_back: int = 364) -> Dict[str, List[Dict]]: + """Batch all features from config.ONCHAIN_FEATURE_NAMES.""" + features = {} + for feat in config.ONCHAIN_FEATURE_NAMES: + self.rate_limiter.wait_if_needed() + if feat == 'funding_rate': + features[feat] = self.get_funding_rates( + asset, 'all_exchange', 'day', days_back) + elif feat == 'net_exchange_flow': + features[feat] = self.get_exchange_net_flow( + asset, 'day', days_back) + elif feat == 'sopr_ratio': + features[feat] = self.get_sopr_ratio(asset, 'day', days_back) + elif feat == 'active_addresses': + features[feat] = self.get_active_addresses( + asset, 'day', days_back) + elif feat == 'leverage_ratio': + features[feat] = self.get_leverage_ratio( + asset, 'day', days_back) + elif feat == 'exchange_whale_ratio': + features[feat] = self.get_exchange_whale_ratio( + asset, 'day', days_back) + elif feat == 'nupl': + features[feat] = self.get_nupl(asset, 'day', days_back) + elif feat == 'mvrv': + features[feat] = self.get_mvrv(asset, 'day', days_back) + elif feat == 'lth_sopr': + features[feat] = self.get_lth_sopr(asset, 'day', days_back) + elif feat == 'puell_multiple': + features[feat] = self.get_puell_multiple( + asset, 'day', days_back) + elif feat == 'fund_flow_ratio': + features[feat] = self.get_fund_flow_ratio( + asset, 'day', days_back) + + time.sleep(1) # Pace for limits + return features + +if __name__ == "__main__": + # Test + try: + client = CryptoQuantClient() + print("Fetching funding rates as test...") + rates = client.get_funding_rates(days_back=7) + print(f"Got {len(rates)} records") + if rates: + print(rates[-1]) + except Exception as e: + print(f"Error: {e}") diff --git a/features.py b/features.py new file mode 100644 index 0000000..7d3b6bb --- /dev/null +++ b/features.py @@ -0,0 +1,164 @@ +import pandas as pd +import numpy as np +import strategy_config as config +from numba import njit + +# ============================================================================== +# --- LOOKBACK WINDOW FOR ROLLING Z-SCORES (PREVENTS LOOK-AHEAD BIAS) --- +# ============================================================================== +ZSCORE_LOOKBACK = 168 # 1 week in hourly candles +MIN_PERIODS_ZSCORE = 24 + +@njit +def ema_nb(arr, window): + alpha = 2.0 / (window + 1.0) + ema_arr = np.full_like(arr, np.nan) + ema_arr[0] = arr[0] + for i in range(1, len(arr)): + ema_arr[i] = alpha * arr[i] + (1.0 - alpha) * ema_arr[i-1] + return ema_arr + +@njit +def atr_nb(high, low, close, window): + n = len(close) + tr = np.zeros(n) + atr = np.full_like(close, np.nan) + for i in range(1, n): + tr[i] = max(high[i] - low[i], abs(high[i] - close[i-1]), + abs(low[i] - close[i-1])) + tr_series = tr[1:window+1] + atr[window] = np.mean(tr_series) + for i in range(window + 1, n): + atr[i] = (atr[i-1] * (window - 1) + tr[i]) / window + return atr + +def _add_price_action_features(df: pd.DataFrame) -> pd.DataFrame: + """Adds price action based features like returns, momentum, SMAs.""" + # Ensure column names match what we expect (lowercase usually) + # If user has 'Close', 'Open' etc. we might need to adjust or rename before calling this. + # Assuming the input df has lowercase 'close', 'high', 'low' etc. or we handle it. + + # Map typical Capitalized names to lowercase if needed + mapper = {c: c.lower() for c in df.columns if c in ['Open', 'High', 'Low', 'Close', 'Volume']} + if mapper: + df = df.rename(columns=mapper) + + df['returns'] = df['close'].pct_change() + df['log_returns'] = np.log(df['close'] / df['close'].shift(1)) + + df[f'momentum_{config.MOMENTUM_1_PERIODS}'] = df['close'].pct_change( + periods=config.MOMENTUM_1_PERIODS) + df[f'momentum_{config.MOMENTUM_2_PERIODS}'] = df['close'].pct_change( + periods=config.MOMENTUM_2_PERIODS) + + df[f'SMA_{config.SMA_FAST_PERIODS}'] = df['close'].rolling( + window=config.SMA_FAST_PERIODS).mean() + df[f'SMA_{config.SMA_SLOW_PERIODS}'] = df['close'].rolling( + window=config.SMA_SLOW_PERIODS).mean() + + df[f'volatility_{config.VOLATILITY_PERIODS}'] = df['log_returns'].rolling( + window=config.VOLATILITY_PERIODS).std() + return df + +def _add_mean_reversion_features(df: pd.DataFrame) -> pd.DataFrame: + """Adds Bollinger Band features.""" + window = config.BBAND_PERIODS + df['bb_middle'] = df['close'].rolling(window=window).mean() + bb_std = df['close'].rolling(window=window).std() + df['bb_upper'] = df['bb_middle'] + (bb_std * 2) + df['bb_lower'] = df['bb_middle'] - (bb_std * 2) + + df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['bb_middle'] + + denom = (df['bb_upper'] - df['bb_lower']) + denom = denom.replace(0, np.nan) + df['bb_percent'] = (df['close'] - df['bb_lower']) / denom + return df + +def _add_volatility_features(df: pd.DataFrame) -> pd.DataFrame: + """Adds ATR features.""" + atr_values = atr_nb( + df['high'].to_numpy(), + df['low'].to_numpy(), + df['close'].to_numpy(), + config.ATR_PERIOD + ) + df['atr'] = atr_values + return df + +def _add_onchain_features(df: pd.DataFrame) -> pd.DataFrame: + """Adds on-chain features with z-score normalization and cycle MAs.""" + for col_name in config.ONCHAIN_FEATURE_NAMES: + if col_name in df.columns: + col_data = df[col_name] + # Ensure numeric + df[col_name] = pd.to_numeric(col_data, errors='coerce') + + # Custom transforms + if col_name == 'net_exchange_flow': + df[col_name] = -df[col_name] # Outflows bullish + elif col_name == 'funding_rate': + df[col_name] *= 100 # To % + + # Rolling Z-Score (prevents look-ahead bias) + if df[col_name].notna().sum() > MIN_PERIODS_ZSCORE: + rolling_mean = df[col_name].rolling( + window=ZSCORE_LOOKBACK, min_periods=MIN_PERIODS_ZSCORE).mean() + rolling_std = df[col_name].rolling( + window=ZSCORE_LOOKBACK, min_periods=MIN_PERIODS_ZSCORE).std() + + rolling_std = rolling_std.replace(0, np.nan) + df[f'{col_name}_z'] = (df[col_name] - rolling_mean) / rolling_std + + # Cycle MAs for key metrics + if col_name == 'nupl': + df[f'{col_name}_ma_{config.NUPL_MA_PERIODS}'] = df[col_name].rolling( + config.NUPL_MA_PERIODS).mean() + elif col_name == 'mvrv': + df[f'{col_name}_ma_{config.MVRV_MA_PERIODS}'] = df[col_name].rolling( + config.MVRV_MA_PERIODS).mean() + else: + # Create NaN columns if missing so model doesn't crash (or handle later) + df[f'{col_name}_z'] = np.nan + + return df + +def _add_target_variables(df: pd.DataFrame) -> pd.DataFrame: + """Adds target variables for ML training.""" + # 1% gain threshold in prediction horizon + df['future_price'] = df['close'].shift(-config.PREDICTION_PERIOD) + future_ret = (df['future_price'] - df['close']) / df['close'] + + df['target'] = (future_ret > 0.01).astype(int) + return df + +def create_features(df: pd.DataFrame) -> pd.DataFrame: + """Engineers all features using modular sub-functions.""" + df_feat = df.copy() + + # Pass through raw data needed for ATR/exits + # Ensure lowercase columns first + mapper = {c: c.lower() for c in df_feat.columns if c in ['Open', 'High', 'Low', 'Close', 'Volume']} + if mapper: + df_feat = df_feat.rename(columns=mapper) + + df_feat['high_raw'] = df_feat['high'] + df_feat['low_raw'] = df_feat['low'] + df_feat['close_raw'] = df_feat['close'] + + # 1. Price-Action + df_feat = _add_price_action_features(df_feat) + + # 2. Mean-Reversion + df_feat = _add_mean_reversion_features(df_feat) + + # 3. Volatility + df_feat = _add_volatility_features(df_feat) + + # 4. On-Chain + df_feat = _add_onchain_features(df_feat) + + # 5. Target + df_feat = _add_target_variables(df_feat) + + return df_feat diff --git a/metrics.py b/metrics.py index 82d80be..6e8e599 100644 --- a/metrics.py +++ b/metrics.py @@ -19,7 +19,21 @@ class Perf: avg_slippage_bps: float -def compute_metrics(equity_curve: pd.Series, trades: list[dict]) -> Perf: +def compute_metrics(equity_curve: pd.Series, trades: list[dict], periods_per_year: int = None) -> Perf: + """ + Compute backtest performance metrics. + + Args: + equity_curve: Series of portfolio values over time + trades: List of trade event dictionaries + periods_per_year: Number of periods in a year for Sharpe annualization. + Defaults to hourly (252 * 24 = 6048). + For minute bars: 252 * 24 * 60 = 525600 + For daily bars: 252 + """ + if periods_per_year is None: + periods_per_year = 252 * 24 # Default to hourly bars + ret = equity_curve.pct_change().fillna(0.0) total_return = equity_curve.iat[-1] / equity_curve.iat[0] - 1.0 cummax = equity_curve.cummax() @@ -27,7 +41,7 @@ def compute_metrics(equity_curve: pd.Series, trades: list[dict]) -> Perf: max_drawdown = dd if ret.std(ddof=0) > 0: - sharpe = (ret.mean() / ret.std(ddof=0)) * np.sqrt(252 * 24 * 60) # minute bars -> annualized + sharpe = (ret.mean() / ret.std(ddof=0)) * np.sqrt(periods_per_year) else: sharpe = 0.0 diff --git a/prepare_data.py b/prepare_data.py new file mode 100644 index 0000000..3ef343a --- /dev/null +++ b/prepare_data.py @@ -0,0 +1,140 @@ +import pandas as pd +import strategy_config as config +from cryptoquant_client import CryptoQuantClient +from features import create_features +import argparse +import os +import time + +def fetch_onchain_data(client, asset='BTC', days_back=365*2): + """Fetches and aligns on-chain data.""" + print(f"Fetching on-chain data for {asset}...") + + # 1. Fetch all metrics + # Note: This might take a while due to rate limits + raw_data = client.fetch_all_onchain(asset, days_back=days_back) + + dfs = [] + for metric_name, records in raw_data.items(): + if not records: + print(f"⚠️ No data for {metric_name}") + continue + + df = pd.DataFrame(records) + # Standardize date column + if 'date' in df.columns: + df['timestamp'] = pd.to_datetime(df['date']) + else: + # Try to find date-like column + cols = [c for c in df.columns if 'date' in c or 'time' in c] + if cols: + df['timestamp'] = pd.to_datetime(df[cols[0]]) + else: + print(f"❌ Could not find date column for {metric_name}") + continue + + df = df.set_index('timestamp').sort_index() + + # Keep only the value column + # Value column name varies by endpoint. + # usually same as metric name or 'value' or specific name. + # Simple heuristic: take the first numeric column that isn't 'timestamp' + numeric_cols = df.select_dtypes(include=['number']).columns + if len(numeric_cols) > 0: + val_col = numeric_cols[0] + df = df[[val_col]].rename(columns={val_col: metric_name}) + + # Resample to hourly and forward fill (since on-chain is daily/block) + # CAUTION: Daily data from CryptoQuant (e.g. Total Active Addresses) is usually + # timestamped at 00:00 but represents the FULL day's activity. + # If we use it at 10:00 AM on the same day, that is Lookahead Bias. + # We must SHIFT it by 1 day to ensure we only use it AFTER it's available (next day). + # Funding rates might be 8h, but 'window=day' implies daily aggregation. + # Safer to lag by 24h. + + df = df.shift(1, freq='D') # Shift index by 1 Day + + df = df.resample('1h').ffill() + dfs.append(df) + else: + print(f"❌ No numeric data for {metric_name}") + + if not dfs: + return pd.DataFrame() + + # Concatenate all on-chain metrics + onchain_df = pd.concat(dfs, axis=1) + return onchain_df + +def main(): + parser = argparse.ArgumentParser(description="Prepare data for MVRV Strategy") + parser.add_argument("--csv", required=True, help="Path to OHLCV CSV file") + parser.add_argument("--days", type=int, default=365, help="Days of on-chain data to fetch") + args = parser.parse_args() + + # 1. Load OHLCV + print(f"Loading OHLCV from {args.csv}...") + df_ohlcv = pd.read_csv(args.csv) + + # Standardize OHLCV columns/index + # Expecting Timestamp/Date column + if 'Timestamp' in df_ohlcv.columns: + # Smart detection of unit + ts_max = df_ohlcv['Timestamp'].max() + if ts_max < 3000000000: # < 3B, likely seconds (valid until ~2065) + unit = 's' + elif ts_max < 3000000000000: # < 3T, likely milliseconds + unit = 'ms' + else: + unit = None # Let pandas guess (ns?) + + df_ohlcv['timestamp'] = pd.to_datetime(df_ohlcv['Timestamp'], unit=unit) + elif 'Date' in df_ohlcv.columns: + df_ohlcv['timestamp'] = pd.to_datetime(df_ohlcv['Date']) + + df_ohlcv = df_ohlcv.set_index('timestamp').sort_index() + + # Resample to 1H for feature engineering + df_1h = df_ohlcv.resample('1h').agg({ + 'Open': 'first', + 'High': 'max', + 'Low': 'min', + 'Close': 'last', + 'Volume': 'sum' + }).dropna() + + # Rename to lowercase for features.py + df_1h = df_1h.rename(columns={ + 'Open': 'open', 'High': 'high', 'Low': 'low', 'Close': 'close', 'Volume': 'volume' + }) + + print(f"OHLCV 1H shape: {df_1h.shape}") + + # 2. Fetch On-Chain + client = CryptoQuantClient() + df_onchain = fetch_onchain_data(client, asset=config.ASSET, days_back=args.days) + print(f"On-Chain shape: {df_onchain.shape}") + + # 3. Merge + # Left join on OHLCV index + df_merged = df_1h.join(df_onchain, how='left') + + # Forward fill on-chain data (it's slower than price) + df_merged = df_merged.ffill() + + # Drop rows where we still have NaNs (start of data) + df_merged = df_merged.dropna() + print(f"Merged shape: {df_merged.shape}") + + # 4. Create Features + print("Engineering features...") + df_features = create_features(df_merged) + df_features = df_features.dropna() + + # 5. Save + print(f"Saving features to {config.FEATURES_PATH}...") + df_features.to_csv(config.FEATURES_PATH) + print("Done.") + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 12727f2..f096102 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,5 +5,10 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ + "numba>=0.63.1", + "python-dotenv>=1.2.1", + "requests>=2.32.5", + "scikit-learn>=1.8.0", "ta>=0.11.0", + "xgboost>=3.1.2", ] diff --git a/strategy_config.py b/strategy_config.py new file mode 100644 index 0000000..c364e60 --- /dev/null +++ b/strategy_config.py @@ -0,0 +1,95 @@ +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# ============================================================================== +# --- 1. MASTER STRATEGY CONFIG --- +# ============================================================================== +ASSET = 'BTC' +TIMEFRAME_HOURS = 1.0 # We work with 1h candles for the ML model + +# --- Define Strategy Logic (in HOURS) --- +PREDICT_HOURS = 5.0 +SMA_FAST_HOURS = 12.0 +SMA_SLOW_HOURS = 50.0 +VOLATILITY_HOURS = 12.0 +BBAND_HOURS = 20.0 +MOMENTUM_1_HOURS = 5.0 +MOMENTUM_2_HOURS = 10.0 +ATR_PERIOD_HOURS = 10.0 + +# --- Backtest / Trade Config --- +PROB_THRESHOLD = 0.55 +SL_ATR_MULT = 1.5 # Widened from 0.8 +TP_ATR_MULT = 3.0 # Widened from 1.5 +FEES_PERCENT = 0.001 +SLIPPAGE_PERCENT = 0.001 + +# --- MVRV/NUPL Strategy-Specific Config --- +NUPL_MA_HOURS = 200.0 # ~200-day MA (normalized to hours if needed, source used hours directly) +MVRV_MA_HOURS = 111.0 # ~111-day MA + +# Thresholds for "Overheated" Regime +MVRV_Z_THRESH = 1.5 +NUPL_THRESH = 0.6 +FUNDING_FILTER = -0.05 # Filter out if funding rate is below this (but source used > -0.05, wait check source) + +# Source: (fund_grid_bool_base > funding_rate_filters_b) where filter is -0.05. +# So we want Funding Rate > -0.05. + +# ============================================================================== +# --- 2. CRYPTOQUANT METRICS --- +# ============================================================================== +ONCHAIN_FEATURE_NAMES = [ + 'funding_rate', 'sopr_ratio', 'leverage_ratio', + 'net_exchange_flow', 'fund_flow_ratio', + 'exchange_whale_ratio', + 'nupl', 'mvrv', 'lth_sopr', 'puell_multiple', + 'active_addresses' +] + +# ============================================================================== +# --- 3. AUTO-CALCULATED PARAMETERS --- +# ============================================================================== +import math + +def hours_to_candles(hours): + return math.ceil(hours / TIMEFRAME_HOURS) + +PREDICTION_PERIOD = hours_to_candles(PREDICT_HOURS) +SMA_FAST_PERIODS = hours_to_candles(SMA_FAST_HOURS) +SMA_SLOW_PERIODS = hours_to_candles(SMA_SLOW_HOURS) +VOLATILITY_PERIODS = hours_to_candles(VOLATILITY_HOURS) +BBAND_PERIODS = hours_to_candles(BBAND_HOURS) +MOMENTUM_1_PERIODS = hours_to_candles(MOMENTUM_1_HOURS) +MOMENTUM_2_PERIODS = hours_to_candles(MOMENTUM_2_HOURS) +ATR_PERIOD = hours_to_candles(ATR_PERIOD_HOURS) + +NUPL_MA_PERIODS = hours_to_candles(NUPL_MA_HOURS) +MVRV_MA_PERIODS = hours_to_candles(MVRV_MA_HOURS) + +# --- FEATURE NAMES --- +BASE_FEATURE_NAMES = [ + 'returns', 'log_returns', f'momentum_{MOMENTUM_1_PERIODS}', + f'momentum_{MOMENTUM_2_PERIODS}', f'SMA_{SMA_FAST_PERIODS}', + f'SMA_{SMA_SLOW_PERIODS}', f'volatility_{VOLATILITY_PERIODS}', + 'bb_lower', 'bb_middle', 'bb_upper', 'bb_width', 'bb_percent', 'atr' +] + +# Append cycle MAs +BASE_FEATURE_NAMES += [ + f'nupl_ma_{NUPL_MA_PERIODS}', f'mvrv_ma_{MVRV_MA_PERIODS}' +] + +ONCHAIN_Z_SCORES = [f'{feat}_z' for feat in ONCHAIN_FEATURE_NAMES] +FEATURE_NAMES = BASE_FEATURE_NAMES + ONCHAIN_Z_SCORES + +# --- PATHS --- +DATA_DIR = 'data' +if not os.path.exists(DATA_DIR): + os.makedirs(DATA_DIR) + +FEATURES_PATH = os.path.join(DATA_DIR, 'features.csv') +MODEL_PATH = os.path.join(DATA_DIR, 'model.pkl') diff --git a/train_model.py b/train_model.py new file mode 100644 index 0000000..7c8ba28 --- /dev/null +++ b/train_model.py @@ -0,0 +1,129 @@ +import pandas as pd +import numpy as np +import xgboost as xgb +import pickle +import sys +from pathlib import Path +from sklearn.model_selection import train_test_split +from sklearn.metrics import accuracy_score, classification_report, precision_recall_fscore_support +import strategy_config as config + +def train_model(): + print(f"--- Starting Model Training Pipeline ---") + + try: + if not os.path.exists(config.FEATURES_PATH): + print(f"Error: {config.FEATURES_PATH} not found. Run prepare_data.py first.") + return + + df = pd.read_csv(config.FEATURES_PATH) + # Ensure index if needed, but read_csv usually reads generic index unless specified + # prepare_data saved with index (timestamp) + if 'timestamp' in df.columns: + df = df.set_index('timestamp') + + print(f"Loaded {len(df)} data points from {config.FEATURES_PATH}") + + y = df['target'] + print(f"Buy signals rate: {y.mean():.1%}") + + # Use the dynamic feature list directly from config.py + # Check if all features exist + available_feats = [f for f in config.FEATURE_NAMES if f in df.columns] + missing_feats = [f for f in config.FEATURE_NAMES if f not in df.columns] + + if missing_feats: + print(f"⚠️ Missing features: {missing_feats}") + print(f"Proceeding with {len(available_feats)} features.") + + X = df[available_feats] + + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.3, shuffle=False + ) + + # Save the test set start index for the backtester to use + # This prevents train/test leakage during backtesting + test_start_idx = len(X_train) + test_start_timestamp = df.index[test_start_idx] if hasattr(df.index, '__getitem__') else test_start_idx + + # Save split info + split_info = { + 'test_start_idx': test_start_idx, + 'test_start_timestamp': str(test_start_timestamp), + 'train_size': len(X_train), + 'test_size': len(X_test) + } + split_info_path = config.MODEL_PATH.replace('.pkl', '_split.pkl') + with open(split_info_path, 'wb') as f: + pickle.dump(split_info, f) + print(f"Split info saved: Test starts at index {test_start_idx} ({test_start_timestamp})") + + print(f"Training set size: {len(X_train)}") + print(f"Test set size: {len(X_test)}") + + print("\nTraining XGBoost model...") + model = xgb.XGBClassifier( + objective='binary:logistic', + eval_metric='logloss', + n_estimators=200, + learning_rate=0.05, + scale_pos_weight=8.0, + max_depth=5, + subsample=0.8, + random_state=42, + early_stopping_rounds=10 + ) + + model.fit( + X_train, y_train, + eval_set=[(X_test, y_test)], + verbose=False + ) + print("Model training complete.") + + y_pred = model.predict(X_test) + y_proba = model.predict_proba(X_test)[:, 1] + + accuracy = accuracy_score(y_test, y_pred) + print(f"\n--- Model Evaluation ---") + print(f"Accuracy on Test Set: {accuracy * 100:.2f}%") + print("\nClassification Report:") + print(classification_report(y_test, y_pred, target_names=['Hold/Sell (0)', 'Buy (1)'])) + + print("\n--- Probability Threshold Analysis ---") + thresholds = [0.35, 0.40, 0.45, 0.50, 0.55, 0.60] + for thresh in thresholds: + pred_at_thresh = (y_proba >= thresh).astype(int) + if pred_at_thresh.sum() > 0: + precision, recall, f1, _ = precision_recall_fscore_support( + y_test, pred_at_thresh, average='binary', zero_division=0 + ) + signal_rate = pred_at_thresh.mean() * 100 + print(f" Thresh {thresh:.2f}: Precision={precision:.2f}, Recall={recall:.2f}, " + f"F1={f1:.2f}, Signals={signal_rate:.1f}%") + else: + print(f" Thresh {thresh:.2f}: No signals generated") + + with open(config.MODEL_PATH, 'wb') as f: + pickle.dump(model, f) + + print(f"\nSUCCESS: Model saved to {config.MODEL_PATH}") + + # Feature Importance + importance = pd.DataFrame({ + 'feature': X.columns, + 'importance': model.feature_importances_ + }).sort_values('importance', ascending=False) + + print("\nTop 10 Features:") + print(importance.head(10)) + + except Exception as e: + print(f"AN ERROR OCCURRED: {e}") + import traceback + traceback.print_exc() + +import os +if __name__ == "__main__": + train_model() diff --git a/uv.lock b/uv.lock index d7fed2f..59fc1d2 100644 --- a/uv.lock +++ b/uv.lock @@ -2,16 +2,156 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172", size = 37232767, upload-time = "2025-12-08T18:15:13.22Z" }, + { url = "https://files.pythonhosted.org/packages/0e/54/737755c0a91558364b9200702c3c9c15d70ed63f9b98a2c32f1c2aa1f3ba/llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54", size = 56275176, upload-time = "2025-12-08T18:15:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/14f32e1d70905c1c0aa4e6609ab5d705c3183116ca02ac6df2091868413a/llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12", size = 55128629, upload-time = "2025-12-08T18:15:19.493Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a7/d526ae86708cea531935ae777b6dbcabe7db52718e6401e0fb9c5edea80e/llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35", size = 38138941, upload-time = "2025-12-08T18:15:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/af0ffb724814cc2ea64445acad05f71cff5f799bb7efb22e47ee99340dbc/llvmlite-0.46.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:d252edfb9f4ac1fcf20652258e3f102b26b03eef738dc8a6ffdab7d7d341d547", size = 37232768, upload-time = "2025-12-08T18:15:25.055Z" }, + { url = "https://files.pythonhosted.org/packages/c9/19/5018e5352019be753b7b07f7759cdabb69ca5779fea2494be8839270df4c/llvmlite-0.46.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379fdd1c59badeff8982cb47e4694a6143bec3bb49aa10a466e095410522064d", size = 56275173, upload-time = "2025-12-08T18:15:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c9/d57877759d707e84c082163c543853245f91b70c804115a5010532890f18/llvmlite-0.46.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e8cbfff7f6db0fa2c771ad24154e2a7e457c2444d7673e6de06b8b698c3b269", size = 55128628, upload-time = "2025-12-08T18:15:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" }, +] + [[package]] name = "lowkey-backtest" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "numba" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "scikit-learn" }, { name = "ta" }, + { name = "xgboost" }, ] [package.metadata] -requires-dist = [{ name = "ta", specifier = ">=0.11.0" }] +requires-dist = [ + { name = "numba", specifier = ">=0.63.1" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "scikit-learn", specifier = ">=1.8.0" }, + { name = "ta", specifier = ">=0.11.0" }, + { name = "xgboost", specifier = ">=3.1.2" }, +] + +[[package]] +name = "numba" +version = "0.63.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, + { url = "https://files.pythonhosted.org/packages/57/f7/e19e6eff445bec52dde5bed1ebb162925a8e6f988164f1ae4b3475a73680/numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0bd4fd820ef7442dcc07da184c3f54bb41d2bdb7b35bacf3448e73d081f730dc", size = 2680954, upload-time = "2025-12-10T02:57:24.145Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/1e222edba1e20e6b113912caa9b1665b5809433cbcb042dfd133c6f1fd38/numba-0.63.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53de693abe4be3bd4dee38e1c55f01c55ff644a6a3696a3670589e6e4c39cde2", size = 3809736, upload-time = "2025-12-10T02:57:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/590bad11a8b3feeac30a24d01198d46bdb76ad15c70d3a530691ce3cae58/numba-0.63.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81227821a72a763c3d4ac290abbb4371d855b59fdf85d5af22a47c0e86bf8c7e", size = 3508854, upload-time = "2025-12-10T02:57:27.438Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f5/3800384a24eed1e4d524669cdbc0b9b8a628800bb1e90d7bd676e5f22581/numba-0.63.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb227b07c2ac37b09432a9bda5142047a2d1055646e089d4a240a2643e508102", size = 2750228, upload-time = "2025-12-10T02:57:30.36Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/53be2aa8a55ee2608ebe1231789cbb217f6ece7f5e1c685d2f0752e95a5b/numba-0.63.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f180883e5508940cc83de8a8bea37fc6dd20fbe4e5558d4659b8b9bef5ff4731", size = 2681153, upload-time = "2025-12-10T02:57:32.016Z" }, + { url = "https://files.pythonhosted.org/packages/13/91/53e59c86759a0648282368d42ba732c29524a745fd555ed1fb1df83febbe/numba-0.63.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0938764afa82a47c0e895637a6c55547a42c9e1d35cac42285b1fa60a8b02bb", size = 3778718, upload-time = "2025-12-10T02:57:33.764Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/2be19eba50b0b7636f6d1f69dfb2825530537708a234ba1ff34afc640138/numba-0.63.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f90a929fa5094e062d4e0368ede1f4497d5e40f800e80aa5222c4734236a2894", size = 3478712, upload-time = "2025-12-10T02:57:35.518Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/4d0c9e756732577a52211f31da13a3d943d185f7fb90723f56d79c696caa/numba-0.63.1-cp314-cp314-win_amd64.whl", hash = "sha256:8d6d5ce85f572ed4e1a135dbb8c0114538f9dd0e3657eeb0bb64ab204cbe2a8f", size = 2752161, upload-time = "2025-12-10T02:57:37.12Z" }, +] [[package]] name = "numpy" @@ -76,6 +216,15 @@ wheels = [ { 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 = "nvidia-nccl-cu12" +version = "2.29.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/b2/e4dc7b33020645746710040cb2a6ac0de8332687d3ce902156dd3d7c351a/nvidia_nccl_cu12-2.29.2-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:0712e55c067965c6093cc793a9bbcc5f37b5b47248e9ebf8ae3af06867757587", size = 289707761, upload-time = "2026-01-07T00:21:30.514Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/609d0392d992259c6dc39881688a7fc13b1397a668bc360fbd68d1396f85/nvidia_nccl_cu12-2.29.2-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:3a9a0bf4142126e0d0ed99ec202579bef8d007601f9fab75af60b10324666b12", size = 289762233, upload-time = "2026-01-07T00:21:56.124Z" }, +] + [[package]] name = "pandas" version = "2.3.1" @@ -122,6 +271,15 @@ 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 = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -131,6 +289,126 @@ 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 = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -150,6 +428,15 @@ dependencies = [ ] 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 = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + [[package]] name = "tzdata" version = "2025.2" @@ -158,3 +445,30 @@ sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be76 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" }, ] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "xgboost" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/64/42310363ecd814de5930981672d20da3d35271721ad2d2b4970b4092825b/xgboost-3.1.2.tar.gz", hash = "sha256:0f94496db277f5c227755e1f3ec775c59bafae38f58c94aa97c5198027c93df5", size = 1237438, upload-time = "2025-11-20T18:33:29.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/1e/efdd603db8cb37422b01d925f9cce1baaac46508661c73f6aafd5b9d7c51/xgboost-3.1.2-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:b44f6ee43a28b998e289ab05285bd65a65d7999c78cf60b215e523d23dc94c5d", size = 2377854, upload-time = "2025-11-20T18:06:21.217Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c6/ed928cb106f56ab73b3f4edb5287c1352251eb9225b5932d3dd5e2803f60/xgboost-3.1.2-py3-none-macosx_12_0_arm64.whl", hash = "sha256:09690f7430504fcd3b3e62bf826bb1282bb49873b68b07120d2696ab5638df41", size = 2211078, upload-time = "2025-11-20T18:06:47.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/2f/5418f4b1deaf0886caf81c5e056299228ac2fc09b965a2dfe5e4496331c8/xgboost-3.1.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f9b83f39340e5852bbf3e918318e7feb7a2a700ac7e8603f9bc3a06787f0d86b", size = 4953319, upload-time = "2025-11-20T18:28:29.851Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/c60fcc137fa685533bb31e721de3ecc88959d393830d59d0204c5cbd2c85/xgboost-3.1.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:24879ac75c0ee21acae0101f791bc43303f072a86d70fdfc89dae10a0008767f", size = 115885060, upload-time = "2025-11-20T18:32:00.773Z" }, + { url = "https://files.pythonhosted.org/packages/30/7d/41847e45ff075f3636c95d1000e0b75189aed4f1ae18c36812575bb42b4b/xgboost-3.1.2-py3-none-win_amd64.whl", hash = "sha256:e627c50003269b4562aa611ed348dff8cb770e11a9f784b3888a43139a0f5073", size = 71979118, upload-time = "2025-11-20T18:27:55.23Z" }, +]