Add MVRV strategy backtesting module with feature engineering and on-chain data integration. Implement model training and evaluation pipeline, including probability threshold analysis. Update configuration for strategy parameters and enhance logging for trade results. Include instructions for running the backtest and preparing data.

This commit is contained in:
Simon Moisy 2026-01-10 06:10:35 +08:00
parent c4aa965a98
commit 5cc6791877
12 changed files with 1444 additions and 5 deletions

2
.vscode/launch.json vendored
View File

@ -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",

42
INSTRUCTIONS_MVRV.md Normal file
View File

@ -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 <path_to_your_ohlcv.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 <path_to_your_ohlcv.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).

View File

@ -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

270
backtest_mvrv.py Normal file
View File

@ -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()

263
cryptoquant_client.py Normal file
View File

@ -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}")

164
features.py Normal file
View File

@ -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

View File

@ -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

140
prepare_data.py Normal file
View File

@ -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()

View File

@ -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",
]

95
strategy_config.py Normal file
View File

@ -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')

129
train_model.py Normal file
View File

@ -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()

316
uv.lock generated
View File

@ -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" },
]