From 7e7a737134fa2e3f8e5d7ba71ec6b510be86d5af Mon Sep 17 00:00:00 2001 From: Simon Moisy Date: Wed, 5 Nov 2025 16:39:46 +0800 Subject: [PATCH] init --- .gitignore | 2 + .python-version | 1 + README.md | 2 - database_manager.py | 265 ++++ main.py | 6 + okxtrading2.0.py | 3032 +++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 14 + requirements.txt | 6 + uv.lock | 790 +++++++++++ 9 files changed, 4116 insertions(+), 2 deletions(-) create mode 100644 .python-version delete mode 100644 README.md create mode 100644 database_manager.py create mode 100644 main.py create mode 100644 okxtrading2.0.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 76c2c3b..2351f34 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ wheels/ # Custom .env *.db +*.json +*.log diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/README.md b/README.md deleted file mode 100644 index 8af09fe..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# OKXTrading - diff --git a/database_manager.py b/database_manager.py new file mode 100644 index 0000000..8cd1447 --- /dev/null +++ b/database_manager.py @@ -0,0 +1,265 @@ +import sqlite3 +import threading +import logging +import os +from datetime import datetime + +logger = logging.getLogger(__name__) + +class DatabaseManager: + """Database Manager""" + + def __init__(self, db_path=None): + # Get database path from environment variable, use default if not set + if db_path is None: + db_path = os.getenv('DB_PATH', 'trading_system.db') + self.db_path = db_path + self.lock = threading.Lock() + self.init_database() + logger.info(f"Database initialization completed, path: {self.db_path}") + + def init_database(self): + """Initialize database""" + with self.lock: + try: # New: try-except to handle database initialization errors + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Create positions table (no changes) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS positions ( + symbol TEXT PRIMARY KEY, + base_amount REAL NOT NULL DEFAULT 0, + entry_price REAL NOT NULL DEFAULT 0, + stop_loss_price REAL, + take_profit_price REAL, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create trade records table (no changes) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS trade_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + side TEXT NOT NULL, + amount REAL NOT NULL, + price REAL NOT NULL, + order_id TEXT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create dynamic stops table (replacement: added highest_price and updated_at fields, no default values) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS dynamic_stops ( + symbol TEXT PRIMARY KEY, + initial_price REAL NOT NULL, + current_stop_loss REAL, + current_take_profit REAL, + trailing_percent REAL DEFAULT 0.03, + highest_price REAL, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP + ) + ''') + + # New: Migration step - check and add missing columns + # Get dynamic_stops table column information + cursor.execute("PRAGMA table_info(dynamic_stops)") + columns = [col[1] for col in cursor.fetchall()] # col[1] is column name + + # If highest_price doesn't exist, add it (no default value) + if 'highest_price' not in columns: + cursor.execute('ALTER TABLE dynamic_stops ADD COLUMN highest_price REAL') + logger.info("Migration: Added dynamic_stops.highest_price column") + + # If updated_at doesn't exist, add it (no default value) + if 'updated_at' not in columns: + cursor.execute('ALTER TABLE dynamic_stops ADD COLUMN updated_at TIMESTAMP') + logger.info("Migration: Added dynamic_stops.updated_at column") + + # New: Set initial values for existing records + # Set highest_price = initial_price if NULL + cursor.execute(''' + UPDATE dynamic_stops + SET highest_price = initial_price + WHERE highest_price IS NULL + ''') + + # Set updated_at = CURRENT_TIMESTAMP if NULL + cursor.execute(''' + UPDATE dynamic_stops + SET updated_at = CURRENT_TIMESTAMP + WHERE updated_at IS NULL + ''') + + conn.commit() + except Exception as e: + logger.error(f"Database initialization error: {e}") + finally: + conn.close() + + def save_position(self, symbol, base_amount, entry_price, stop_loss_price=None, take_profit_price=None): + """Save position status""" + with self.lock: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + INSERT OR REPLACE INTO positions + (symbol, base_amount, entry_price, stop_loss_price, take_profit_price, updated_time) + VALUES (?, ?, ?, ?, ?, ?) + ''', (symbol, base_amount, entry_price, stop_loss_price, take_profit_price, datetime.now())) + + conn.commit() + conn.close() + + def load_position(self, symbol): + """Load position status""" + with self.lock: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + SELECT base_amount, entry_price, stop_loss_price, take_profit_price + FROM positions WHERE symbol = ? + ''', (symbol,)) + + result = cursor.fetchone() + conn.close() + + if result: + return { + 'base_amount': result[0], + 'entry_price': result[1], + 'stop_loss_price': result[2], + 'take_profit_price': result[3] + } + return None + + def delete_position(self, symbol): + """Delete position record""" + with self.lock: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute('DELETE FROM positions WHERE symbol = ?', (symbol,)) + conn.commit() + conn.close() + + def save_trade_record(self, symbol, side, amount, price, order_id): + """Save trade record""" + with self.lock: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO trade_records + (symbol, side, amount, price, order_id) + VALUES (?, ?, ?, ?, ?) + ''', (symbol, side, amount, price, order_id)) + + conn.commit() + conn.close() + + def set_dynamic_stop(self, symbol, initial_price, trailing_percent=0.03, multiplier=2): + """Set dynamic stop-loss""" + with self.lock: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + current_stop_loss = initial_price * (1 - trailing_percent) + current_take_profit = initial_price * (1 + trailing_percent * multiplier) + highest_price = initial_price + + # Manually set updated_at = CURRENT_TIMESTAMP during INSERT + cursor.execute(''' + INSERT OR REPLACE INTO dynamic_stops + (symbol, initial_price, current_stop_loss, current_take_profit, trailing_percent, highest_price, updated_at) + VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ''', (symbol, initial_price, current_stop_loss, current_take_profit, trailing_percent, highest_price)) + + conn.commit() + conn.close() + + def update_dynamic_stop(self, symbol, current_price, trailing_percent=0.03, multiplier=2): + """Update dynamic stop-loss (trailing take-profit)""" + with self.lock: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # First query current highest_price and current_stop_loss + cursor.execute('SELECT highest_price, current_stop_loss FROM dynamic_stops WHERE symbol = ?', (symbol,)) + result = cursor.fetchone() + + if result: + highest_price, current_stop_loss = result + if highest_price is None: + highest_price = current_price + logger.warning(f"Dynamic stop record {symbol} highest_price is empty, initializing with current price") + if current_stop_loss is None: + current_stop_loss = current_price * (1 - trailing_percent) + logger.warning(f"Dynamic stop record {symbol} current_stop_loss is empty, recalculating") + + # Only update if current_price > highest_price + if current_price > highest_price: + highest_price = current_price + updated = True + + # Calculate new_stop_loss based on highest_price (only moves upward) + new_stop_loss = highest_price * (1 - trailing_percent) + if new_stop_loss > current_stop_loss: + current_stop_loss = new_stop_loss + updated = True + + # Update take_profit based on highest_price + current_take_profit = highest_price * (1 + trailing_percent * multiplier) + + if updated: + # Manually set updated_at = CURRENT_TIMESTAMP during UPDATE + cursor.execute(''' + UPDATE dynamic_stops + SET highest_price = ?, current_stop_loss = ?, current_take_profit = ?, updated_at = CURRENT_TIMESTAMP + WHERE symbol = ? + ''', (highest_price, current_stop_loss, current_take_profit, symbol)) + conn.commit() + + conn.close() + return current_stop_loss if result else None + + def get_dynamic_stop(self, symbol): + """Get dynamic stop-loss information""" + with self.lock: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Handle potentially missing highest_price during SELECT (use COALESCE to fall back to initial_price) + cursor.execute(''' + SELECT current_stop_loss, current_take_profit, trailing_percent, + COALESCE(highest_price, initial_price) AS highest_price + FROM dynamic_stops WHERE symbol = ? + ''', (symbol,)) + + result = cursor.fetchone() + conn.close() + + if result: + return { + 'current_stop_loss': result[0], + 'current_take_profit': result[1], + 'trailing_percent': result[2], + 'highest_price': result[3] + } + return None + + def delete_dynamic_stop(self, symbol): + """Delete dynamic stop-loss record""" + with self.lock: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute('DELETE FROM dynamic_stops WHERE symbol = ?', (symbol,)) + conn.commit() + conn.close() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..3818585 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from okxtrading2-0!") + + +if __name__ == "__main__": + main() diff --git a/okxtrading2.0.py b/okxtrading2.0.py new file mode 100644 index 0000000..b05ec29 --- /dev/null +++ b/okxtrading2.0.py @@ -0,0 +1,3032 @@ +import os +import time +import threading +import logging +import pandas as pd +import numpy as np +import requests +import json +import hashlib +import hmac +import base64 +import schedule +import okx.Account as Account +import okx.MarketData as MarketData +import okx.Trade as Trade +from datetime import datetime, timezone, timedelta +from dotenv import load_dotenv +from logging.handlers import RotatingFileHandler +from database_manager import DatabaseManager +import atexit +import signal + +tdmode = 'cross' + +# Load environment variables +load_dotenv() + +def setup_logging(): + """Configure logging system""" + # Set environment variable to ensure UTF-8 encoding + os.environ['PYTHONIOENCODING'] = 'utf-8' + + # Create formatter + formatter = logging.Formatter( + '%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # File handler - using UTF-8 encoding + file_handler = RotatingFileHandler( + 'trading_system.log', + maxBytes=10 * 1024 * 1024, + backupCount=5, + encoding='utf-8' + ) + file_handler.setFormatter(formatter) + file_handler.setLevel(logging.INFO) + + # Console handler - handle encoding issues + class SafeStreamHandler(logging.StreamHandler): + def emit(self, record): + try: + msg = self.format(record) + stream = self.stream + if hasattr(stream, 'buffer'): + stream.buffer.write(msg.encode('utf-8') + b'\n') + stream.buffer.flush() + else: + stream.write(msg + self.terminator) + self.flush() + except UnicodeEncodeError: + safe_msg = msg.encode('ascii', 'ignore').decode('ascii') + stream.write(safe_msg + self.terminator) + self.flush() + except Exception: + self.handleError(record) + + console_handler = SafeStreamHandler() + console_handler.setFormatter(formatter) + console_handler.setLevel(logging.INFO) + + # Configure root logger + logging.basicConfig( + level=logging.INFO, + handlers=[file_handler, console_handler] + ) + + return logging.getLogger(__name__) + +logger = setup_logging() + +class APIRateLimiter: + """API rate limiter""" + + def __init__(self, calls_per_second=2): + self.min_interval = 1.0 / calls_per_second + self.last_call = {} + self.lock = threading.Lock() + + def wait(self, api_name): + """Wait until API can be called""" + with self.lock: + current_time = time.time() + if api_name in self.last_call: + elapsed = current_time - self.last_call[api_name] + if elapsed < self.min_interval: + time.sleep(self.min_interval - elapsed) + self.last_call[api_name] = current_time + +class OKXAPIClient: + """OKX API Client (using official SDK)""" + + def __init__(self): + self.api_key = os.getenv('OKX_API_KEY') + self.secret_key = os.getenv('OKX_SECRET_KEY') + self.password = os.getenv('OKX_PASSWORD') + + if not all([self.api_key, self.secret_key, self.password]): + raise ValueError("Please set OKX API key and password") + + # Initialize OKX SDK client - using live trading environment + Flag = "1" # Live trading environment + self.account_api = Account.AccountAPI(self.api_key, self.secret_key, self.password, False, Flag) + self.market_api = MarketData.MarketAPI(self.api_key, self.secret_key, self.password, False, Flag) + self.trade_api = Trade.TradeAPI(self.api_key, self.secret_key, self.password, False, Flag) + + # API rate limiting + self.rate_limiter = APIRateLimiter(2) + self.log_http = False + + # Cache instrument info + self.instrument_cache = {} + + def get_market_data(self, symbol, timeframe='1H', limit=200): + """Get market data""" + self.rate_limiter.wait("market_data") + + try: + result = self.market_api.get_candlesticks( + instId=symbol, + bar=timeframe, + limit=str(limit) + ) + + if self.log_http: + logger.debug(f"HTTP Request: GET {symbol} {timeframe} {result.get('code', 'Unknown')}") + + # Error checking + if 'code' not in result or result['code'] != '0': + error_msg = result.get('msg', 'Unknown error') + error_code = result.get('code', 'Unknown code') + logger.error(f"Failed to get {symbol} market data: {error_msg} (code: {error_code})") + return None + + # Check if data exists + if 'data' not in result or not result['data']: + logger.warning(f"{symbol} market data is empty") + return None + + # Create DataFrame + columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCcyQuote', 'confirm'] + df = pd.DataFrame(result['data'], columns=columns) + + # Convert data types + df['timestamp'] = pd.to_datetime(df['timestamp'].astype(np.int64), unit='ms') + numeric_cols = ['open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCcyQuote'] + df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric) + + return df.sort_values('timestamp') + + except Exception as e: + logger.error(f"Error getting market data: {e}") + return None + + def get_account_balance(self): + """Get account balance (USDT)""" + self.rate_limiter.wait("balance") + + try: + result = self.account_api.get_account_balance() + + if self.log_http: + logger.debug(f"HTTP Request: GET balance {result['code']}") + + # Correction: code "0" indicates success + if result['code'] != '0': + logger.error(f"Failed to get balance: {result['msg']} (code: {result['code']})") + return None + + # Extract USDT balance + for currency in result['data'][0]['details']: + if currency['ccy'] == 'USDT': + return float(currency['availBal']) + + return 0.0 + + except Exception as e: + logger.error(f"Error getting balance: {e}") + return None + + def get_currency_balances(self): + """Get all currency balances""" + self.rate_limiter.wait("balances") + + try: + result = self.account_api.get_account_balance() + + # Correction: code "0" indicates success + if result['code'] != '0': + logger.error(f"Failed to get balance: {result['msg']} (code: {result['code']})") + return {} + + # Check response data structure + if not result['data'] or len(result['data']) == 0: + logger.error("No balance data in API response") + return {} + + # Check if details field exists + if 'details' not in result['data'][0]: + logger.error("No details field in API response") + return {} + + balances = {} + for currency in result['data'][0]['details']: + if float(currency.get('availBal', 0)) > 0: + balances[currency['ccy']] = { + 'amount': float(currency.get('availBal', 0)), + 'frozen': float(currency.get('frozenBal', 0)) + } + + return balances + + except Exception as e: + logger.error(f"Error getting currency balances: {e}") + return {} + + def get_positions(self): + """Get exchange position information (based on currency balances)""" + try: + # Get all currency balances + balances = self.get_currency_balances() + if not balances: + return {} + + # Filter out non-USDT currencies as positions + positions = {} + for currency, balance in balances.items(): + if currency != 'USDT' and balance['amount'] > 0: + # Construct trading pair symbol + symbol = f"{currency}-USDT" + # Get current price to calculate position value + current_price = self.get_current_price(symbol) + if current_price: + positions[symbol] = { + 'amount': balance['amount'], + 'value': balance['amount'] * current_price, + 'avg_price': 0.0 # Spot positions don't have average price concept + } + else: + positions[symbol] = { + 'amount': balance['amount'], + 'value': 0.0, + 'avg_price': 0.0 + } + + return positions + + except Exception as e: + logger.error(f"Error getting positions: {e}") + return {} + + def get_current_price(self, symbol): + """Get current price""" + self.rate_limiter.wait("price") + + try: + result = self.market_api.get_ticker(instId=symbol) + + # Correction: code "0" indicates success + if result['code'] != '0': + logger.error(f"Failed to get price: {result['msg']} (code: {result['code']})") + return None + + return float(result['data'][0]['last']) + + except Exception as e: + logger.error(f"Error getting price: {e}") + return None + + def get_instrument_info(self, symbol): + """Get trading pair information""" + if symbol in self.instrument_cache: + return self.instrument_cache[symbol] + + self.rate_limiter.wait("instrument") + + try: + result = self.account_api.get_instruments(instType='SPOT') + + if result['code'] != '0': + logger.error(f"Failed to get instrument: {result['msg']} (code: {result['code']})") + return None, None + + # Find specified trading pair + for inst in result['data']: + if inst['instId'] == symbol: + min_sz = float(inst['minSz']) + lot_sz = float(inst['lotSz']) + logger.debug(f"Got {symbol} precision: minSz={min_sz}, lotSz={lot_sz}") + self.instrument_cache[symbol] = (min_sz, lot_sz) + return min_sz, lot_sz + + logger.error(f"Trading pair not found: {symbol}") + return None, None + + except Exception as e: + logger.error(f"Error getting instrument info: {e}") + return None, None + + def get_default_min_size(self, symbol): + """Get default minimum order size""" + # Set default minimum order size based on currency + defaults = { + 'BTC-USDT': 0.0001, + 'ETH-USDT': 0.001, + 'SOL-USDT': 0.01, + 'XRP-USDT': 1.0 + } + return defaults.get(symbol, 0.01) # Default 0.01 + + def create_order(self, symbol, side, amount, retries=3): + """Create order""" + for attempt in range(retries): + try: + self.rate_limiter.wait("order") + + # Parse trading pair symbol + parts = symbol.split('-') + if len(parts) != 2: + logger.error(f"Invalid trading pair format: {symbol}") + return None + + base_currency, quote_currency = parts + + # Adjust parameters based on buy/sell direction + if side == 'buy': + # When buying, amount is quote currency amount (USDT amount) + # Use amount-based order placement + order_params = { + 'instId': symbol, + 'tdMode': tdmode, + 'side': 'buy', + 'ordType': 'market', + 'sz': str(amount), # Quote currency amount + 'tgtCcy': 'quote_ccy' # Specify sz as quote currency + } + logger.info(f"[{symbol}] Create buy order: amount={amount:.2f} {quote_currency}") + + else: + # When selling, amount is base currency quantity + # Get precision info and adjust quantity + min_sz, lot_sz = self.get_instrument_info(symbol) + if min_sz is None: + min_sz = self.get_default_min_size(symbol) + if lot_sz is None: + lot_sz = min_sz + + # Adjust quantity to appropriate precision + if lot_sz > 0: + amount = (amount / lot_sz) * lot_sz + + amount_str = f"{amount:.10f}" + + order_params = { + 'instId': symbol, + 'tdMode': tdmode, + 'side': 'sell', + 'ordType': 'market', + 'sz': amount_str # Base currency quantity + } + logger.info(f"[{symbol}] Create sell order: quantity={amount_str} {base_currency}") + + # Use SDK to create order + result = self.trade_api.place_order(**order_params) + + if self.log_http: + logger.debug(f"HTTP Request: POST create order {result['code']}") + + # Check API response + if result['code'] != '0': + logger.error(f"Failed to create order: {result['msg']} (code: {result['code']})") + if 'data' in result and len(result['data']) > 0: + for item in result['data']: + logger.error(f"Detailed error: {item.get('sMsg', 'Unknown')} (sCode: {item.get('sCode', 'Unknown')})") + # Specific error handling + if result['code'] == '50113': # Insufficient permissions + logger.error("API key may not have trading permissions, please check API key settings") + elif result['code'] == '51020': # Minimum order amount + logger.error("Order amount below exchange minimum requirement") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + continue + + # Check order status + if len(result['data']) > 0: + order_data = result['data'][0] + if order_data.get('sCode') != '0': + logger.error(f"Order creation failed: {order_data.get('sMsg', 'Unknown error')} (sCode: {order_data.get('sCode', 'Unknown')})") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + continue + + order_id = order_data.get('ordId') + if order_id: + logger.info(f"Order created successfully: {order_id}") + return order_id + else: + logger.error("Order ID is empty") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + continue + else: + logger.error("No order data in API response") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + continue + + except Exception as e: + logger.error(f"Error creating order (attempt {attempt+1}/{retries}): {str(e)}") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + else: + return None + + return None + + def get_order_status(self, symbol, order_id): + """Get order status""" + self.rate_limiter.wait("order_status") + + try: + result = self.trade_api.get_order(instId=symbol, ordId=order_id) + + # Correction: code "0" indicates success + if result['code'] != '0': + logger.error(f"Failed to get order status: {result['msg']} (code: {result['code']})") + return None + + if len(result['data']) > 0: + order_data = result['data'][0] + return { + 'state': order_data.get('state'), + 'avgPx': float(order_data.get('avgPx', 0)), + 'accFillSz': float(order_data.get('accFillSz', 0)), + 'fillPx': float(order_data.get('fillPx', 0)), + 'fillSz': float(order_data.get('fillSz', 0)), + 'fillTime': order_data.get('fillTime') + } + else: + logger.error("No order data in API response") + return None + + except Exception as e: + logger.error(f"Error getting order status: {e}") + return None + + def wait_for_order_completion(self, symbol, order_id, max_attempts=10, interval=1): + """Wait for order completion""" + for attempt in range(max_attempts): + order_status = self.get_order_status(symbol, order_id) + if order_status is None: + return None + + state = order_status['state'] + if state == 'filled': + logger.info(f"Order completed: {order_id}, fill price={order_status['avgPx']:.2f}, fill quantity={order_status['accFillSz']:.10f}") + return order_status + elif state == 'canceled': + logger.warning(f"Order canceled: {order_id}") + return None + elif state == 'partially_filled': + logger.info(f"Order partially filled: {order_id}, filled={order_status['accFillSz']:.10f}") + time.sleep(interval) + else: + logger.info(f"Order status: {state}, waiting...") + time.sleep(interval) + + logger.warning(f"Order not completed within specified time: {order_id}") + return None + +class DeepSeekAnalyzer: + """DeepSeek AI Analyzer (KEN2.0 optimized)""" + + def __init__(self): + self.api_key = os.getenv('DEEPSEEK_API_KEY') + self.api_url = 'https://api.deepseek.com/v1/chat/completions' + + def analyze_market(self, symbol, market_data, technical_indicators): + """Use DeepSeek to analyze market (optimized version)""" + if not self.api_key: + logger.warning("DeepSeek API key not set, skipping AI analysis") + return None + + try: + # Prepare analysis data + latest = market_data.iloc[-1] + technical_summary = self._prepare_enhanced_technical_summary(technical_indicators, market_data) + + prompt = self._create_enhanced_analysis_prompt(symbol, latest, market_data, technical_summary) + response = self._call_deepseek_api(prompt) + + return self._parse_deepseek_response(response) + + except Exception as e: + logger.error(f"DeepSeek analysis error: {e}") + return None + + def _prepare_enhanced_technical_summary(self, indicators, market_data): + """Prepare enhanced technical indicator summary""" + summary = [] + + try: + # Price information + current_price = indicators.get('current_price', 0) + summary.append(f"Current price: ${current_price:.4f}") + + # Multi-period RSI + rsi_7 = indicators.get('rsi_7', 50) + rsi_14 = indicators.get('rsi', 50) + rsi_21 = indicators.get('rsi_21', 50) + if hasattr(rsi_7, '__len__'): + rsi_7, rsi_14, rsi_21 = rsi_7.iloc[-1], rsi_14.iloc[-1], rsi_21.iloc[-1] + + summary.append(f"RSI(7/14/21): {rsi_7:.1f}/{rsi_14:.1f}/{rsi_21:.1f}") + + # RSI status analysis + rsi_status = [] + for rsi_val, period in [(rsi_7, 7), (rsi_14, 14), (rsi_21, 21)]: + if rsi_val > 70: + rsi_status.append(f"RSI{period} overbought") + elif rsi_val < 30: + rsi_status.append(f"RSI{period} oversold") + if rsi_status: + summary.append(f"RSI status: {', '.join(rsi_status)}") + + # KDJ indicator + k, d, j = indicators.get('kdj', (50, 50, 50)) + if hasattr(k, '__len__'): + k, d, j = k.iloc[-1], d.iloc[-1], j.iloc[-1] + + summary.append(f"KDJ: K={k:.1f}, D={d:.1f}, J={j:.1f}") + if j > 80: + summary.append("KDJ status: Overbought area") + elif j < 20: + summary.append("KDJ status: Oversold area") + + # MACD indicator + macd_line, signal_line, histogram = indicators.get('macd', (0, 0, 0)) + if hasattr(macd_line, '__len__'): + macd_line, signal_line, histogram = macd_line.iloc[-1], signal_line.iloc[-1], histogram.iloc[-1] + + summary.append(f"MACD: line={macd_line:.4f}, signal={signal_line:.4f}, histogram={histogram:.4f}") + summary.append(f"MACD status: {'Golden cross' if macd_line > signal_line else 'Death cross'}") + + # Moving averages + sma_20 = indicators.get('sma_20', 0) + sma_50 = indicators.get('sma_50', 0) + sma_100 = indicators.get('sma_100', 0) + if hasattr(sma_20, '__len__'): + sma_20, sma_50, sma_100 = sma_20.iloc[-1], sma_50.iloc[-1], sma_100.iloc[-1] + + price_vs_ma = [] + if current_price > sma_20: price_vs_ma.append("Above 20-day MA") + else: price_vs_ma.append("Below 20-day MA") + if current_price > sma_50: price_vs_ma.append("Above 50-day MA") + else: price_vs_ma.append("Below 50-day MA") + + summary.append(f"Moving averages: 20-day=${sma_20:.2f}, 50-day=${sma_50:.2f}, 100-day=${sma_100:.2f}") + summary.append(f"Price position: {', '.join(price_vs_ma)}") + + # Bollinger Bands + upper, middle, lower = indicators.get('bollinger_2std', (0, 0, 0)) + if hasattr(upper, '__len__'): + upper, middle, lower = upper.iloc[-1], middle.iloc[-1], lower.iloc[-1] + + bb_position = (current_price - lower) / (upper - lower) * 100 if (upper - lower) > 0 else 50 + summary.append(f"Bollinger Band position: {bb_position:.1f}% (0%=lower band, 100%=upper band)") + + # Trend strength + trend_strength = indicators.get('trend_strength', 0) + summary.append(f"Trend strength: {trend_strength:.2f} (0-1, higher means stronger trend)") + + # Volatility + volatility = indicators.get('volatility', 0) * 100 # Convert to percentage + summary.append(f"Annualized volatility: {volatility:.1f}%") + + # ATR + atr = indicators.get('atr', 0) + if hasattr(atr, '__len__'): + atr = atr.iloc[-1] + atr_percent = (atr / current_price * 100) if current_price > 0 else 0 + summary.append(f"ATR: {atr:.4f} ({atr_percent:.2f}%)") + + # Support resistance information + sr_levels = TechnicalAnalyzer.calculate_support_resistance(market_data) + if sr_levels: + summary.append(f"Static support: ${sr_levels['static_support']:.4f}, resistance: ${sr_levels['static_resistance']:.4f}") + summary.append(f"Dynamic support: ${sr_levels['dynamic_support']:.4f}, resistance: ${sr_levels['dynamic_resistance']:.4f}") + summary.append(f"Relative resistance: {sr_levels['current_vs_resistance']:+.2f}%, relative support: {sr_levels['current_vs_support']:+.2f}%") + + return "\n".join(summary) + + except Exception as e: + logger.error(f"Error preparing technical summary: {e}") + return "Technical indicator calculation exception" + + def _create_enhanced_analysis_prompt(self, symbol, latest, market_data, technical_summary): + """Create enhanced analysis prompt""" + # Calculate price changes + price_change_24h = 0 + price_change_7d = 0 + + if len(market_data) >= 24: + price_change_24h = ((latest['close'] - market_data.iloc[-24]['close']) / market_data.iloc[-24]['close'] * 100) + + if len(market_data) >= 168: # 7 days data (hourly) + price_change_7d = ((latest['close'] - market_data.iloc[-168]['close']) / market_data.iloc[-168]['close'] * 100) + + # Calculate volume changes + volume_trend = "Rising" if latest['volume'] > market_data['volume'].mean() else "Falling" + + prompt = f""" +You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market. Please comprehensively analyze the {symbol} trading pair and execute trades. + +# Core Objective +**Maximize Sharpe Ratio** +Sharpe Ratio = Average Return / Return Volatility, which means: +Quality trades (high win rate, large profit/loss ratio) → Improve Sharpe +Stable returns, control drawdowns → Improve Sharpe +Frequent trading, small profits and losses → Increase volatility, severely reduce Sharpe + +【Market Overview】 +- Current price: ${latest['close']:.4f} +- 24-hour change: {price_change_24h:+.2f}% +- 7-day change: {price_change_7d:+.2f}% +- Volume: {latest['volume']:.0f} ({volume_trend}) +- Volume relative level: {'High' if latest['volume'] > market_data['volume'].quantile(0.7) else 'Normal' if latest['volume'] > market_data['volume'].quantile(0.3) else 'Low'} + +【Multi-dimensional Technical Analysis】 +{technical_summary} + +【Market Environment Assessment】 +Please consider comprehensively: +1. Trend direction and strength +2. Overbought/oversold status +3. Volume coordination +4. Volatility level +5. Support resistance positions +6. Multi-timeframe consistency + +【Risk Management Suggestions】 +- Suggested position: Adjust based on signal strength and volatility +- Stop-loss setting: Reference ATR and support levels +- Holding time: Based on trend strength + +【Output Format Requirements】 +Please reply strictly in the following JSON format: +{{ + "action": "BUY/SELL/HOLD", + "confidence": "HIGH/MEDIUM/LOW", + "position_size": "Suggested position ratio (0.1-0.5)", + "stop_loss": "Suggested stop-loss price or percentage", + "take_profit": "Suggested take-profit price or percentage", + "timeframe": "Suggested holding time (SHORT/MEDIUM/LONG)", + "reasoning": "Detailed analysis logic and risk explanation" +}} + +Please provide objective suggestions based on professional technical analysis and risk management principles. +""" + + return prompt + + def analyze_sell_proportion(self, symbol, position_info, market_analysis, current_price): + """Analyze sell proportion""" + if not self.api_key: + logger.warning("DeepSeek API key not set, using default sell proportion") + return self._get_fallback_sell_proportion(position_info, 'MEDIUM') + + try: + prompt = self._create_sell_proportion_prompt(symbol, position_info, market_analysis, current_price) + response = self._call_deepseek_api(prompt) + return self._parse_sell_proportion_response(response) + + except Exception as e: + logger.error(f"DeepSeek sell proportion analysis error: {e}") + return self._get_fallback_sell_proportion(position_info, 'MEDIUM') + + def _create_sell_proportion_prompt(self, symbol, position_info, market_analysis, current_price): + """Create sell proportion analysis prompt""" + + # Calculate profit situation + entry_price = position_info['entry_price'] + profit_ratio = (current_price - entry_price) / entry_price if entry_price > 0 else 0 + profit_status = "Profit" if profit_ratio > 0 else "Loss" + profit_amount = (current_price - entry_price) * position_info['base_amount'] + + # Calculate position ratio + position_ratio = position_info['position_ratio'] * 100 # Convert to percentage + + # Prepare technical indicator summary + technical_summary = self._prepare_enhanced_technical_summary( + market_analysis['technical_indicators'], + market_analysis['market_data'] + ) + + prompt = f""" +You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market. Analyze the {symbol} position situation and consider and execute sell decisions. + +【Current Position Situation】 +- Position quantity: {position_info['base_amount']:.8f} +- Average entry price: ${entry_price:.4f} +- Current price: ${current_price:.4f} +- Profit ratio: {profit_ratio:+.2%} ({profit_status}) +- Profit amount: ${profit_amount:+.2f} +- Position ratio: {position_ratio:.2f}% (percentage of total portfolio) +- Position value: ${position_info['position_value']:.2f} + +【Market Technical Analysis】 +{technical_summary} + +【Risk Situation】 +- Current price distance to stop-loss: {position_info['distance_to_stop_loss']:.2%} +- Current price distance to take-profit: {position_info['distance_to_take_profit']:.2%} +- Market volatility: {position_info['market_volatility']:.2%} +- Trend strength: {position_info.get('trend_strength', 0):.2f} + +【Sell Strategy Considerations】 +Please consider the following factors to determine sell proportion: +1. Profit ratio: Large profits can consider partial profit-taking, keep some position for higher gains +2. Position ratio: Overweight positions should reduce position to lower risk, light positions can consider holding +3. Technical signals: Strong bearish signals should increase sell proportion, bullish signals can reduce sell proportion +4. Market environment: High volatility markets should be more conservative, low volatility markets can be more aggressive +5. Risk control: Close to stop-loss should sell decisively, far from stop-loss can be more flexible + +【Position Management Principles】 +- Profit over 20%: Consider partial profit-taking (30-70%), lock in profits +- Profit 10-20%: Decide based on signal strength (20-50%) +- Small profit (0-10%): Mainly based on technical signals (0-30%) +- Small loss (0-5%): Based on risk control (50-80%) +- Large loss (>5%): Consider stop-loss or position reduction (80-100%) + +【Output Format Requirements】 +Please reply strictly in the following JSON format, only containing numeric ratio (0-1): +{{ + "sell_proportion": 0.75 +}} + +Explanation: +- 0.1 means sell 10% of position +- 0.5 means sell 50% of position +- 1.0 means sell all position +- 0.0 means don't sell (only in extremely bullish situations) + +Please provide reasonable sell proportion suggestions based on professional analysis and risk management. +""" + + return prompt + + def _parse_sell_proportion_response(self, response): + """Parse sell proportion response""" + try: + content = response['choices'][0]['message']['content'] + + # Try to parse JSON format + if '{' in content and '}' in content: + json_start = content.find('{') + json_end = content.rfind('}') + 1 + json_str = content[json_start:json_end] + + parsed = json.loads(json_str) + proportion = parsed.get('sell_proportion', 0.5) + + # Ensure proportion is within reasonable range + proportion = max(0.0, min(1.0, proportion)) + logger.info(f"DeepSeek suggested sell proportion: {proportion:.1%}") + return proportion + else: + # Text analysis: try to extract proportion from text + import re + patterns = [ + r'[\"\"]([\d.]+)%[\"\"]', # "50%" + r'sell\s*([\d.]+)%', # sell 50% + r'([\d.]+)%', # 50% + r'[\"\"]([\d.]+)[\"\"]', # "0.5" + ] + + for pattern in patterns: + match = re.search(pattern, content) + if match: + value = float(match.group(1)) + if value > 1: # If in percentage format + value = value / 100 + proportion = max(0.0, min(1.0, value)) + logger.info(f"Parsed sell proportion from text: {proportion:.1%}") + return proportion + + # Default value + logger.warning("Unable to parse sell proportion, using default 50%") + return 0.5 + + except Exception as e: + logger.error(f"Error parsing sell proportion response: {e}") + return 0.5 + + def _get_fallback_sell_proportion(self, position_info, decision_confidence): + """Fallback sell proportion decision (when DeepSeek is unavailable)""" + + profit_ratio = position_info['profit_ratio'] + position_ratio = position_info['position_ratio'] + market_volatility = position_info.get('market_volatility', 0) + trend_strength = position_info.get('trend_strength', 0) + + # Decision based on profit situation + if profit_ratio >= 0.20: # Profit over 20% + base_proportion = 0.6 if decision_confidence == 'HIGH' else 0.4 + elif profit_ratio >= 0.10: # Profit 10%-20% + base_proportion = 0.5 if decision_confidence == 'HIGH' else 0.3 + elif profit_ratio >= 0: # Profit 0%-10% + base_proportion = 0.4 if decision_confidence == 'HIGH' else 0.2 + elif profit_ratio >= -0.05: # Loss 0%-5% + base_proportion = 0.7 if decision_confidence == 'HIGH' else 0.5 + else: # Loss over 5% + base_proportion = 0.9 if decision_confidence == 'HIGH' else 0.7 + + # Adjustment based on position ratio + if position_ratio > 0.15: # Overweight position + base_proportion = min(1.0, base_proportion + 0.2) + elif position_ratio < 0.05: # Very light position + base_proportion = max(0.1, base_proportion - 0.1) + + # Adjustment based on market volatility + if market_volatility > 0.05: # High volatility market + base_proportion = min(1.0, base_proportion + 0.1) + elif market_volatility < 0.02: # Low volatility market + base_proportion = max(0.1, base_proportion - 0.1) + + # Adjustment based on trend strength + if trend_strength > 0.7: # Strong trend + base_proportion = max(0.1, base_proportion - 0.1) + elif trend_strength < 0.3: # Weak trend + base_proportion = min(1.0, base_proportion + 0.1) + + # Ensure proportion is within reasonable range + base_proportion = max(0.1, min(1.0, base_proportion)) + + logger.info(f"Fallback sell proportion decision: profit={profit_ratio:+.2%}, position ratio={position_ratio:.2%}, " + f"volatility={market_volatility:.2%}, trend strength={trend_strength:.2f}, final proportion={base_proportion:.1%}") + return base_proportion + + def _call_deepseek_api(self, prompt, max_retries=3): + """Call DeepSeek API (improved retry mechanism)""" + headers = { + 'Authorization': f'Bearer {self.api_key}', + 'Content-Type': 'application/json' + } + + data = { + 'model': 'deepseek-chat', + 'messages': [ + { + 'role': 'system', + 'content': 'You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market, pursuing stable and sustainable returns' + }, + { + 'role': 'user', + 'content': prompt + } + ], + 'temperature': 0.3, + 'max_tokens': 1000 + } + + for attempt in range(max_retries): + try: + response = requests.post( + self.api_url, + headers=headers, + json=data, + timeout=30 # 30 second timeout + ) + + if response.status_code == 200: + return response.json() + elif response.status_code == 429: # Rate limit + wait_time = 2 ** attempt # Exponential backoff + logger.warning(f"DeepSeek API rate limit, waiting {wait_time} seconds before retry") + time.sleep(wait_time) + continue + else: + logger.error(f"DeepSeek API call failed: {response.status_code} - {response.text}") + if attempt < max_retries - 1: + time.sleep(1) + continue + else: + raise Exception(f"API call failed: {response.status_code}") + + except requests.exceptions.Timeout: + logger.warning(f"DeepSeek API request timeout, attempt {attempt + 1}/{max_retries}") + if attempt < max_retries - 1: + time.sleep(1) + continue + else: + raise Exception("DeepSeek API request timeout") + except Exception as e: + logger.error(f"DeepSeek API call exception: {e}") + if attempt < max_retries - 1: + time.sleep(1) + continue + else: + raise + + def _parse_deepseek_response(self, response): + """Parse DeepSeek response - enhanced version""" + try: + content = response['choices'][0]['message']['content'].strip() + logger.debug(f"DeepSeek raw response: {content}") + + # Remove possible code block markers + if content.startswith('```json'): + content = content[7:] + if content.endswith('```'): + content = content[:-3] + content = content.strip() + + # Try to parse JSON format + if '{' in content and '}' in content: + json_start = content.find('{') + json_end = content.rfind('}') + 1 + json_str = content[json_start:json_end] + + try: + parsed = json.loads(json_str) + result = { + 'action': parsed.get('action', 'HOLD').upper(), + 'confidence': parsed.get('confidence', 'MEDIUM').upper(), + 'reasoning': parsed.get('reasoning', ''), + 'raw_response': content + } + + # Validate action and confidence legality + if result['action'] not in ['BUY', 'SELL', 'HOLD']: + logger.warning(f"AI returned illegal action: {result['action']}, using HOLD") + result['action'] = 'HOLD' + + if result['confidence'] not in ['HIGH', 'MEDIUM', 'LOW']: + logger.warning(f"AI returned illegal confidence: {result['confidence']}, using MEDIUM") + result['confidence'] = 'MEDIUM' + + logger.info(f"AI parsing successful: {result['action']} (confidence: {result['confidence']})") + return result + + except json.JSONDecodeError as e: + logger.warning(f"JSON parsing failed: {e}, trying text analysis") + return self._parse_text_response(content) + else: + # Text analysis + return self._parse_text_response(content) + + except Exception as e: + logger.error(f"Error parsing DeepSeek response: {e}") + # Return default values to avoid system crash + return { + 'action': 'HOLD', + 'confidence': 'MEDIUM', + 'reasoning': f'Parsing error: {str(e)}', + 'raw_response': str(response) + } + + def _parse_text_response(self, text): + """Parse text response - enhanced version""" + text_lower = text.lower() + + # More precise action recognition + action = 'HOLD' + if any(word in text_lower for word in ['买入', '做多', 'buy', 'long', '上涨', '看涨']): + action = 'BUY' + elif any(word in text_lower for word in ['卖出', '做空', 'sell', 'short', '下跌', '看跌']): + action = 'SELL' + + # More precise confidence level recognition + confidence = 'MEDIUM' + if any(word in text_lower for word in ['强烈', '高信心', 'high', '非常', '强烈建议']): + confidence = 'HIGH' + elif any(word in text_lower for word in ['低', '低信心', 'low', '轻微', '谨慎']): + confidence = 'LOW' + + logger.info(f"Text parsing result: {action} (confidence: {confidence})") + + return { + 'action': action, + 'confidence': confidence, + 'reasoning': text, + 'raw_response': text + } + +class TechnicalAnalyzer: + """Technical Analyzer""" + + @staticmethod + def calculate_indicators(df): + """Calculate technical indicators (optimized version)""" + if df is None or len(df) < 20: # Lower minimum data requirement + logger.warning("Insufficient data, unable to calculate technical indicators") + return {} + + try: + indicators = {} + + # KDJ indicator + k, d, j = TechnicalAnalyzer.calculate_kdj(df) + if k is not None: + indicators['kdj'] = (k, d, j) + + # RSI indicator (multiple time periods) + rsi_14 = TechnicalAnalyzer.calculate_rsi(df['close'], 14) + rsi_7 = TechnicalAnalyzer.calculate_rsi(df['close'], 7) + rsi_21 = TechnicalAnalyzer.calculate_rsi(df['close'], 21) + if rsi_14 is not None: + indicators['rsi'] = rsi_14 + indicators['rsi_7'] = rsi_7 + indicators['rsi_21'] = rsi_21 + + # ATR indicator + atr = TechnicalAnalyzer.calculate_atr(df) + if atr is not None: + indicators['atr'] = atr + + # MACD indicator + macd_line, signal_line, histogram = TechnicalAnalyzer.calculate_macd(df['close']) + if macd_line is not None: + indicators['macd'] = (macd_line, signal_line, histogram) + + # Bollinger Bands (multiple standard deviations) + upper1, middle1, lower1 = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 1) + upper2, middle2, lower2 = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 2) + if upper1 is not None: + indicators['bollinger_1std'] = (upper1, middle1, lower1) + indicators['bollinger_2std'] = (upper2, middle2, lower2) + + # Moving Averages (multiple periods) + sma_20 = TechnicalAnalyzer.calculate_sma(df['close'], 20) + sma_50 = TechnicalAnalyzer.calculate_sma(df['close'], 50) + sma_100 = TechnicalAnalyzer.calculate_sma(df['close'], 100) + ema_12 = TechnicalAnalyzer.calculate_ema(df['close'], 12) + ema_26 = TechnicalAnalyzer.calculate_ema(df['close'], 26) + if sma_20 is not None: + indicators['sma_20'] = sma_20 + indicators['sma_50'] = sma_50 + indicators['sma_100'] = sma_100 + indicators['ema_12'] = ema_12 + indicators['ema_26'] = ema_26 + + # Stochastic Oscillator + k, d = TechnicalAnalyzer.calculate_stochastic(df) + if k is not None: + indicators['stochastic'] = (k, d) + + # Trend strength + indicators['trend_strength'] = TechnicalAnalyzer.calculate_trend_strength(df) + + # Volatility + indicators['volatility'] = TechnicalAnalyzer.calculate_volatility(df) + + # Current price + indicators['current_price'] = df['close'].iloc[-1] + + return indicators + + except Exception as e: + logger.error(f"Error calculating technical indicators: {e}") + return {} + + @staticmethod + def calculate_ema(prices, period): + """Calculate Exponential Moving Average""" + try: + return prices.ewm(span=period, adjust=False).mean() + except Exception as e: + logger.error(f"Error calculating EMA: {e}") + return None + + @staticmethod + def calculate_trend_strength(df, period=20): + """Calculate trend strength""" + try: + if len(df) < period: + return 0 + + # Use linear regression to calculate trend strength + x = np.arange(len(df)) + y = df['close'].values + + # Linear regression + slope, intercept = np.polyfit(x[-period:], y[-period:], 1) + + # Calculate R² value as trend strength + y_pred = slope * x[-period:] + intercept + ss_res = np.sum((y[-period:] - y_pred) ** 2) + ss_tot = np.sum((y[-period:] - np.mean(y[-period:])) ** 2) + r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0 + + return abs(r_squared) # Take absolute value, trend strength doesn't distinguish positive/negative + + except Exception as e: + logger.error(f"Error calculating trend strength: {e}") + return 0 + + @staticmethod + def detect_rsi_divergence(df, rsi_period=14): + """Detect RSI bullish divergence""" + try: + if len(df) < 30: # Need sufficient data + return False + + # Calculate RSI + rsi = TechnicalAnalyzer.calculate_rsi(df['close'], rsi_period) + if rsi is None: + return False + + # Use pandas methods to simplify calculation + # Find lowest points in recent 10 periods + recent_lows = df['low'].tail(10) + recent_rsi = rsi.tail(10) + + # Find price lowest point and RSI lowest point + min_price_idx = recent_lows.idxmin() + min_rsi_idx = recent_rsi.idxmin() + + # If price lowest point appears later than RSI lowest point, possible bullish divergence + if min_price_idx > min_rsi_idx: + # Check if price is making new lows while RSI is rising + price_trend = (recent_lows.iloc[-1] < recent_lows.iloc[-5]) + rsi_trend = (recent_rsi.iloc[-1] > recent_rsi.iloc[-5]) + + return price_trend and rsi_trend and recent_rsi.iloc[-1] < 40 + + return False + + except Exception as e: + logger.error(f"RSI divergence detection error: {e}") + return False + + @staticmethod + def detect_macd_divergence(df): + """Detect MACD bullish divergence""" + try: + if len(df) < 30: + return False + + # Calculate MACD, especially focus on histogram + _, _, histogram = TechnicalAnalyzer.calculate_macd(df['close']) + if histogram is None: + return False + + # Use recent data + recent_lows = df['low'].tail(10) + recent_hist = histogram.tail(10) + + # Find price lowest point and MACD histogram lowest point + min_price_idx = recent_lows.idxmin() + min_hist_idx = recent_hist.idxmin() + + # If price lowest point appears later than histogram lowest point, possible bullish divergence + if min_price_idx > min_hist_idx: + # Check price trend and histogram trend + price_trend = (recent_lows.iloc[-1] < recent_lows.iloc[-5]) + hist_trend = (recent_hist.iloc[-1] > recent_hist.iloc[-5]) + + # Histogram rising from negative area is stronger signal + hist_improving = recent_hist.iloc[-1] > recent_hist.iloc[-3] + + return price_trend and hist_trend and hist_improving + + return False + + except Exception as e: + logger.error(f"MACD divergence detection error: {e}") + return False + + @staticmethod + def volume_confirmation(df, period=5): + """Bottom volume confirmation""" + try: + if len(df) < period + 5: + return False + + current_volume = df['volume'].iloc[-1] + avg_volume = df['volume'].tail(period).mean() + + # Current volume significantly higher than average + volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1 + + # Price falling but volume increasing (possible bottom accumulation) + price_change = (df['close'].iloc[-1] - df['close'].iloc[-2]) / df['close'].iloc[-2] + + if volume_ratio > 1.5 and price_change < 0: + return True + + return False + except Exception as e: + logger.error(f"Volume confirmation error: {e}") + return False + + @staticmethod + def detect_hammer_pattern(df, lookback=5): + """Detect hammer reversal pattern""" + try: + if len(df) < lookback + 1: + return False + + latest = df.iloc[-1] + body_size = abs(latest['close'] - latest['open']) + total_range = latest['high'] - latest['low'] + + # Avoid division by zero + if total_range == 0: + return False + + # Hammer characteristics: lower shadow at least 2x body, very short upper shadow + lower_shadow = min(latest['open'], latest['close']) - latest['low'] + upper_shadow = latest['high'] - max(latest['open'], latest['close']) + + is_hammer = (lower_shadow >= 2 * body_size and + upper_shadow <= body_size * 0.5 and + body_size > 0) + + # Needs to be in downtrend + prev_trend = df['close'].iloc[-lookback] > df['close'].iloc[-1] + + return is_hammer and prev_trend + + except Exception as e: + logger.error(f"Hammer pattern detection error: {e}") + return False + + @staticmethod + def detect_reversal_patterns(df): + """Detect bottom reversal patterns""" + reversal_signals = [] + + try: + # RSI bullish divergence + if TechnicalAnalyzer.detect_rsi_divergence(df): + reversal_signals.append({ + 'type': 'RSI_DIVERGENCE_BULLISH', + 'action': 'BUY', + 'strength': 'MEDIUM', + 'description': 'RSI bullish divergence, momentum divergence bullish' + }) + + # MACD bullish divergence + if TechnicalAnalyzer.detect_macd_divergence(df): + reversal_signals.append({ + 'type': 'MACD_DIVERGENCE_BULLISH', + 'action': 'BUY', + 'strength': 'MEDIUM', + 'description': 'MACD histogram bullish divergence, momentum strengthening' + }) + + # Volume confirmation + if TechnicalAnalyzer.volume_confirmation(df): + reversal_signals.append({ + 'type': 'VOLUME_CONFIRMATION', + 'action': 'BUY', + 'strength': 'LOW', + 'description': 'Bottom volume increase, signs of capital entry' + }) + + # Add hammer pattern detection + if TechnicalAnalyzer.detect_hammer_pattern(df): + reversal_signals.append({ + 'type': 'HAMMER_PATTERN', + 'action': 'BUY', + 'strength': 'LOW', + 'description': 'Hammer pattern, short-term reversal signal' + }) + + except Exception as e: + logger.error(f"Reversal pattern detection error: {e}") + + return reversal_signals + + @staticmethod + def calculate_support_resistance(df, period=20): + """Calculate support resistance levels (fixed version)""" + try: + # Recent high resistance + resistance = df['high'].rolling(window=period).max().iloc[-1] + + # Recent low support + support = df['low'].rolling(window=period).min().iloc[-1] + + # Dynamic support resistance (based on Bollinger Bands) + # Fix: correctly receive three return values from Bollinger Bands + upper_bb, middle_bb, lower_bb = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 2) + + # Check if Bollinger Band calculation successful + if upper_bb is not None and lower_bb is not None: + resistance_bb = upper_bb.iloc[-1] if hasattr(upper_bb, 'iloc') else upper_bb + support_bb = lower_bb.iloc[-1] if hasattr(lower_bb, 'iloc') else lower_bb + else: + # If Bollinger Band calculation fails, use static values as backup + resistance_bb = resistance + support_bb = support + + current_price = df['close'].iloc[-1] + + return { + 'static_resistance': resistance, + 'static_support': support, + 'dynamic_resistance': resistance_bb, + 'dynamic_support': support_bb, + 'current_vs_resistance': (current_price - resistance) / resistance * 100 if resistance > 0 else 0, + 'current_vs_support': (current_price - support) / support * 100 if support > 0 else 0 + } + except Exception as e: + logger.error(f"Error calculating support resistance: {e}") + # Return default values to avoid subsequent errors + current_price = df['close'].iloc[-1] if len(df) > 0 else 0 + return { + 'static_resistance': current_price * 1.1 if current_price > 0 else 0, + 'static_support': current_price * 0.9 if current_price > 0 else 0, + 'dynamic_resistance': current_price * 1.05 if current_price > 0 else 0, + 'dynamic_support': current_price * 0.95 if current_price > 0 else 0, + 'current_vs_resistance': 0, + 'current_vs_support': 0 + } + + @staticmethod + def calculate_volatility(df, period=20): + """Calculate volatility""" + try: + returns = df['close'].pct_change().dropna() + volatility = returns.rolling(window=period).std() * np.sqrt(365) # Annualized volatility + return volatility.iloc[-1] if len(volatility) > 0 else 0 + except Exception as e: + logger.error(f"Error calculating volatility: {e}") + return 0 + + @staticmethod + def generate_weighted_signals(df): + """Generate weighted technical signals""" + signals = [] + weights = { + 'KDJ_GOLDEN_CROSS': 0.15, + 'MACD_GOLDEN_CROSS': 0.20, + 'MA_GOLDEN_CROSS': 0.25, + 'RSI_OVERSOLD': 0.15, + 'BOLLINGER_LOWER_TOUCH': 0.15, + 'STOCH_GOLDEN_CROSS': 0.10 + } + + # Calculate various indicator signals + technical_signals = TechnicalAnalyzer.generate_signals(df) + + # Calculate weighted score + total_score = 0 + signal_count = 0 + + for signal in technical_signals: + weight = weights.get(signal['type'], 0.05) + strength_multiplier = 1.0 if signal['strength'] == 'STRONG' else 0.7 + total_score += weight * strength_multiplier + signal_count += 1 + + # Normalize score + normalized_score = min(1.0, total_score) + + # Determine action + if normalized_score > 0.6: + action = 'BUY' + elif normalized_score < 0.3: + action = 'SELL' + else: + action = 'HOLD' + + return { + 'score': normalized_score, + 'action': action, + 'signal_count': signal_count, + 'signals': technical_signals, + 'confidence': 'HIGH' if normalized_score > 0.7 or normalized_score < 0.2 else 'MEDIUM' + } + + @staticmethod + def calculate_kdj(df, n=9, m1=3, m2=3): + """Calculate KDJ indicator""" + try: + if len(df) < n: + return pd.Series([np.nan] * len(df)), pd.Series([np.nan] * len(df)), pd.Series([np.nan] * len(df)) + + low_list = df['low'].rolling(window=n, min_periods=1).min() + high_list = df['high'].rolling(window=n, min_periods=1).max() + + # Avoid division by zero error + denominator = high_list - low_list + denominator = denominator.replace(0, np.nan) + + rsv = ((df['close'] - low_list) / denominator) * 100 + rsv = rsv.fillna(50) + + k_series = rsv.ewm(span=m1-1, adjust=False).mean() + d_series = k_series.ewm(span=m2-1, adjust=False).mean() + j_series = 3 * k_series - 2 * d_series + + return k_series, d_series, j_series + + except Exception as e: + logger.error(f"Error calculating KDJ: {e}") + return None, None, None + + @staticmethod + def calculate_rsi(prices, period=14): + """Calculate RSI""" + try: + if len(prices) < period: + return pd.Series([np.nan] * len(prices)) + + delta = prices.diff() + gain = (delta.where(delta > 0, 0)).rolling(window=period, min_periods=1).mean() + loss = (-delta.where(delta < 0, 0)).rolling(window=period, min_periods=1).mean() + + # Avoid division by zero error + rs = gain / loss.replace(0, np.nan) + rsi = 100 - (100 / (1 + rs)) + return rsi.fillna(50) + + except Exception as e: + logger.error(f"Error calculating RSI: {e}") + return None + + @staticmethod + def calculate_atr(df, period=14): + """Calculate ATR""" + try: + if len(df) < period: + return pd.Series([np.nan] * len(df)) + + high_low = df['high'] - df['low'] + high_close = np.abs(df['high'] - df['close'].shift()) + low_close = np.abs(df['low'] - df['close'].shift()) + + true_range = np.maximum(high_low, np.maximum(high_close, low_close)) + atr = true_range.rolling(window=period, min_periods=1).mean() + return atr + + except Exception as e: + logger.error(f"Error calculating ATR: {e}") + return None + + @staticmethod + def calculate_macd(prices, fast_period=12, slow_period=26, signal_period=9): + """Calculate MACD""" + try: + ema_fast = prices.ewm(span=fast_period, adjust=False).mean() + ema_slow = prices.ewm(span=slow_period, adjust=False).mean() + macd_line = ema_fast - ema_slow + signal_line = macd_line.ewm(span=signal_period, adjust=False).mean() + histogram = macd_line - signal_line + return macd_line, signal_line, histogram + except Exception as e: + logger.error(f"Error calculating MACD: {e}") + return None, None, None + + @staticmethod + def calculate_bollinger_bands(prices, period=20, std_dev=2): + """Calculate Bollinger Bands""" + try: + if len(prices) < period: + logger.warning(f"Data length {len(prices)} insufficient, unable to calculate {period}-period Bollinger Bands") + return None, None, None + + middle = prices.rolling(window=period).mean() + std = prices.rolling(window=period).std() + + # Handle NaN standard deviation + std = std.fillna(0) + + upper = middle + (std * std_dev) + lower = middle - (std * std_dev) + + return upper, middle, lower + except Exception as e: + logger.error(f"Error calculating Bollinger Bands: {e}") + return None, None, None + + @staticmethod + def calculate_sma(prices, period): + """Calculate Simple Moving Average""" + try: + return prices.rolling(window=period).mean() + except Exception as e: + logger.error(f"Error calculating SMA: {e}") + return None + + @staticmethod + def calculate_stochastic(df, k_period=14, d_period=3): + """Calculate Stochastic Oscillator""" + try: + low_min = df['low'].rolling(window=k_period).min() + high_max = df['high'].rolling(window=k_period).max() + k = 100 * ((df['close'] - low_min) / (high_max - low_min)) + d = k.rolling(window=d_period).mean() + return k, d + except Exception as e: + logger.error(f"Error calculating Stochastic: {e}") + return None, None + + @staticmethod + def generate_signals(df): + """Generate technical signals""" + signals = [] + + try: + # KDJ + k, d, j = TechnicalAnalyzer.calculate_kdj(df) + if k is not None and len(k) > 1: + latest_k, latest_d, latest_j = k.iloc[-1], d.iloc[-1], j.iloc[-1] + prev_k, prev_d, prev_j = k.iloc[-2], d.iloc[-2], j.iloc[-2] + + if prev_k <= prev_d and latest_k > latest_d: + signals.append({'type': 'KDJ_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif prev_k >= prev_d and latest_k < latest_d: + signals.append({'type': 'KDJ_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) + + if latest_j < 20: + signals.append({'type': 'KDJ_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif latest_j > 80: + signals.append({'type': 'KDJ_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # RSI + rsi = TechnicalAnalyzer.calculate_rsi(df['close']) + if rsi is not None and len(rsi) > 0: + latest_rsi = rsi.iloc[-1] + + if latest_rsi < 30: + signals.append({'type': 'RSI_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif latest_rsi > 70: + signals.append({'type': 'RSI_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # MACD + macd_line, signal_line, _ = TechnicalAnalyzer.calculate_macd(df['close']) + if macd_line is not None and len(macd_line) > 1: + latest_macd = macd_line.iloc[-1] + latest_signal = signal_line.iloc[-1] + prev_macd = macd_line.iloc[-2] + prev_signal = signal_line.iloc[-2] + + if prev_macd <= prev_signal and latest_macd > latest_signal: + signals.append({'type': 'MACD_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif prev_macd >= prev_signal and latest_macd < latest_signal: + signals.append({'type': 'MACD_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # Bollinger Bands + upper, middle, lower = TechnicalAnalyzer.calculate_bollinger_bands(df['close']) + if upper is not None: + latest_close = df['close'].iloc[-1] + if latest_close <= lower.iloc[-1]: + signals.append({'type': 'BOLlinger_LOWER_TOUCH', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif latest_close >= upper.iloc[-1]: + signals.append({'type': 'BOLlinger_UPPER_TOUCH', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # Moving Averages + sma_short = TechnicalAnalyzer.calculate_sma(df['close'], 50) + sma_long = TechnicalAnalyzer.calculate_sma(df['close'], 200) + if sma_short is not None and sma_long is not None and len(sma_short) > 1: + latest_short = sma_short.iloc[-1] + latest_long = sma_long.iloc[-1] + prev_short = sma_short.iloc[-2] + prev_long = sma_long.iloc[-2] + + if prev_short <= prev_long and latest_short > latest_long: + signals.append({'type': 'MA_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif prev_short >= prev_long and latest_short < latest_long: + signals.append({'type': 'MA_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # Stochastic + k, d = TechnicalAnalyzer.calculate_stochastic(df) + if k is not None and len(k) > 1: + latest_k = k.iloc[-1] + latest_d = d.iloc[-1] + prev_k = k.iloc[-2] + prev_d = d.iloc[-2] + + if latest_k < 20: + signals.append({'type': 'STOCH_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif latest_k > 80: + signals.append({'type': 'STOCH_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'}) + + if prev_k <= prev_d and latest_k > latest_d and latest_k < 80: + signals.append({'type': 'STOCH_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif prev_k >= prev_d and latest_k < latest_d and latest_k > 20: + signals.append({'type': 'STOCH_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) + + except Exception as e: + logger.error(f"Error generating technical signals: {e}") + + return signals + +class RiskManager: + """Risk Control Manager (Dynamic Position Management Version)""" + + def __init__(self, api): + self.api = api + self.max_total_position = float(os.getenv('MAX_TOTAL_POSITION', '1')) # Maximum total position 100% + self.max_single_position = float(os.getenv('MAX_SINGLE_POSITION', '0.3')) # Single currency maximum position 30% + self.stop_loss = float(os.getenv('STOP_LOSS', '0.05')) # Stop-loss 5% + self.take_profit = float(os.getenv('TAKE_PROFIT', '0.15')) # Take-profit 15% + self.max_daily_trades = int(os.getenv('MAX_DAILY_TRADES', '10')) # Maximum daily trades + self.dust_threshold_value = 1.0 # Dust position value threshold (USDT) + self.low_position_ratio = 0.05 # Low position ratio threshold (5%) + + self.trailing_stop_percent = float(os.getenv('TRAILING_STOP_PERCENT', '0.03')) # Trailing stop percentage + self.monitor_interval = int(os.getenv('MONITOR_INTERVAL', '300')) # Monitoring interval seconds + + # Dynamic position parameters + self.volatility_adjustment = True + self.trend_adjustment = True + self.market_sentiment_adjustment = True + + # Trade records + self.daily_trade_count = {} + self.db = DatabaseManager() + + def calculate_total_assets(self): + """Calculate total assets (in USDT)""" + try: + # Get all currency balances + balances = self.api.get_currency_balances() + if not balances: + logger.error("Unable to get currency balances") + return 0.0, 0.0, {} + + total_usdt_value = 0.0 + position_details = {} + + # Calculate USDT balance (including available and frozen) + usdt_balance = balances.get('USDT', {}) + usdt_avail = usdt_balance.get('amount', 0.0) + usdt_frozen = usdt_balance.get('frozen', 0.0) + total_usdt = usdt_avail + usdt_frozen + total_usdt_value += total_usdt + + # Calculate value of other currencies + for currency, balance_info in balances.items(): + if currency != 'USDT': + avail = balance_info.get('amount', 0.0) + frozen = balance_info.get('frozen', 0.0) + total_amount = avail + frozen + if total_amount > 0: + symbol = f"{currency}-USDT" + current_price = self.api.get_current_price(symbol) + if current_price: + value = total_amount * current_price + total_usdt_value += value + position_details[symbol] = { + 'amount': total_amount, # Total quantity (available + frozen) + 'value': value, + 'current_price': current_price + } + else: + logger.warning(f"Unable to get {currency} price, skipping value calculation") + + logger.debug(f"Total asset calculation: USDT={total_usdt:.2f}(available{usdt_avail:.2f}+frozen{usdt_frozen:.2f}), position value={total_usdt_value - total_usdt:.2f}, total={total_usdt_value:.2f}") + + return total_usdt_value, usdt_avail, position_details + + except Exception as e: + logger.error(f"Error calculating total assets: {e}") + return 0.0, 0.0, {} + + def get_position_ratio(self, symbol): + """Get specified currency's position ratio""" + try: + total_assets, usdt_balance, positions = self.calculate_total_assets() + if total_assets == 0: + return 0.0 + + position_value = positions.get(symbol, {}).get('value', 0.0) + return position_value / total_assets + + except Exception as e: + logger.error(f"Error calculating position ratio: {e}") + return 0.0 + + def get_available_usdt_ratio(self): + """Get available USDT ratio""" + try: + total_assets, usdt_balance, _ = self.calculate_total_assets() + if total_assets == 0: + return 0.0 + + return usdt_balance / total_assets + + except Exception as e: + logger.error(f"Error calculating USDT ratio: {e}") + return 0.0 + + def get_position_size(self, symbol, confidence, current_price): + """Calculate position size based on confidence level and USDT availability""" + try: + total_assets, usdt_balance, positions = self.calculate_total_assets() + + if total_assets == 0: + logger.warning("Total assets is 0, unable to calculate position") + return 0.0 + + # Get current position value + current_pos_value = positions.get(symbol, {}).get('value', 0.0) + + # Calculate USDT available ratio + usdt_ratio = usdt_balance / total_assets + + # Adjust maximum position based on USDT availability + adjusted_max_single = self.max_single_position * min(1.0, usdt_ratio * 3) # More USDT allows larger positions + + # Calculate maximum additional position + max_single_add = max(0, total_assets * adjusted_max_single - current_pos_value) + max_total_add = max(0, total_assets * self.max_total_position - (total_assets - usdt_balance)) + + max_add = min(max_single_add, max_total_add, usdt_balance) + + # Adjust based on confidence level + multiplier = { + 'HIGH': 0.8, # High confidence uses 80% of available amount + 'MEDIUM': 0.5, # Medium confidence uses 50% + 'LOW': 0.2 # Low confidence uses 20% + }.get(confidence, 0.2) + + position_size = max_add * multiplier + + # Ensure doesn't exceed available USDT balance + position_size = min(position_size, usdt_balance) + + logger.info(f"Position calculation: {symbol}, total assets=${total_assets:.2f}, USDT=${usdt_balance:.2f}, " + f"current position=${current_pos_value:.2f}, suggested position=${position_size:.2f}") + + return position_size + + except Exception as e: + logger.error(f"Error calculating position size: {e}") + return 0.0 + + def get_dynamic_position_size(self, symbol, confidence, technical_indicators, current_price): + """Dynamic position calculation""" + try: + # Base position calculation + base_size = self.get_position_size(symbol, confidence, current_price) + if base_size == 0: + return 0.0 + + # Get market state parameters + volatility = technical_indicators.get('volatility', 0) + trend_strength = technical_indicators.get('trend_strength', 0) + market_state = self.assess_market_state(technical_indicators) + + # Volatility adjustment (high volatility reduces position) + volatility_factor = self._calculate_volatility_factor(volatility) + + # Trend strength adjustment (strong trend increases position) + trend_factor = self._calculate_trend_factor(trend_strength) + + # Market state adjustment + market_factor = self._calculate_market_factor(market_state) + + # Confidence level adjustment + confidence_factor = self._calculate_confidence_factor(confidence) + + # Calculate dynamic position + dynamic_size = base_size * volatility_factor * trend_factor * market_factor * confidence_factor + + logger.info(f"Dynamic position calculation: {symbol}, base={base_size:.2f}, " + f"volatility factor={volatility_factor:.2f}, trend factor={trend_factor:.2f}, " + f"market factor={market_factor:.2f}, confidence factor={confidence_factor:.2f}, " + f"final={dynamic_size:.2f}") + + return dynamic_size + + except Exception as e: + logger.error(f"Dynamic position calculation error: {e}") + return self.get_position_size(symbol, confidence, current_price) + + def _calculate_volatility_factor(self, volatility): + """Calculate volatility adjustment factor""" + if not self.volatility_adjustment: + return 1.0 + + # Annualized volatility conversion and adjustment + if volatility > 0.8: # 80%+ annualized volatility + return 0.3 + elif volatility > 0.6: # 60-80% + return 0.5 + elif volatility > 0.4: # 40-60% + return 0.7 + elif volatility > 0.2: # 20-40% + return 0.9 + else: # <20% + return 1.0 + + def _calculate_trend_factor(self, trend_strength): + """Calculate trend strength adjustment factor""" + if not self.trend_adjustment: + return 1.0 + + if trend_strength > 0.7: # Strong trend + return 1.3 + elif trend_strength > 0.4: # Medium trend + return 1.1 + elif trend_strength > 0.2: # Weak trend + return 1.0 + else: # No trend + return 0.8 + + def _calculate_market_factor(self, market_state): + """Calculate market state adjustment factor""" + if not self.market_sentiment_adjustment: + return 1.0 + + factors = { + 'STRONG_BULL': 1.2, + 'BULL': 1.1, + 'NEUTRAL': 1.0, + 'BEAR': 0.7, + 'STRONG_BEAR': 0.5 + } + return factors.get(market_state, 1.0) + + def _calculate_confidence_factor(self, confidence): + """Calculate confidence level adjustment factor""" + factors = { + 'HIGH': 1.0, + 'MEDIUM': 0.7, + 'LOW': 0.4 + } + return factors.get(confidence, 0.5) + + def assess_market_state(self, technical_indicators): + """Assess market state""" + try: + # Get key indicators + rsi = technical_indicators.get('rsi', 50) + if hasattr(rsi, '__len__'): + rsi = rsi.iloc[-1] + + macd_line, signal_line, _ = technical_indicators.get('macd', (0, 0, 0)) + if hasattr(macd_line, '__len__'): + macd_line, signal_line = macd_line.iloc[-1], signal_line.iloc[-1] + + sma_20 = technical_indicators.get('sma_20', 0) + sma_50 = technical_indicators.get('sma_50', 0) + current_price = technical_indicators.get('current_price', 0) + + if hasattr(sma_20, '__len__'): + sma_20, sma_50 = sma_20.iloc[-1], sma_50.iloc[-1] + + # Calculate bullish signal score + bull_signals = 0 + total_signals = 0 + + # RSI signal + if rsi > 50: bull_signals += 1 + total_signals += 1 + + # MACD signal + if macd_line > signal_line: bull_signals += 1 + total_signals += 1 + + # Moving average signal + if current_price > sma_20: bull_signals += 1 + if current_price > sma_50: bull_signals += 1 + total_signals += 2 + + # Determine market state + bull_ratio = bull_signals / total_signals + + if bull_ratio >= 0.8: + return 'STRONG_BULL' + elif bull_ratio >= 0.6: + return 'BULL' + elif bull_ratio >= 0.4: + return 'NEUTRAL' + elif bull_ratio >= 0.2: + return 'BEAR' + else: + return 'STRONG_BEAR' + + except Exception as e: + logger.error(f"Error assessing market state: {e}") + return 'NEUTRAL' + + def is_dust_position(self, symbol, current_price, base_amount): + """Check if dust position""" + hold_value = base_amount * current_price if current_price else 0 + return hold_value < self.dust_threshold_value + + def is_low_position_ratio(self, symbol, total_assets, current_price, base_amount): + """Check if position ratio is low""" + hold_value = base_amount * current_price if current_price else 0 + return (hold_value / total_assets) < self.low_position_ratio if total_assets > 0 else False + + def can_trade(self, symbol, amount, current_price): + """Check if can trade""" + try: + # Check daily trade count + today = datetime.now().date() + if symbol not in self.daily_trade_count: + self.daily_trade_count[symbol] = {'date': today, 'count': 0} + + if self.daily_trade_count[symbol]['date'] != today: + self.daily_trade_count[symbol] = {'date': today, 'count': 0} + + if self.daily_trade_count[symbol]['count'] >= self.max_daily_trades: + logger.warning(f"{symbol} daily trade count reached {self.max_daily_trades} limit") + return False + + return True + + except Exception as e: + logger.error(f"Error checking trade conditions: {e}") + return False + + def increment_trade_count(self, symbol): + """Increment trade count""" + try: + today = datetime.now().date() + if symbol in self.daily_trade_count and self.daily_trade_count[symbol]['date'] == today: + self.daily_trade_count[symbol]['count'] += 1 + except Exception as e: + logger.error(f"Error incrementing trade count: {e}") + +class TradingStrategy: + """Trading Strategy (Comprehensive Optimized Version)""" + + def __init__(self, symbol, config, api, risk_manager, deepseek): + self.symbol = symbol + self.config = config + self.api = api + self.risk_manager = risk_manager + self.deepseek = deepseek + self.technical_analyzer = TechnicalAnalyzer() + self.db = DatabaseManager() # Use default configuration + + # Position status + self.base_amount = 0.0 + self.entry_price = 0.0 + + # Dynamic stop-loss monitoring - using environment variable configured interval + self.monitor_interval = 300 # Default 5 minutes + if hasattr(risk_manager, 'monitor_interval'): + self.monitor_interval = risk_manager.monitor_interval + self.monitor_running = False + self.monitor_thread = None + + # Running status control + self.strategy_running = False + + # Load position status + self.load_position() + + # Start dynamic stop-loss monitoring + self.start_dynamic_stop_monitor() + + def load_position(self): + """Load position status from database""" + try: + position_data = self.db.load_position(self.symbol) + if position_data: + self.base_amount = position_data['base_amount'] + self.entry_price = position_data['entry_price'] + logger.info(f"Loaded {self.symbol} position: currency quantity={self.base_amount:.10f}, entry price=${self.entry_price:.2f}") + else: + # If no database record, sync with exchange position + self.sync_with_exchange() + except Exception as e: + logger.error(f"Error loading position status: {e}") + self.sync_with_exchange() + + def save_position(self): + """Save position status to database""" + try: + if self.base_amount > 0: + self.db.save_position(self.symbol, self.base_amount, self.entry_price) + else: + # If position is 0, delete record + self.db.delete_position(self.symbol) + # Also delete dynamic stop record + self.db.delete_dynamic_stop(self.symbol) + except Exception as e: + logger.error(f"Error saving position status: {e}") + + def sync_with_exchange(self): + """Sync with exchange position""" + try: + logger.debug(f"Starting {self.symbol} position status sync") + + # Get all currency balances + balances = self.api.get_currency_balances() + if not balances: + logger.warning("Unable to get currency balances") + return False + + # Extract base currency + base_currency = self.symbol.split('-')[0] + + # Update local balance + if base_currency in balances: + new_base_amount = balances[base_currency].get('amount', 0.0) + + # If position quantity changes, print log + if abs(new_base_amount - self.base_amount) > 1e-10: + logger.info(f"Synced {self.symbol} latest balance: {base_currency}={new_base_amount:.10f}") + + # If position decreases but entry price not updated, keep original entry price (for calculating profit/loss) + if new_base_amount < self.base_amount and self.entry_price > 0: + # Position decreased, reset entry price to conservative estimate (current price) + if new_base_amount <= 0: + self.entry_price = 0 + logger.info(f"{self.symbol} position cleared, reset entry price to 0") + else: + current_price = self.api.get_current_price(self.symbol) + if current_price: + self.entry_price = current_price + logger.info(f"{self.symbol} partial sell, reset entry price to current price: ${current_price:.2f}") + else: + logger.warning(f"{self.symbol} partial sell but unable to get current price, keep original entry price") + + # If from no position to having position, and no entry price record + elif self.base_amount == 0 and new_base_amount > 0 and self.entry_price == 0: + # Try to load historical entry price from database + position_data = self.db.load_position(self.symbol) + if position_data and position_data['entry_price'] > 0: + self.entry_price = position_data['entry_price'] + logger.info(f"Restored entry price from database: ${self.entry_price:.2f}") + else: + # Get current price as reference (but note this is reference price) + current_price = self.api.get_current_price(self.symbol) + if current_price: + self.entry_price = current_price + logger.info(f"Set reference entry price: ${current_price:.2f} (Note: This is reference price, actual entry price may differ)") + + self.base_amount = new_base_amount + + else: + self.base_amount = 0.0 + # Don't clear entry price, keep for historical record analysis + logger.info(f"Synced {self.symbol}: No position") + + # Save position status + self.save_position() + return True + + except Exception as e: + logger.error(f"Error syncing exchange position: {e}") + return False + + def get_actual_position_from_exchange(self): + """Get actual position data from exchange""" + try: + balances = self.api.get_currency_balances() + if not balances: + return 0.0 + + base_currency = self.symbol.split('-')[0] + return balances.get(base_currency, {}).get('amount', 0.0) + + except Exception as e: + logger.error(f"Error getting exchange position: {e}") + return 0.0 + + def analyze_market(self): + """Comprehensive market analysis (optimized version)""" + try: + # Get market data + df = self.api.get_market_data(self.symbol, self.config['timeframe']) + if df is None or len(df) < 20: + logger.error(f"Failed to get {self.symbol} market data") + return None + + # Calculate technical indicators + technical_indicators = self.technical_analyzer.calculate_indicators(df) + if not technical_indicators: + logger.warning(f"Failed to calculate {self.symbol} technical indicators") + return None + + # Weighted technical signals + weighted_signals = self.technical_analyzer.generate_weighted_signals(df) + + # DeepSeek AI analysis + deepseek_analysis = self.deepseek.analyze_market(self.symbol, df, technical_indicators) + + # Technical signals + technical_signals = self.technical_analyzer.generate_signals(df) + + return { + 'deepseek': deepseek_analysis, + 'technical_signals': technical_signals, + 'weighted_signals': weighted_signals, + 'technical_indicators': technical_indicators, + 'current_price': df['close'].iloc[-1], + 'market_data': df + } + + except Exception as e: + logger.error(f"{self.symbol} market analysis error: {e}") + return None + + def make_enhanced_decision(self, analysis): + """Enhanced trading decision""" + if analysis is None: + return {'action': 'HOLD', 'confidence': 'LOW', 'reason': 'Insufficient analysis data'} + + deepseek_signal = analysis['deepseek'] + weighted_signals = analysis['weighted_signals'] + technical_indicators = analysis['technical_indicators'] + market_data = analysis['market_data'] + + # Build decision matrix + decision_matrix = self._build_decision_matrix(deepseek_signal, weighted_signals, technical_indicators, market_data) + + return self._finalize_decision(decision_matrix) + + def _build_decision_matrix(self, ai_signal, weighted_signal, technical_indicators, market_data): + """Build decision matrix""" + matrix = { + 'ai_weight': 0.6, # AI analysis weight + 'technical_weight': 0.2, # Traditional technical indicator weight + 'reversal_weight': 0.05, # Reversal signal weight + 'support_resistance_weight': 0.1, # Support resistance weight + 'market_weight': 0.05 # Market state weight + } + + # AI signal scoring + ai_score = self._score_ai_signal(ai_signal) if ai_signal else 0.5 + + # Technical signal scoring + tech_score = self._score_technical_signal(weighted_signal) + + # Market state scoring + market_score = self._score_market_condition(technical_indicators) + + # Reversal signal scoring + reversal_signals = TechnicalAnalyzer.detect_reversal_patterns(market_data) + reversal_score = 0.5 + if reversal_signals: + # Score based on reversal signal quantity and strength + strong_signals = [s for s in reversal_signals if s['strength'] == 'STRONG'] + reversal_score = 0.5 + (len(strong_signals) * 0.1) + (len(reversal_signals) * 0.05) + reversal_score = min(0.9, reversal_score) + + # Support resistance scoring + sr_levels = TechnicalAnalyzer.calculate_support_resistance(market_data) + sr_score = 0.5 + if sr_levels: + # Near support level adds points, near resistance level subtracts points + if sr_levels['current_vs_support'] > -2: # Very close to support + sr_score = 0.7 + elif sr_levels['current_vs_resistance'] < 2: # Very close to resistance + sr_score = 0.3 + + # Comprehensive scoring + total_score = ( + ai_score * matrix['ai_weight'] + + tech_score * matrix['technical_weight'] + + reversal_score * matrix['reversal_weight'] + + sr_score * matrix['support_resistance_weight'] + + market_score * matrix['market_weight'] + ) + logger.info(f"[{self.symbol}] Decision matrix details: " + f"AI score={ai_score:.3f}, technical score={tech_score:.3f}, " + f"market score={market_score:.3f}, reversal score={reversal_score:.3f}, " + f"support resistance score={sr_score:.3f}, total score={total_score:.3f}") + + # Determine action and confidence + if total_score > 0.6: + action = 'BUY' + confidence = 'HIGH' if total_score > 0.75 else 'MEDIUM' + elif total_score < 0.4: + action = 'SELL' + confidence = 'HIGH' if total_score < 0.25 else 'MEDIUM' + else: + action = 'HOLD' + confidence = 'MEDIUM' if abs(total_score - 0.5) > 0.1 else 'LOW' + + return { + 'total_score': total_score, + 'action': action, + 'confidence': confidence, + 'components': { + 'ai_score': ai_score, + 'tech_score': tech_score, + 'market_score': market_score, + 'reversal_score': reversal_score, + 'sr_score': sr_score + } + } + + def _score_ai_signal(self, ai_signal): + """AI signal scoring""" + if not ai_signal: + logger.warning("AI signal is empty, using neutral score") + return 0.5 + + action = ai_signal.get('action', 'HOLD') + confidence = ai_signal.get('confidence', 'MEDIUM') + + # Correction: Redesign scoring logic to ensure score has differentiation + if action == 'BUY': + base_score = 0.7 # Buy base score + elif action == 'SELL': + base_score = 0.3 # Sell base score + else: # HOLD + base_score = 0.5 # Hold neutral score + + # Confidence adjustment - ensure final score is within reasonable range + confidence_adjustments = { + 'HIGH': 0.2, # High confidence +0.2 + 'MEDIUM': 0.0, # Medium confidence unchanged + 'LOW': -0.2 # Low confidence -0.2 + } + + adjustment = confidence_adjustments.get(confidence, 0.0) + final_score = base_score + adjustment + + # Ensure within 0-1 range + final_score = max(0.1, min(0.9, final_score)) + + logger.debug(f"AI signal scoring: action={action}, confidence={confidence}, " + f"base={base_score}, adjustment={adjustment}, final={final_score}") + + return final_score + + def _score_technical_signal(self, weighted_signal): + """Technical signal scoring""" + if not weighted_signal: + logger.warning("Technical signal is empty, using neutral score") + return 0.5 + + action = weighted_signal.get('action', 'HOLD') + score = weighted_signal.get('score', 0.5) + signal_count = weighted_signal.get('signal_count', 0) + + # Adjust based on signal strength and quantity + if action == 'BUY': + # Buy signal: 0.5-1.0 range + if score > 0.7 and signal_count >= 3: + final_score = 0.8 + elif score > 0.6: + final_score = 0.7 + else: + final_score = 0.6 + elif action == 'SELL': + # Sell signal: 0.0-0.5 range + if score < 0.3 and signal_count >= 3: + final_score = 0.2 + elif score < 0.4: + final_score = 0.3 + else: + final_score = 0.4 + else: # HOLD + final_score = 0.5 + + logger.debug(f"Technical signal scoring: action={action}, score={score}, " + f"signal_count={signal_count}, final={final_score}") + + return final_score + + def _score_market_condition(self, technical_indicators): + """Market state scoring""" + try: + # Assess market state based on multiple indicators + rsi = technical_indicators.get('rsi', 50) + if hasattr(rsi, '__len__'): + rsi = rsi.iloc[-1] + + trend_strength = technical_indicators.get('trend_strength', 0) + volatility = technical_indicators.get('volatility', 0) + + # RSI scoring (30-70 is neutral) + rsi_score = 0.5 + if rsi < 30: # Oversold + rsi_score = 0.8 + elif rsi > 70: # Overbought + rsi_score = 0.2 + + # Trend strength scoring + trend_score = 0.5 + (trend_strength * 0.3) # Strong trend favors buying + + # Volatility scoring (medium volatility is best) + vol_score = 0.7 if 0.3 < volatility < 0.6 else 0.5 + + # Comprehensive scoring + return (rsi_score + trend_score + vol_score) / 3 + + except Exception as e: + logger.error(f"Market state scoring error: {e}") + return 0.5 + + def _finalize_decision(self, decision_matrix): + """Final decision""" + action = decision_matrix['action'] + confidence = decision_matrix['confidence'] + total_score = decision_matrix['total_score'] + + reason = f"Comprehensive score: {total_score:.3f} (AI: {decision_matrix['components']['ai_score']:.3f}, " + reason += f"Technical: {decision_matrix['components']['tech_score']:.3f}, " + reason += f"Market: {decision_matrix['components']['market_score']:.3f}," + reason += f"Reversal: {decision_matrix['components']['reversal_score']:.3f}," + reason += f"Support Resistance: {decision_matrix['components']['sr_score']:.3f})," + + return { + 'action': action, + 'confidence': confidence, + 'reason': reason, + 'total_score': total_score, + 'source': 'Enhanced Decision Matrix' + } + + def execute_trade(self, decision, current_price): + """Execute trade""" + if decision['action'] == 'HOLD': + logger.info(f"[{self.symbol}] Decision is HOLD, not executing trade") + return + + # Sync exchange position + self.sync_with_exchange() + actual_position = self.base_amount # Use synced position + + logger.info(f"[{self.symbol}] Current position: {self.base_amount:.10f}") + + # Only get needed total asset information + total_assets, available_usdt, _ = self.risk_manager.calculate_total_assets() + + if decision['action'] == 'BUY': + # Get market analysis for dynamic position calculation + market_analysis = self.analyze_market() + if market_analysis is None: + logger.warning(f"[{self.symbol}] Unable to get market analysis, using base position") + buy_amount = self.risk_manager.get_position_size(self.symbol, decision['confidence'], current_price) + else: + # Use dynamic position calculation + buy_amount = self.risk_manager.get_dynamic_position_size( + self.symbol, decision['confidence'], market_analysis['technical_indicators'], current_price + ) + + if buy_amount == 0: + logger.warning(f"[{self.symbol}] Calculated position size is 0, skipping buy") + return + + # Get exchange minimum trade quantity for buy check + min_sz, _ = self.api.get_instrument_info(self.symbol) + if min_sz is None: + min_sz = self.api.get_default_min_size(self.symbol) + + # Calculate base currency quantity to buy + estimated_base_amount = buy_amount / current_price if current_price > 0 else 0 + + logger.info(f"[{self.symbol}] Pre-buy check: estimated quantity={estimated_base_amount:.10f}, " + f"minimum trade quantity={min_sz}, available USDT=${available_usdt:.2f}, " + f"buy amount=${buy_amount:.2f}") + + # Check if buy quantity meets exchange minimum requirement + if estimated_base_amount < min_sz: + logger.warning(f"[{self.symbol}] Estimated buy quantity {estimated_base_amount:.10f} less than minimum trade quantity {min_sz}, canceling buy") + return + + # Check if quote currency balance is sufficient + if available_usdt < buy_amount: + logger.warning(f"[{self.symbol}] Quote currency balance insufficient: need ${buy_amount:.2f}, current ${available_usdt:.2f}") + return + + logger.info(f"[{self.symbol}] Creating buy order: amount=${buy_amount:.2f}, price=${current_price:.2f}") + order_id = self.api.create_order( + symbol=self.symbol, + side='buy', + amount=buy_amount + ) + + if order_id: + # Wait for order completion + order_status = self.api.wait_for_order_completion(self.symbol, order_id) + if order_status is None: + logger.error(f"[{self.symbol}] Order not completed") + return + + # Get actual fill price and quantity + fill_price = order_status['avgPx'] + fill_amount = order_status['accFillSz'] + + # Update position status + self.base_amount += fill_amount + + # Update average entry price (weighted average) + if self.entry_price > 0: + # If adding position, calculate weighted average price + self.entry_price = (self.entry_price * (self.base_amount - fill_amount) + fill_price * fill_amount) / self.base_amount + logger.info(f"[{self.symbol}] Position addition operation, updated average entry price: ${self.entry_price:.2f}") + else: + self.entry_price = fill_price + + logger.info(f"[{self.symbol}] Buy successful: quantity={fill_amount:.10f}, price=${fill_price:.2f}") + logger.info(f"[{self.symbol}] Updated position: {self.symbol.split('-')[0]}={self.base_amount:.10f}") + + # Sync exchange position + self.sync_with_exchange() + + # Set dynamic stop-loss + trailing_percent = self.risk_manager.trailing_stop_percent + self.db.set_dynamic_stop(self.symbol, fill_price, trailing_percent=trailing_percent) + logger.info(f"[{self.symbol}] Set dynamic stop-loss: initial price=${fill_price:.2f}, trailing_percent={trailing_percent:.1%}") + + # Note: sync_with_exchange already saves position, no need to save again here + self.risk_manager.increment_trade_count(self.symbol) + else: + logger.error(f"[{self.symbol}] Buy order creation failed") + + elif decision['action'] == 'SELL': + if actual_position <= 0: + logger.info(f"[{self.symbol}] Exchange actual position is 0, ignoring sell signal") + # Update local status + self.base_amount = 0 + self.entry_price = 0 + self.save_position() + return + + # Prepare position information for AI analysis + position_info = self._prepare_position_info(actual_position, current_price, total_assets) + + # Get market analysis data (for technical indicators) + market_analysis = self.analyze_market() + + # Use DeepSeek to analyze sell proportion + try: + sell_proportion = self.deepseek.analyze_sell_proportion( + self.symbol, position_info, market_analysis, current_price + ) + except Exception as e: + logger.warning(f"[{self.symbol}] DeepSeek sell proportion analysis failed: {e}, using fallback decision") + sell_proportion = self.deepseek._get_fallback_sell_proportion(position_info, decision['confidence']) + + # Calculate sell quantity based on AI suggestion + sell_amount = actual_position * sell_proportion + + # Ensure sell quantity doesn't exceed actual position + sell_amount = min(sell_amount, actual_position) + + logger.info(f"[{self.symbol}] AI suggested sell proportion: {sell_proportion:.1%}, calculated sell quantity: {sell_amount:.10f}") + + # Get exchange minimum trade quantity for sell check + min_sz, _ = self.api.get_instrument_info(self.symbol) + if min_sz is None: + min_sz = self.api.get_default_min_size(self.symbol) + + # Check if sell quantity is less than exchange minimum trade quantity + if sell_amount < min_sz: + if sell_proportion < 1.0: # Partial sell + logger.info(f"[{self.symbol}] Sell quantity {sell_amount:.10f} less than minimum trade quantity {min_sz}, ignoring this partial sell") + return + else: # Full sell + logger.info(f"[{self.symbol}] Position quantity {sell_amount:.10f} less than minimum trade quantity {min_sz}, dust position, ignoring sell") + return + + # Record sell decision details + logger.info(f"[{self.symbol}] Final sell decision: proportion={sell_proportion:.1%}, quantity={sell_amount:.10f}, " + f"reason={decision.get('reason', 'AI decision')}") + + # Execute sell order + order_id = self.api.create_order( + symbol=self.symbol, + side='sell', + amount=sell_amount + ) + + if order_id: + # Wait for order completion + order_status = self.api.wait_for_order_completion(self.symbol, order_id) + if order_status is None: + logger.error(f"[{self.symbol}] Order not completed") + return + + # Get actual fill price and quantity + fill_price = order_status['avgPx'] + fill_amount = order_status['accFillSz'] + + # Calculate profit + profit = (fill_price - self.entry_price) * fill_amount + profit_percent = (fill_price / self.entry_price - 1) * 100 if self.entry_price > 0 else 0 + + # Sync exchange position + self.sync_with_exchange() + + logger.info(f"[{self.symbol}] Sell successful: quantity={fill_amount:.10f}, price=${fill_price:.2f}") + logger.info(f"[{self.symbol}] Profit: ${profit:.2f} ({profit_percent:+.2f}%)") + logger.info(f"[{self.symbol}] Updated position: {self.symbol.split('-')[0]}={self.base_amount:.10f}") + + # Save trade record + self.db.save_trade_record(self.symbol, 'sell', fill_amount, fill_price, order_id) + + # If fully sold, reset status + if self.base_amount <= 0: + self.entry_price = 0 + self.db.delete_dynamic_stop(self.symbol) + logger.info(f"[{self.symbol}] Position cleared, removed dynamic stop-loss") + else: + # After partial sell, update dynamic stop-loss + self.db.set_dynamic_stop(self.symbol, self.entry_price, trailing_percent=0.03, multiplier=2) + logger.info(f"[{self.symbol}] After partial sell, reset dynamic stop-loss") + + # Note: sync_with_exchange already saves position, no need to save again here + self.risk_manager.increment_trade_count(self.symbol) + else: + logger.error(f"[{self.symbol}] Sell order creation failed") + + def _prepare_position_info(self, actual_position, current_price, total_assets): + """Prepare position information for AI analysis""" + try: + # Calculate position value + position_value = actual_position * current_price + + # Calculate position ratio + position_ratio = position_value / total_assets if total_assets > 0 else 0 + + # Calculate profit ratio + profit_ratio = (current_price - self.entry_price) / self.entry_price if self.entry_price > 0 else 0 + + # Calculate distance to stop-loss/take-profit + stop_loss_price = self.entry_price * (1 - self.risk_manager.stop_loss) if self.entry_price > 0 else 0 + take_profit_price = self.entry_price * (1 + self.risk_manager.take_profit) if self.entry_price > 0 else 0 + + distance_to_stop_loss = (current_price - stop_loss_price) / current_price if current_price > 0 else 0 + distance_to_take_profit = (take_profit_price - current_price) / current_price if current_price > 0 else 0 + + # Get market volatility + market_data = self.api.get_market_data(self.symbol, '1H', limit=24) + if market_data is not None and len(market_data) > 0: + market_volatility = market_data['close'].pct_change().std() * 100 + else: + market_volatility = 0 + + # Get trend strength + market_analysis = self.analyze_market() + trend_strength = 0 + if market_analysis and 'technical_indicators' in market_analysis: + trend_strength = market_analysis['technical_indicators'].get('trend_strength', 0) + + position_info = { + 'base_amount': actual_position, + 'entry_price': self.entry_price, + 'current_price': current_price, + 'position_value': position_value, + 'position_ratio': position_ratio, + 'profit_ratio': profit_ratio, + 'distance_to_stop_loss': distance_to_stop_loss, + 'distance_to_take_profit': distance_to_take_profit, + 'market_volatility': market_volatility, + 'trend_strength': trend_strength, + 'total_assets': total_assets + } + + logger.debug(f"[{self.symbol}] Position analysis information: ratio={position_ratio:.2%}, profit={profit_ratio:+.2%}, " + f"distance to stop-loss={distance_to_stop_loss:.2%}, volatility={market_volatility:.2f}%") + + return position_info + + except Exception as e: + logger.error(f"[{self.symbol}] Error preparing position information: {e}") + # Return basic position information + return { + 'base_amount': actual_position, + 'entry_price': self.entry_price, + 'current_price': current_price, + 'position_value': actual_position * current_price, + 'position_ratio': 0, + 'profit_ratio': 0, + 'distance_to_stop_loss': 0, + 'distance_to_take_profit': 0, + 'market_volatility': 0, + 'trend_strength': 0, + 'total_assets': total_assets + } + + def start_dynamic_stop_monitor(self): + """Start dynamic stop-loss monitoring""" + if self.monitor_running and self.monitor_thread and self.monitor_thread.is_alive(): + logger.info(f"[{self.symbol}] Dynamic stop-loss monitoring already running") + return + + self.monitor_running = True + self.monitor_thread = threading.Thread(target=self._dynamic_stop_monitor_loop, name=f"StopMonitor-{self.symbol}") + self.monitor_thread.daemon = True + self.monitor_thread.start() + logger.info(f"[{self.symbol}] Dynamic stop-loss monitoring started") + + def stop_dynamic_stop_monitor(self): + """Stop dynamic stop-loss monitoring""" + self.monitor_running = False + if self.monitor_thread and self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=5) + if self.monitor_thread.is_alive(): + logger.warning(f"{self.symbol} dynamic stop-loss monitoring thread didn't exit normally") + else: + logger.info(f"{self.symbol} dynamic stop-loss monitoring stopped") + + def _dynamic_stop_monitor_loop(self): + """Dynamic stop-loss monitoring loop""" + while self.monitor_running: + try: + # Only monitor when there is position + if self.base_amount > 0: + self.check_dynamic_stops() + + # Monitor every 5 minutes + time.sleep(self.monitor_interval) + + except Exception as e: + logger.error(f"[{self.symbol}] Dynamic stop-loss monitoring error: {e}") + time.sleep(60) # Wait 1 minute before continuing when error occurs + + def check_dynamic_stops(self): + """Check dynamic stop-loss conditions""" + try: + # Get current price + current_price = self.api.get_current_price(self.symbol) + if not current_price: + logger.warning(f"[{self.symbol}] Unable to get current price, skipping stop-loss check") + return + + # Get dynamic stop-loss information + stop_info = self.db.get_dynamic_stop(self.symbol) + if not stop_info: + logger.debug(f"[{self.symbol}] No dynamic stop-loss settings") + return + + stop_loss_price = stop_info['current_stop_loss'] + take_profit_price = stop_info['current_take_profit'] + trailing_percent = stop_info['trailing_percent'] + + logger.debug(f"[{self.symbol}] Dynamic stop-loss check: current price=${current_price:.2f}, stop-loss=${stop_loss_price:.2f}, take-profit=${take_profit_price:.2f}") + + # Check stop-loss condition + if current_price <= stop_loss_price: + logger.warning(f"⚠️ {self.symbol} triggered dynamic stop-loss! Current price=${current_price:.2f} <= stop-loss price=${stop_loss_price:.2f}") + self.execute_trade({ + 'action': 'SELL', + 'confidence': 'HIGH', + 'reason': f'Dynamic stop-loss triggered: {current_price:.2f} <= {stop_loss_price:.2f}' + }, current_price) + return + + # Check take-profit condition + if current_price >= take_profit_price: + logger.info(f"🎯 {self.symbol} triggered dynamic take-profit! Current price=${current_price:.2f} >= take-profit price=${take_profit_price:.2f}") + self.execute_trade({ + 'action': 'SELL', + 'confidence': 'HIGH', + 'reason': f'Dynamic take-profit triggered: {current_price:.2f} >= {take_profit_price:.2f}' + }, current_price) + return + + # Update trailing stop-loss (only moves upward) - fix logic + if stop_info and current_price > stop_info.get('highest_price', 0): + new_stop_loss = self.db.update_dynamic_stop(self.symbol, current_price, trailing_percent=trailing_percent, multiplier=2) + if new_stop_loss: + logger.info(f"[{self.symbol}] Trailing stop-loss updated: new stop-loss price=${new_stop_loss:.2f}") + + except Exception as e: + logger.error(f"[{self.symbol}] Error checking dynamic stop-loss: {e}") + + def run(self): + """Run strategy (using enhanced decision)""" + self.strategy_running = True + + while self.strategy_running: + try: + logger.info(f"\n=== {self.symbol} Strategy Auto Execution ===") + + # Market analysis + analysis = self.analyze_market() + if analysis is None: + logger.warning("Analysis failed, waiting for next execution") + time.sleep(self.config['interval']) + continue + + # Make trading decision + decision = self.make_enhanced_decision(analysis) + logger.info(f"{self.symbol} Decision: {decision['action']} (Confidence: {decision['confidence']}, Score: {decision['total_score']:.3f})") + + if decision['reason']: + logger.info(f"Reason: {decision['reason']}") + + # Execute trade + self.execute_trade(decision, analysis['current_price']) + + # Risk management check + self.check_risk_management(analysis['current_price']) + + time.sleep(self.config['interval']) + + except Exception as e: + logger.error(f"Strategy execution error: {e}") + if "Network" in str(e) or "Connection" in str(e): + wait_time = min(300, self.config['interval'] * 2) # Network error wait longer + else: + wait_time = self.config['interval'] + + if self.strategy_running: + time.sleep(wait_time) + + def stop_strategy(self): + """Stop strategy running""" + with threading.Lock(): # Add thread lock + self.strategy_running = False + logger.info(f"{self.symbol} strategy stop signal sent") + + def check_risk_management(self, current_price): + """Check risk management (fixed stop-loss/take-profit)""" + if self.base_amount == 0: + return + + # Calculate profit/loss percentage + pnl_pct = (current_price - self.entry_price) / self.entry_price if self.entry_price > 0 else 0 + + # Fixed stop-loss/take-profit check + if pnl_pct <= -self.risk_manager.stop_loss: + logger.warning(f"⚠️ {self.symbol} triggered fixed stop-loss ({pnl_pct:.2%})") + self.execute_trade({ + 'action': 'SELL', + 'confidence': 'HIGH', + 'reason': 'Fixed stop-loss triggered' + }, current_price) + elif pnl_pct >= self.risk_manager.take_profit: + logger.info(f"🎯 {self.symbol} triggered fixed take-profit ({pnl_pct:.2%})") + self.execute_trade({ + 'action': 'SELL', + 'confidence': 'HIGH', + 'reason': 'Fixed take-profit triggered' + }, current_price) + +class MultiStrategyRunner: + """Multi-Strategy Runner""" + + def __init__(self): + self.running = False + self.strategies = {} + self.threads = {} + + # Configure each currency strategy + self.symbol_configs = { + 'ETH-USDT': {'name': 'Ethereum', 'interval': 1800, 'timeframe': '1H'}, + 'BTC-USDT': {'name': 'Bitcoin', 'interval': 1800, 'timeframe': '1H'}, + 'SOL-USDT': {'name': 'Solana', 'interval': 1800, 'timeframe': '1H'}, + 'XRP-USDT': {'name': 'Ripple', 'interval': 1800, 'timeframe': '1H'}, + 'BNB-USDT': {'name': 'Binance Coin', 'interval': 1800, 'timeframe': '1H'}, + 'OKB-USDT': {'name': 'OKB', 'interval': 1800, 'timeframe': '1H'}, + } + + # Initialize API client + self.api = OKXAPIClient() + self.deepseek = DeepSeekAnalyzer() + self.risk_manager = RiskManager(self.api) + + # Initialize all strategies + for symbol, config in self.symbol_configs.items(): + self.strategies[symbol] = TradingStrategy(symbol, config, self.api, self.risk_manager, self.deepseek) + + # Register exit handlers + atexit.register(self.shutdown) + signal.signal(signal.SIGINT, lambda s, f: self.shutdown()) + signal.signal(signal.SIGTERM, lambda s, f: self.shutdown()) + + def start_strategy(self, symbol): + """Start single currency strategy""" + if symbol not in self.strategies: + logger.error(f"Unsupported {symbol} trading pair") + return False + + # Check if already running + if symbol in self.threads and self.threads[symbol].is_alive(): + logger.info(f"{symbol} strategy already running") + return True + + def strategy_worker(): + """Strategy worker thread""" + strategy = self.strategies[symbol] + logger.info(f"Starting {symbol} strategy") + + try: + # Directly run strategy, strategy has its own loop internally + strategy.run() + except Exception as e: + logger.error(f"{symbol} strategy execution error: {e}") + finally: + logger.info(f"{symbol} strategy thread ended") + # Remove from active thread list when thread ends + if symbol in self.threads: + del self.threads[symbol] + + # Create and start thread + thread = threading.Thread(target=strategy_worker, name=f"Strategy-{symbol}") + thread.daemon = True + thread.start() + self.threads[symbol] = thread + + logger.info(f"Started {symbol} strategy") + return True + + def start_all_strategies(self): + """Start all strategies""" + self.running = True + + for symbol in self.strategies.keys(): + self.start_strategy(symbol) + time.sleep(60) # Each currency starts with 60 second interval + + + logger.info("All strategies started") + + def stop_strategy(self, symbol): + """Stop single currency strategy""" + if symbol in self.strategies: + # First stop strategy + self.strategies[symbol].stop_strategy() + + if symbol in self.threads: + thread = self.threads[symbol] + thread.join(timeout=10) # Wait 10 seconds + if thread.is_alive(): + logger.warning(f"Unable to stop {symbol} strategy thread") + else: + del self.threads[symbol] + logger.info(f"Stopped {symbol} strategy") + + # Stop dynamic stop-loss monitoring + if symbol in self.strategies: + self.strategies[symbol].stop_dynamic_stop_monitor() + + def stop_all_strategies(self): + """Stop all strategies""" + self.running = False + + # First send stop signal to all strategies + for symbol in list(self.strategies.keys()): + self.strategies[symbol].stop_strategy() + + # Then stop threads + for symbol in list(self.threads.keys()): + self.stop_strategy(symbol) + + logger.info("All strategies stopped") + + def get_status(self): + """Get system status""" + status = { + 'running': self.running, + 'active_strategies': list(self.threads.keys()), + 'strategies_detail': {} + } + + for symbol, strategy in self.strategies.items(): + status['strategies_detail'][symbol] = { + 'base_amount': strategy.base_amount, + 'entry_price': strategy.entry_price, + 'config': self.symbol_configs[symbol] + } + + return status + + def shutdown(self): + """System shutdown handling""" + logger.info("System shutting down, stopping all strategies and monitoring...") + self.stop_all_strategies() + + # Stop all dynamic stop-loss monitoring + for symbol, strategy in self.strategies.items(): + try: + strategy.stop_dynamic_stop_monitor() + logger.info(f"Stopped {symbol} dynamic stop-loss monitoring") + except Exception as e: + logger.error(f"Error stopping {symbol} dynamic stop-loss monitoring: {e}") + + # Wait for all threads to end + for symbol, thread in list(self.threads.items()): + if thread.is_alive(): + thread.join(timeout=5) # Wait 5 seconds + if thread.is_alive(): + logger.warning(f"{symbol} strategy thread didn't exit normally") + + # Ensure all resources released + time.sleep(1) + logger.info("System shutdown completed") + print("👋 System safely exited, thank you for using!") + +def main(): + """Main function""" + print("=" * 60) + print(" " * 20 + "Multi-Currency Intelligent Trading System") + print("=" * 60) + print("Core Features:") + print(" • DeepSeek AI Driven Decisions") + print(" • KDJ + RSI + ATR Technical Indicators") + print(" • Multi-level Risk Control") + print(" • Position Status Persistence") + print("=" * 60) + + # Check environment variables + required_env_vars = ['OKX_API_KEY', 'OKX_SECRET_KEY', 'OKX_PASSWORD'] + if not all(os.getenv(var) for var in required_env_vars): + print("❌ Please set OKX API environment variables") + return + + # Create strategy runner + runner = MultiStrategyRunner() + + # Add debug mode switch + debug_mode = False + + def toggle_debug_mode(): + nonlocal debug_mode + debug_mode = not debug_mode + runner.api.log_http = debug_mode + print(f"Debug mode {'enabled' if debug_mode else 'disabled'}") + + try: + while True: + print("\n" + "=" * 50) + print(" " * 15 + "Trading System Control Panel") + print("=" * 50) + print("1. Start All Strategies") + print("2. Start Specific Strategy") + print("3. Stop All Strategies") + print("4. Stop Specific Strategy") + print("5. View System Status") + print("6. Manual Position Sync") + print("7. Toggle Debug Mode") + print("8. Exit System") + print("=" * 50) + + choice = input("Please enter choice (1-8): ").strip() + + if choice == '1': + print("Starting all trading strategies...") + runner.start_all_strategies() + print("✅ All strategies started") + + elif choice == '2': + print("\nSelect currency to start:") + symbols = list(runner.symbol_configs.keys()) + for i, symbol in enumerate(symbols, 1): + config = runner.symbol_configs[symbol] + print(f"{i}. {symbol} ({config['name']})") + print(f"{len(symbols)+1}. Back") + + symbol_choice = input("Please enter choice: ").strip() + if symbol_choice.isdigit(): + index = int(symbol_choice) - 1 + if 0 <= index < len(symbols): + symbol = symbols[index] + runner.start_strategy(symbol) + print(f"✅ Started {symbol} strategy") + elif index == len(symbols): + continue + else: + print("❌ Invalid choice") + else: + print("❌ Invalid input") + + elif choice == '3': + print("Stopping all trading strategies...") + runner.stop_all_strategies() + print("✅ All strategies stopped") + + elif choice == '4': + print("\nSelect currency to stop:") + symbols = list(runner.symbol_configs.keys()) + for i, symbol in enumerate(symbols, 1): + config = runner.symbol_configs[symbol] + print(f"{i}. {symbol} ({config['name']})") + print(f"{len(symbols)+1}. Back") + + symbol_choice = input("Please enter choice: ").strip() + if symbol_choice.isdigit(): + index = int(symbol_choice) - 1 + if 0 <= index < len(symbols): + symbol = symbols[index] + runner.stop_strategy(symbol) + print(f"✅ Stopped {symbol} strategy") + elif index == len(symbols): + continue + else: + print("❌ Invalid choice") + else: + print("❌ Invalid input") + + elif choice == '5': + status = runner.get_status() + print(f"\nSystem Status: {'Running' if status['running'] else 'Stopped'}") + print(f"Active Strategies: {len(status['active_strategies'])}") + for symbol in status['active_strategies']: + print(f" • {symbol}") + + print("\nPosition Status:") + for symbol, detail in status['strategies_detail'].items(): + base_amount = detail['base_amount'] + entry_price = detail['entry_price'] + if base_amount > 0: + print(f" {symbol}: Position {base_amount:.10f} @ ${entry_price:.2f}") + + elif choice == '6': + print("Manual sync all currency positions...") + for symbol, strategy in runner.strategies.items(): + strategy.sync_with_exchange() + print("✅ Sync completed") + + elif choice == '7': + toggle_debug_mode() + + elif choice == '8': + print("Exiting system, please wait for all strategies to stop...") + break + + else: + print("❌ Invalid choice, please re-enter") + + except KeyboardInterrupt: + print("\nReceived interrupt signal, exiting...") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..92dda6d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "okxtrading2-0" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "numpy>=2.3.4", + "pandas>=2.3.3", + "python-dotenv>=1.2.1", + "python-okx>=0.4.0", + "requests>=2.32.5", + "schedule>=1.2.2", +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..12a6535 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +ccxt>=4.2.6 +pandas>=2.0.3 +numpy>=1.24.3 +requests>=2.31.0 +python-dotenv>=1.0.0 +schedule>=1.2.0 \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..510476e --- /dev/null +++ b/uv.lock @@ -0,0 +1,790 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "automat" +version = "25.4.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977, upload-time = "2025-04-16T20:12:16.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[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 = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "constantly" +version = "23.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300, upload-time = "2023-10-28T23:18:24.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547, upload-time = "2023-10-28T23:18:23.038Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + +[[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 = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "incremental" +version = "24.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157, upload-time = "2024-07-29T20:03:55.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516, upload-time = "2024-07-29T20:03:53.677Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, + { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, + { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, + { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, + { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, + { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, + { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, + { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, + { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, + { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, + { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, + { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, + { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, +] + +[[package]] +name = "okxtrading2-0" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "python-dotenv" }, + { name = "python-okx" }, + { name = "requests" }, + { name = "schedule" }, +] + +[package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=2.3.4" }, + { name = "pandas", specifier = ">=2.3.3" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "python-okx", specifier = ">=0.4.0" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "schedule", specifier = ">=1.2.2" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pyopenssl" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "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 = "python-okx" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "importlib-metadata" }, + { name = "keyring" }, + { name = "loguru" }, + { name = "pyopenssl" }, + { name = "requests" }, + { name = "twisted" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/07/07320f86d8c66bbbfc940cb4de1caf8699a211e8839e14b3b970a4132496/python_okx-0.4.0.tar.gz", hash = "sha256:c2a878dc2c2c3badac61dbb3f980b386dd89932a4b5d11e82fe1cce6619c92c2", size = 23474, upload-time = "2025-07-28T03:20:55.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/35/bb5f1cea2d3432cac1fe448f1b80d5f42712cc2ac7221449ec168af86a7f/python_okx-0.4.0-py3-none-any.whl", hash = "sha256:20e2df6a1ed1ea0d995eb31307ca8d76749b7b880db88f1252e98ad16242e5f2", size = 33189, upload-time = "2025-07-28T03:20:54.009Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[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 = "schedule" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/91/b525790063015759f34447d4cf9d2ccb52cdee0f1dd6ff8764e863bcb74c/schedule-1.2.2.tar.gz", hash = "sha256:15fe9c75fe5fd9b9627f3f19cc0ef1420508f9f9a46f45cd0769ef75ede5f0b7", size = 26452, upload-time = "2024-06-18T20:03:14.633Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/a7/84c96b61fd13205f2cafbe263cdb2745965974bdf3e0078f121dfeca5f02/schedule-1.2.2-py3-none-any.whl", hash = "sha256:5bef4a2a0183abf44046ae0d164cadcac21b1db011bdd8102e4a0c1e91e06a7d", size = 12220, upload-time = "2024-05-25T18:41:59.121Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/9f/11ef35cf1027c1339552ea7bfe6aaa74a8516d8b5caf6e7d338daf54fd80/secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c", size = 19748, upload-time = "2025-09-09T16:42:13.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "twisted" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "automat" }, + { name = "constantly" }, + { name = "hyperlink" }, + { name = "incremental" }, + { name = "typing-extensions" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/0f/82716ed849bf7ea4984c21385597c949944f0f9b428b5710f79d0afc084d/twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316", size = 3545725, upload-time = "2025-06-07T09:52:24.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/66/ab7efd8941f0bc7b2bd555b0f0471bff77df4c88e0cc31120c82737fec77/twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7", size = 3204767, upload-time = "2025-06-07T09:52:21.428Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "1.26.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/56/d87d6d3c4121c0bcec116919350ca05dc3afd2eeb7dc88d07e8083f8ea94/urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", size = 299806, upload-time = "2022-08-22T13:27:23.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/de/5be2e3eed8426f871b170663333a0f627fc2924cc386cd41be065e7ea870/urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997", size = 140381, upload-time = "2022-08-22T13:27:19.719Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zope-interface" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/3a/7fcf02178b8fad0a51e67e32765cd039ae505d054d744d76b8c2bbcba5ba/zope_interface-8.0.1.tar.gz", hash = "sha256:eba5610d042c3704a48222f7f7c6ab5b243ed26f917e2bc69379456b115e02d1", size = 253746, upload-time = "2025-09-25T05:55:51.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/a6/0f08713ddda834c428ebf97b2a7fd8dea50c0100065a8955924dbd94dae8/zope_interface-8.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:115f27c1cc95ce7a517d960ef381beedb0a7ce9489645e80b9ab3cbf8a78799c", size = 208609, upload-time = "2025-09-25T05:58:53.698Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/d423045f54dc81e0991ec655041e7a0eccf6b2642535839dd364b35f4d7f/zope_interface-8.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af655c573b84e3cb6a4f6fd3fbe04e4dc91c63c6b6f99019b3713ef964e589bc", size = 208797, upload-time = "2025-09-25T05:58:56.258Z" }, + { url = "https://files.pythonhosted.org/packages/c6/43/39d4bb3f7a80ebd261446792493cfa4e198badd47107224f5b6fe1997ad9/zope_interface-8.0.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:23f82ef9b2d5370750cc1bf883c3b94c33d098ce08557922a3fbc7ff3b63dfe1", size = 259242, upload-time = "2025-09-25T05:58:21.602Z" }, + { url = "https://files.pythonhosted.org/packages/da/29/49effcff64ef30731e35520a152a9dfcafec86cf114b4c2aff942e8264ba/zope_interface-8.0.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35a1565d5244997f2e629c5c68715b3d9d9036e8df23c4068b08d9316dcb2822", size = 264696, upload-time = "2025-09-25T05:58:13.351Z" }, + { url = "https://files.pythonhosted.org/packages/c7/39/b947673ec9a258eeaa20208dd2f6127d9fbb3e5071272a674ebe02063a78/zope_interface-8.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:029ea1db7e855a475bf88d9910baab4e94d007a054810e9007ac037a91c67c6f", size = 264229, upload-time = "2025-09-25T06:26:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/eed6efd1fc3788d1bef7a814e0592d8173b7fe601c699b935009df035fc2/zope_interface-8.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0beb3e7f7dc153944076fcaf717a935f68d39efa9fce96ec97bafcc0c2ea6cab", size = 212270, upload-time = "2025-09-25T05:58:53.584Z" }, + { url = "https://files.pythonhosted.org/packages/5f/dc/3c12fca01c910c793d636ffe9c0984e0646abaf804e44552070228ed0ede/zope_interface-8.0.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:c7cc027fc5c61c5d69e5080c30b66382f454f43dc379c463a38e78a9c6bab71a", size = 208992, upload-time = "2025-09-25T05:58:40.712Z" }, + { url = "https://files.pythonhosted.org/packages/46/71/6127b7282a3e380ca927ab2b40778a9c97935a4a57a2656dadc312db5f30/zope_interface-8.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fcf9097ff3003b7662299f1c25145e15260ec2a27f9a9e69461a585d79ca8552", size = 209051, upload-time = "2025-09-25T05:58:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/56/86/4387a9f951ee18b0e41fda77da77d59c33e59f04660578e2bad688703e64/zope_interface-8.0.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6d965347dd1fb9e9a53aa852d4ded46b41ca670d517fd54e733a6b6a4d0561c2", size = 259223, upload-time = "2025-09-25T05:58:23.191Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/ce60a114466abc067c68ed41e2550c655f551468ae17b4b17ea360090146/zope_interface-8.0.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a3b8bb77a4b89427a87d1e9eb969ab05e38e6b4a338a9de10f6df23c33ec3c2", size = 264690, upload-time = "2025-09-25T05:58:15.052Z" }, + { url = "https://files.pythonhosted.org/packages/36/9a/62a9ba3a919594605a07c34eee3068659bbd648e2fa0c4a86d876810b674/zope_interface-8.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:87e6b089002c43231fb9afec89268391bcc7a3b66e76e269ffde19a8112fb8d5", size = 264201, upload-time = "2025-09-25T06:26:27.797Z" }, + { url = "https://files.pythonhosted.org/packages/da/06/8fe88bd7edef60566d21ef5caca1034e10f6b87441ea85de4bbf9ea74768/zope_interface-8.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:64a43f5280aa770cbafd0307cb3d1ff430e2a1001774e8ceb40787abe4bb6658", size = 212273, upload-time = "2025-09-25T06:00:25.398Z" }, +]