From 91a41467f3508c2ba6ee44e5c8231b43ddbc054f Mon Sep 17 00:00:00 2001 From: ashione Date: Mon, 22 Sep 2025 01:28:25 +0800 Subject: [PATCH 1/5] crypto trading plugin --- .gitignore | 3 + pyproject.toml | 4 + vertex_flow/plugins/crypto_trading/README.md | 249 ++++++++ .../plugins/crypto_trading/__init__.py | 14 + vertex_flow/plugins/crypto_trading/client.py | 286 +++++++++ vertex_flow/plugins/crypto_trading/config.py | 125 ++++ .../plugins/crypto_trading/error_handler.py | 172 ++++++ vertex_flow/plugins/crypto_trading/example.py | 546 ++++++++++++++++++ .../plugins/crypto_trading/exchanges.py | 497 ++++++++++++++++ .../plugins/crypto_trading/indicators.py | 332 +++++++++++ .../crypto_trading/position_metrics.py | 323 +++++++++++ .../plugins/crypto_trading/requirements.txt | 12 + vertex_flow/plugins/crypto_trading/trading.py | 443 ++++++++++++++ 13 files changed, 3006 insertions(+) create mode 100644 vertex_flow/plugins/crypto_trading/README.md create mode 100644 vertex_flow/plugins/crypto_trading/__init__.py create mode 100644 vertex_flow/plugins/crypto_trading/client.py create mode 100644 vertex_flow/plugins/crypto_trading/config.py create mode 100644 vertex_flow/plugins/crypto_trading/error_handler.py create mode 100644 vertex_flow/plugins/crypto_trading/example.py create mode 100644 vertex_flow/plugins/crypto_trading/exchanges.py create mode 100644 vertex_flow/plugins/crypto_trading/indicators.py create mode 100644 vertex_flow/plugins/crypto_trading/position_metrics.py create mode 100644 vertex_flow/plugins/crypto_trading/requirements.txt create mode 100644 vertex_flow/plugins/crypto_trading/trading.py diff --git a/.gitignore b/.gitignore index b8f548f..4ed30b6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,8 @@ models/ # 配置文件(包含敏感信息) config/llm.yml +.env +.env.* # 虚拟环境 venv/ @@ -76,3 +78,4 @@ config/llm.yml.backup* reports/* docker/.env vertex_flow/plugins/wechat/.env +vertex_flow/plugins/crypto_trading/.env diff --git a/pyproject.toml b/pyproject.toml index 834a9c3..2603512 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,10 @@ dependencies = [ "socksio>=1.0.0", "pywebview>=5.4", "nest-asyncio>=1.6.0", + "websocket-client>=1.8.0", + "pandas>=2.3.2", + "numpy>=2.0.2", + "ta-lib>=0.6.7", ] [project.urls] diff --git a/vertex_flow/plugins/crypto_trading/README.md b/vertex_flow/plugins/crypto_trading/README.md new file mode 100644 index 0000000..e35064a --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/README.md @@ -0,0 +1,249 @@ +# Crypto Trading Plugin + +数字货币量化交易插件,支持OKX和Binance交易所的API集成,提供账户管理、技术分析和自动化交易功能。 + +## 功能特性 + +- **多交易所支持**: 支持OKX和Binance交易所 +- **账户管理**: 获取账户信息、余额、交易费率 +- **技术分析**: 计算多种技术指标(RSI、MACD、布林带等) +- **自动交易**: 基于技术分析信号的自动买卖 +- **风险管理**: 内置止损止盈和仓位管理 +- **实时数据**: 获取实时价格、K线数据 + +## 安装依赖 + +```bash +pip install -r requirements.txt +``` + +## 配置设置 + +### 1. 环境变量配置 + +复制 `.env.example` 到 `.env` 并填入你的API密钥: + +```bash +cp .env.example .env +``` + +编辑 `.env` 文件: + +```env +# OKX配置 +OKX_API_KEY=your_okx_api_key +OKX_SECRET_KEY=your_okx_secret_key +OKX_PASSPHRASE=your_okx_passphrase +OKX_SANDBOX=true # 测试环境 + +# Binance配置 +BINANCE_API_KEY=your_binance_api_key +BINANCE_SECRET_KEY=your_binance_secret_key +BINANCE_SANDBOX=true # 测试环境 +``` + +### 2. 程序化配置 + +```python +from crypto_trading.config import CryptoTradingConfig + +config = CryptoTradingConfig() + +# 设置OKX配置 +config.set_okx_config( + api_key="your_api_key", + secret_key="your_secret_key", + passphrase="your_passphrase", + sandbox=True +) + +# 设置Binance配置 +config.set_binance_config( + api_key="your_api_key", + secret_key="your_secret_key", + sandbox=True +) +``` + +## 基本使用 + +### 1. 初始化客户端 + +```python +from crypto_trading import CryptoTradingClient, TradingEngine + +# 初始化客户端 +client = CryptoTradingClient() + +# 初始化交易引擎 +trading_engine = TradingEngine(client) +``` + +### 2. 获取账户信息 + +```python +# 获取所有交易所账户信息 +all_accounts = client.get_all_account_info() + +# 获取特定交易所账户信息 +okx_account = client.get_account_info("okx") +binance_account = client.get_account_info("binance") + +# 获取余额 +usdt_balance = client.get_balance("okx", "USDT") +all_balances = client.get_balance("okx") +``` + +### 3. 获取市场数据 + +```python +# 获取价格信息 +ticker = client.get_ticker("okx", "BTC-USDT") +print(f"当前价格: ${ticker['price']}") + +# 获取K线数据 +klines = client.get_klines("okx", "BTC-USDT", "1h", 100) + +# 获取交易费率 +fees = client.get_trading_fees("okx", "BTC-USDT") +print(f"挂单费率: {fees['maker_fee']}") +print(f"吃单费率: {fees['taker_fee']}") +``` + +### 4. 技术分析 + +```python +from crypto_trading.indicators import TechnicalIndicators + +# 获取K线数据 +klines = client.get_klines("okx", "BTC-USDT", "1h", 100) + +# 计算所有技术指标 +indicators = TechnicalIndicators.calculate_all_indicators(klines) + +print(f"RSI: {indicators['rsi']}") +print(f"MACD: {indicators['macd']}") +print(f"布林带: {indicators['bollinger_bands']}") + +# 生成交易信号 +signals = TechnicalIndicators.get_trading_signals(indicators) +print(f"整体信号: {signals['overall']}") +``` + +### 5. 手动交易 + +```python +# 市价买入 +buy_result = trading_engine.buy_market("okx", "BTC-USDT", 100.0) # 买入$100 +print(f"买入结果: {buy_result}") + +# 市价卖出 +sell_result = trading_engine.sell_market("okx", "BTC-USDT", 0.001) # 卖出0.001 BTC +print(f"卖出结果: {sell_result}") + +# 限价买入 +limit_buy = trading_engine.buy_limit("okx", "BTC-USDT", 0.001, 50000.0) + +# 限价卖出 +limit_sell = trading_engine.sell_limit("okx", "BTC-USDT", 0.001, 60000.0) +``` + +### 6. 自动交易 + +```python +# 基于技术分析信号自动交易 +auto_result = trading_engine.auto_trade_by_signals("okx", "BTC-USDT", 100.0) +print(f"自动交易结果: {auto_result}") + +# 获取交易摘要 +summary = trading_engine.get_trading_summary("okx", "BTC-USDT") +print(f"推荐信号: {summary['technical_analysis']['overall_signal']}") +print(f"推荐仓位: ${summary['risk_management']['recommended_position_size_usdt']}") +``` + +## 技术指标说明 + +插件支持以下技术指标: + +- **移动平均线**: SMA, EMA +- **动量指标**: RSI, Williams %R +- **趋势指标**: MACD, 布林带 +- **波动率指标**: ATR, 布林带 +- **成交量指标**: OBV +- **震荡指标**: 随机指标, CCI +- **支撑阻力**: 自动识别支撑阻力位 + +## 风险管理 + +插件内置多种风险管理功能: + +- **仓位管理**: 基于账户余额和风险比例计算仓位 +- **止损止盈**: 自动计算止损止盈价位 +- **风险控制**: 限制单笔交易最大金额 + +```python +# 风险管理配置 +config.trading_config.risk_percentage = 0.02 # 每笔交易风险2% +config.trading_config.max_position_size = 1000.0 # 最大仓位$1000 +config.trading_config.stop_loss_percentage = 0.05 # 止损5% +config.trading_config.take_profit_percentage = 0.10 # 止盈10% +``` + +## 注意事项 + +⚠️ **重要提醒**: + +1. **测试环境**: 首次使用请务必在沙盒/测试环境中测试 +2. **API权限**: 确保API密钥有足够的交易权限 +3. **资金安全**: 不要在生产环境中使用未经充分测试的策略 +4. **风险控制**: 始终设置合理的止损和仓位管理 +5. **监控**: 定期监控交易结果和账户状态 + +## 示例代码 + +运行示例代码: + +```bash +python example.py +``` + +示例包含: +- 基本功能演示 +- 技术分析示例 +- 交易操作示例 +- 风险管理示例 + +## 故障排除 + +### 常见问题 + +1. **API连接失败** + - 检查API密钥是否正确 + - 确认网络连接正常 + - 验证API权限设置 + +2. **交易失败** + - 检查账户余额是否充足 + - 确认交易对格式正确 + - 验证最小交易数量要求 + +3. **技术指标计算错误** + - 确保K线数据充足(至少50根) + - 检查数据格式是否正确 + +### 调试模式 + +启用调试模式获取详细日志: + +```python +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +## 许可证 + +本插件遵循MIT许可证。 + +## 免责声明 + +本插件仅供学习和研究使用。数字货币交易存在高风险,可能导致资金损失。使用本插件进行实际交易的风险由用户自行承担。 \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/__init__.py b/vertex_flow/plugins/crypto_trading/__init__.py new file mode 100644 index 0000000..54bf336 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/__init__.py @@ -0,0 +1,14 @@ +""" +Crypto Trading Plugin for Vertex Flow + +This plugin provides quantitative trading capabilities for cryptocurrency exchanges, +supporting OKX and Binance APIs for account management, trading, and technical analysis. +""" + +from .client import CryptoTradingClient +from .exchanges import OKXClient, BinanceClient +from .indicators import TechnicalIndicators +from .trading import TradingEngine + +__version__ = "1.0.0" +__all__ = ["CryptoTradingClient", "OKXClient", "BinanceClient", "TechnicalIndicators", "TradingEngine"] \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/client.py b/vertex_flow/plugins/crypto_trading/client.py new file mode 100644 index 0000000..c800338 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/client.py @@ -0,0 +1,286 @@ +""" +Main crypto trading client that integrates all exchange functionalities +""" + +import time +from typing import Dict, Any, List, Optional, Union + +try: + from .config import CryptoTradingConfig + from .exchanges import OKXClient, BinanceClient, BaseExchange +except ImportError: + from config import CryptoTradingConfig + from exchanges import OKXClient, BinanceClient, BaseExchange + + +class CryptoTradingClient: + """Main client for crypto trading operations""" + + def __init__(self, config: Optional[CryptoTradingConfig] = None): + self.config = config or CryptoTradingConfig() + self.exchanges: Dict[str, BaseExchange] = {} + self._initialize_exchanges() + + def _initialize_exchanges(self): + """Initialize exchange clients based on configuration""" + if self.config.okx_config: + self.exchanges["okx"] = OKXClient(self.config.okx_config) + + if self.config.binance_config: + self.exchanges["binance"] = BinanceClient(self.config.binance_config) + + def get_available_exchanges(self) -> List[str]: + """Get list of available exchanges""" + return list(self.exchanges.keys()) + + def get_account_info(self, exchange: str) -> Dict[str, Any]: + """ + Get account information from specified exchange + + Args: + exchange: Exchange name ('okx' or 'binance') + + Returns: + Account information dictionary + """ + if exchange not in self.exchanges: + raise ValueError(f"Exchange '{exchange}' not configured or not supported") + + try: + account_info = self.exchanges[exchange].get_account_info() + return self._normalize_account_info(exchange, account_info) + except Exception as e: + return {"error": str(e), "exchange": exchange} + + def get_all_account_info(self) -> Dict[str, Dict[str, Any]]: + """Get account information from all configured exchanges""" + results = {} + for exchange_name in self.exchanges: + results[exchange_name] = self.get_account_info(exchange_name) + return results + + def get_trading_fees(self, exchange: str, symbol: str) -> Dict[str, float]: + """ + Get trading fees for a symbol on specified exchange + + Args: + exchange: Exchange name ('okx' or 'binance') + symbol: Trading symbol (e.g., 'BTC-USDT' for OKX, 'BTCUSDT' for Binance) + + Returns: + Dictionary with maker_fee and taker_fee + """ + if exchange not in self.exchanges: + raise ValueError(f"Exchange '{exchange}' not configured or not supported") + + try: + return self.exchanges[exchange].get_trading_fees(symbol) + except Exception as e: + return {"error": str(e), "maker_fee": 0, "taker_fee": 0} + + def get_ticker(self, exchange: str, symbol: str) -> Dict[str, Any]: + """ + Get ticker information for a symbol + + Args: + exchange: Exchange name ('okx' or 'binance') + symbol: Trading symbol + + Returns: + Ticker information dictionary + """ + if exchange not in self.exchanges: + raise ValueError(f"Exchange '{exchange}' not configured or not supported") + + try: + return self.exchanges[exchange].get_ticker(symbol) + except Exception as e: + return {"error": str(e), "exchange": exchange, "symbol": symbol} + + def get_klines(self, exchange: str, symbol: str, interval: str = "1h", limit: int = 100) -> List[List]: + """ + Get kline/candlestick data + + Args: + exchange: Exchange name ('okx' or 'binance') + symbol: Trading symbol + interval: Time interval ('1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w') + limit: Number of klines to retrieve + + Returns: + List of kline data [timestamp, open, high, low, close, volume] + """ + if exchange not in self.exchanges: + raise ValueError(f"Exchange '{exchange}' not configured or not supported") + + try: + return self.exchanges[exchange].get_klines(symbol, interval, limit) + except Exception as e: + print(f"Error getting klines: {e}") + return [] + + def get_balance(self, exchange: str, currency: Optional[str] = None) -> Union[Dict[str, float], float]: + """ + Get balance for specific currency or all currencies + + Args: + exchange: Exchange name + currency: Currency symbol (optional, if None returns all balances) + + Returns: + Balance information + """ + account_info = self.get_account_info(exchange) + + if "error" in account_info: + return 0.0 if currency else {} + + balances = account_info.get("balances", {}) + + if currency: + return balances.get(currency, 0.0) + + return balances + + def _normalize_account_info(self, exchange: str, raw_info: Dict[str, Any]) -> Dict[str, Any]: + """Normalize account information across different exchanges""" + normalized = { + "exchange": exchange, + "balances": {}, + "total_value_usdt": 0.0, + "raw_data": raw_info + } + + try: + if exchange == "okx": + if raw_info.get("data"): + for balance_info in raw_info["data"]: + for detail in balance_info.get("details", []): + currency = detail.get("ccy") + available = float(detail.get("availBal", 0)) + frozen = float(detail.get("frozenBal", 0)) + + if available > 0 or frozen > 0: + normalized["balances"][currency] = { + "available": available, + "frozen": frozen, + "total": available + frozen + } + + elif exchange == "binance": + for balance in raw_info.get("balances", []): + currency = balance.get("asset") + free = float(balance.get("free", 0)) + locked = float(balance.get("locked", 0)) + + if free > 0 or locked > 0: + normalized["balances"][currency] = { + "available": free, + "frozen": locked, + "total": free + locked + } + + except Exception as e: + normalized["error"] = f"Error normalizing account info: {str(e)}" + + return normalized + + def get_exchange_status(self) -> Dict[str, Dict[str, Any]]: + """Get status of all configured exchanges""" + status = {} + + for exchange_name in self.exchanges: + try: + # Test connection by getting account info + account_info = self.get_account_info(exchange_name) + status[exchange_name] = { + "connected": "error" not in account_info, + "error": account_info.get("error"), + "last_check": "now" + } + except Exception as e: + status[exchange_name] = { + "connected": False, + "error": str(e), + "last_check": "now" + } + + return status + + def get_spot_positions(self, exchange: str) -> Dict[str, Any]: + """ + Get spot positions for a specific exchange + + Args: + exchange: Exchange name ('okx' or 'binance') + + Returns: + Spot positions information + """ + if exchange not in self.exchanges: + return {"error": f"Exchange '{exchange}' not configured or not supported"} + + try: + return self.exchanges[exchange].get_spot_positions() + except Exception as e: + return {"error": f"Failed to get spot positions: {str(e)}"} + + def get_futures_positions(self, exchange: str) -> Dict[str, Any]: + """ + Get futures positions for a specific exchange + + Args: + exchange: Exchange name ('okx' or 'binance') + + Returns: + Futures positions information + """ + if exchange not in self.exchanges: + return {"error": f"Exchange '{exchange}' not configured or not supported"} + + try: + return self.exchanges[exchange].get_futures_positions() + except Exception as e: + return {"error": f"Failed to get futures positions: {str(e)}"} + + def get_all_positions(self, exchange: str = None) -> Dict[str, Any]: + """ + Get both spot and futures positions for one or all exchanges + + Args: + exchange: Exchange name (optional, if None returns all exchanges) + + Returns: + All positions information + """ + if exchange: + # Get positions for specific exchange + if exchange not in self.exchanges: + return {"error": f"Exchange '{exchange}' not configured or not supported"} + + spot_positions = self.get_spot_positions(exchange) + futures_positions = self.get_futures_positions(exchange) + + return { + "exchange": exchange, + "spot": spot_positions, + "futures": futures_positions, + "timestamp": time.time() + } + else: + # Get positions for all exchanges + all_positions = {} + + for exchange_name in self.exchanges: + spot_positions = self.get_spot_positions(exchange_name) + futures_positions = self.get_futures_positions(exchange_name) + + all_positions[exchange_name] = { + "spot": spot_positions, + "futures": futures_positions + } + + return { + "all_exchanges": all_positions, + "timestamp": time.time() + } \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/config.py b/vertex_flow/plugins/crypto_trading/config.py new file mode 100644 index 0000000..2c81031 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/config.py @@ -0,0 +1,125 @@ +""" +Configuration management for crypto trading plugin +""" + +import os +from typing import Dict, Any, Optional +from dataclasses import dataclass +from dotenv import load_dotenv + +# 加载.env文件 +load_dotenv() + + +@dataclass +class ExchangeConfig: + """Exchange configuration""" + api_key: str + secret_key: str + passphrase: Optional[str] = None # For OKX + sandbox: bool = False + base_url: Optional[str] = None + + +@dataclass +class TradingConfig: + """Trading configuration""" + default_symbol: str = "BTC-USDT" + max_position_size: float = 1000.0 + risk_percentage: float = 0.02 + stop_loss_percentage: float = 0.05 + take_profit_percentage: float = 0.10 + + +class CryptoTradingConfig: + """Main configuration class for crypto trading plugin""" + + def __init__(self): + self.okx_config: Optional[ExchangeConfig] = None + self.binance_config: Optional[ExchangeConfig] = None + self.trading_config = TradingConfig() + self._load_from_env() + + def _load_from_env(self): + """Load configuration from environment variables""" + # OKX Configuration + okx_api_key = os.getenv("OKX_API_KEY") + okx_secret_key = os.getenv("OKX_SECRET_KEY") + okx_passphrase = os.getenv("OKX_PASSPHRASE") + + if okx_api_key and okx_secret_key and okx_passphrase: + self.okx_config = ExchangeConfig( + api_key=okx_api_key, + secret_key=okx_secret_key, + passphrase=okx_passphrase, + sandbox=os.getenv("OKX_SANDBOX", "false").lower() == "true" + ) + + # Binance Configuration + binance_api_key = os.getenv("BINANCE_API_KEY") + binance_secret_key = os.getenv("BINANCE_SECRET_KEY") + + if binance_api_key and binance_secret_key: + self.binance_config = ExchangeConfig( + api_key=binance_api_key, + secret_key=binance_secret_key, + sandbox=os.getenv("BINANCE_SANDBOX", "false").lower() == "true" + ) + + # Trading Configuration + default_symbol = os.getenv("DEFAULT_SYMBOL") + if default_symbol: + self.trading_config.default_symbol = default_symbol + + max_position_size = os.getenv("MAX_POSITION_SIZE") + if max_position_size: + try: + self.trading_config.max_position_size = float(max_position_size) + except ValueError: + pass + + risk_percentage = os.getenv("RISK_PERCENTAGE") + if risk_percentage: + try: + self.trading_config.risk_percentage = float(risk_percentage) + except ValueError: + pass + + stop_loss_percentage = os.getenv("STOP_LOSS_PERCENTAGE") + if stop_loss_percentage: + try: + self.trading_config.stop_loss_percentage = float(stop_loss_percentage) + except ValueError: + pass + + take_profit_percentage = os.getenv("TAKE_PROFIT_PERCENTAGE") + if take_profit_percentage: + try: + self.trading_config.take_profit_percentage = float(take_profit_percentage) + except ValueError: + pass + + def set_okx_config(self, api_key: str, secret_key: str, passphrase: str, sandbox: bool = False): + """Set OKX configuration""" + self.okx_config = ExchangeConfig( + api_key=api_key, + secret_key=secret_key, + passphrase=passphrase, + sandbox=sandbox + ) + + def set_binance_config(self, api_key: str, secret_key: str, sandbox: bool = False): + """Set Binance configuration""" + self.binance_config = ExchangeConfig( + api_key=api_key, + secret_key=secret_key, + sandbox=sandbox + ) + + def get_config_dict(self) -> Dict[str, Any]: + """Get configuration as dictionary""" + return { + "okx": self.okx_config.__dict__ if self.okx_config else None, + "binance": self.binance_config.__dict__ if self.binance_config else None, + "trading": self.trading_config.__dict__ + } \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/error_handler.py b/vertex_flow/plugins/crypto_trading/error_handler.py new file mode 100644 index 0000000..75bf225 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/error_handler.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +错误处理工具模块 +提供统一的错误格式化和处理功能 +""" + +import re +from typing import Dict, Any, Optional +import requests + + +class ErrorHandler: + """统一的错误处理工具类""" + + @staticmethod + def format_api_error( + service_name: str, + error: Exception, + response: Optional[requests.Response] = None + ) -> Dict[str, Any]: + """ + 格式化API错误信息 + + Args: + service_name: 服务名称 (如 "OKX", "Binance") + error: 异常对象 + response: HTTP响应对象 (可选) + + Returns: + 格式化的错误信息字典 + """ + error_info = { + "error": True, + "service": service_name, + "type": type(error).__name__, + "message": str(error) + } + + if isinstance(error, requests.exceptions.RequestException): + error_info["category"] = "network" + + if hasattr(error, 'response') and error.response is not None: + response = error.response + error_info["status_code"] = response.status_code + + # 根据状态码提供更友好的错误信息 + if response.status_code == 401: + error_info["user_message"] = "API密钥无效或已过期,请检查配置" + elif response.status_code == 403: + error_info["user_message"] = "API权限不足,请检查API密钥权限设置" + elif response.status_code == 429: + error_info["user_message"] = "请求频率过高,请稍后重试" + elif response.status_code >= 500: + error_info["user_message"] = f"{service_name}服务器暂时不可用,请稍后重试" + else: + error_info["user_message"] = f"{service_name}API请求失败 (状态码: {response.status_code})" + + # 处理响应内容 + content_type = response.headers.get('content-type', '').lower() + if 'html' in content_type: + error_info["response_type"] = "html" + error_info["user_message"] += " - 服务器返回了错误页面" + elif response.text: + error_info["response_type"] = "text" + # 只保留前100个字符的有用信息 + clean_text = ErrorHandler._clean_error_text(response.text) + if clean_text: + error_info["response_preview"] = clean_text[:100] + else: + error_info["user_message"] = f"网络连接失败,无法访问{service_name}服务" + + elif "json" in str(error).lower() or "JSONDecodeError" in type(error).__name__: + error_info["category"] = "parsing" + error_info["user_message"] = f"{service_name}返回了无效的数据格式" + + if response: + content_type = response.headers.get('content-type', '').lower() + if 'html' in content_type: + error_info["user_message"] += " (服务器返回了HTML页面)" + + else: + error_info["category"] = "unknown" + error_info["user_message"] = f"{service_name}服务出现未知错误" + + return error_info + + @staticmethod + def _clean_error_text(text: str) -> str: + """ + 清理错误文本,移除HTML标签和多余的空白字符 + + Args: + text: 原始文本 + + Returns: + 清理后的文本 + """ + if not text: + return "" + + # 移除HTML标签 + clean_text = re.sub(r'<[^>]+>', '', text) + + # 移除多余的空白字符 + clean_text = re.sub(r'\s+', ' ', clean_text).strip() + + # 如果文本太长,只保留开头部分 + if len(clean_text) > 200: + clean_text = clean_text[:200] + "..." + + return clean_text + + @staticmethod + def get_user_friendly_message(error_info: Dict[str, Any]) -> str: + """ + 获取用户友好的错误信息 + + Args: + error_info: 错误信息字典 + + Returns: + 用户友好的错误消息 + """ + return error_info.get("user_message", error_info.get("message", "未知错误")) + + @staticmethod + def is_retryable_error(error_info: Dict[str, Any]) -> bool: + """ + 判断错误是否可以重试 + + Args: + error_info: 错误信息字典 + + Returns: + 是否可以重试 + """ + status_code = error_info.get("status_code") + + # 网络错误通常可以重试 + if error_info.get("category") == "network": + # 401, 403 等认证错误不应该重试 + if status_code in [401, 403]: + return False + # 429 (频率限制) 可以重试,但需要等待 + # 5xx 服务器错误可以重试 + return status_code is None or status_code >= 429 + + # 解析错误通常不需要重试 + if error_info.get("category") == "parsing": + return False + + # 其他错误可以尝试重试 + return True + + @staticmethod + def format_simple_error(service_name: str, message: str) -> Dict[str, Any]: + """ + 格式化简单错误信息 + + Args: + service_name: 服务名称 + message: 错误消息 + + Returns: + 格式化的错误信息 + """ + return { + "error": True, + "service": service_name, + "message": message, + "user_message": message + } \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/example.py b/vertex_flow/plugins/crypto_trading/example.py new file mode 100644 index 0000000..ad7fa54 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/example.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python3 +""" +Crypto Trading Plugin Example + +This example demonstrates how to use the crypto trading plugin for quantitative trading. +""" + +import os +import sys +import time +from typing import Dict, Any + +# Add the plugin to Python path +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Load environment variables from .env file +try: + from dotenv import load_dotenv + load_dotenv() + print("✅ Loaded .env file") +except ImportError: + print("⚠️ python-dotenv not installed. Install with: uv add python-dotenv") + print(" Environment variables will be loaded from system environment only.") +except Exception as e: + print(f"⚠️ Could not load .env file: {e}") + +from config import CryptoTradingConfig +from client import CryptoTradingClient +from trading import TradingEngine +from indicators import TechnicalIndicators +from position_metrics import PositionMetrics + + +def setup_config_example(): + """Example of setting up configuration programmatically""" + config = CryptoTradingConfig() + + # Set OKX configuration (replace with your actual credentials) + config.set_okx_config( + api_key="your_okx_api_key", + secret_key="your_okx_secret_key", + passphrase="your_okx_passphrase", + sandbox=True # Use sandbox for testing + ) + + # Set Binance configuration (replace with your actual credentials) + config.set_binance_config( + api_key="your_binance_api_key", + secret_key="your_binance_secret_key", + sandbox=True # Use sandbox for testing + ) + + # Adjust trading parameters + #config.trading_config.default_symbol = "BTC-USDT" + config.trading_config.default_symbol = "PUMP-USDT" + config.trading_config.risk_percentage = 0.01 # 1% risk per trade + config.trading_config.max_position_size = 100.0 # Max $100 per trade + + return config + + +def get_symbol_for_exchange(config: CryptoTradingConfig, exchange: str) -> str: + """ + Get the symbol formatted for the specific exchange + + Args: + config: Trading configuration + exchange: Exchange name ('okx' or 'binance') + + Returns: + Symbol formatted for the exchange + """ + default_symbol = config.trading_config.default_symbol + + # If the symbol is already in the correct format for the exchange, return it + if exchange == "okx": + # OKX uses format like "BTC-USDT" + if "-" in default_symbol: + return default_symbol + else: + # Convert from Binance format (BTCUSDT) to OKX format (BTC-USDT) + # This is a simple conversion, might need adjustment for other symbols + if default_symbol.endswith("USDT"): + base = default_symbol[:-4] + return f"{base}-USDT" + elif default_symbol.endswith("USDC"): + base = default_symbol[:-4] + return f"{base}-USDC" + else: + return default_symbol + else: + # Binance uses format like "BTCUSDT" + if "-" not in default_symbol: + return default_symbol + else: + # Convert from OKX format (BTC-USDT) to Binance format (BTCUSDT) + return default_symbol.replace("-", "") + + +def basic_usage_example(): + """Basic usage example""" + print("=== Crypto Trading Plugin Basic Usage Example ===\n") + + # Initialize configuration (will load from environment variables and .env file) + config = CryptoTradingConfig() + client = CryptoTradingClient(config) + + # Display configuration status + print("📋 Configuration Status:") + print(f" OKX configured: {'✅' if config.okx_config else '❌'}") + print(f" Binance configured: {'✅' if config.binance_config else '❌'}") + + # Check available exchanges + exchanges = client.get_available_exchanges() + print(f"\n🏢 Available exchanges: {exchanges}") + + if not exchanges: + print("\n❌ No exchanges configured. Please set up your API credentials.") + print("\n📝 To configure exchanges:") + print(" 1. Copy .env.example to .env") + print(" 2. Fill in your API credentials") + print(" 3. Set sandbox=true for testing") + return + + # Use the first available exchange for examples + exchange = exchanges[0] + symbol = get_symbol_for_exchange(config, exchange) + + print(f"\n🔄 Using exchange: {exchange}") + print(f"📊 Trading symbol: {symbol}") + + # Get account information + print("\n--- Account Information ---") + account_info = client.get_account_info(exchange) + if "error" not in account_info: + print(f"💰 Account balances: {account_info.get('balances', {})}") + else: + print(f"❌ Error getting account info: {account_info['error']}") + + # Get trading fees + print("\n--- Trading Fees ---") + fees = client.get_trading_fees(exchange, symbol) + print(f"📈 Maker fee: {fees.get('maker_fee', 'N/A')}") + print(f"📉 Taker fee: {fees.get('taker_fee', 'N/A')}") + + # Get ticker information + print("\n--- Market Data ---") + ticker = client.get_ticker(exchange, symbol) + if "error" not in ticker: + print(f"💵 Current price: ${ticker.get('price', 'N/A')}") + print(f"📊 24h volume: {ticker.get('volume', 'N/A')}") + print(f"📈 24h change: {ticker.get('change', 'N/A')}%") + else: + # 从错误字典中获取用户友好的错误消息 + error_msg = ticker.get('user_message', ticker.get('message', 'Unknown error')) + print(f"❌ Error getting ticker: {error_msg}") + + +def technical_analysis_example(): + """Technical analysis example""" + print("\n=== Technical Analysis Example ===\n") + + config = CryptoTradingConfig() + client = CryptoTradingClient(config) + + exchanges = client.get_available_exchanges() + if not exchanges: + print("No exchanges configured.") + return + + exchange = exchanges[0] + symbol = get_symbol_for_exchange(config, exchange) + + # Get klines data + print(f"Getting klines data for {symbol} on {exchange}...") + klines = client.get_klines(exchange, symbol, "1h", 100) + + if not klines: + print("Failed to get klines data") + return + + print(f"Retrieved {len(klines)} klines") + + # Calculate technical indicators + print("\n--- Technical Indicators ---") + indicators = TechnicalIndicators.calculate_all_indicators(klines) + + if "error" not in indicators: + print(f"Current price: ${indicators.get('current_price', 'N/A')}") + print(f"RSI (14): {indicators.get('rsi', 'N/A')}") + print(f"SMA (20): ${indicators.get('sma_20', 'N/A')}") + print(f"EMA (12): ${indicators.get('ema_12', 'N/A')}") + + if 'macd' in indicators: + macd = indicators['macd'] + print(f"MACD: {macd.get('macd', 'N/A')}") + print(f"MACD Signal: {macd.get('signal', 'N/A')}") + + if 'bollinger_bands' in indicators: + bb = indicators['bollinger_bands'] + print(f"Bollinger Upper: ${bb.get('upper', 'N/A')}") + print(f"Bollinger Lower: ${bb.get('lower', 'N/A')}") + else: + print(f"Error calculating indicators: {indicators['error']}") + + # Generate trading signals + print("\n--- Trading Signals ---") + signals = TechnicalIndicators.get_trading_signals(indicators) + + for indicator, signal in signals.items(): + print(f"{indicator.upper()}: {signal}") + + +def trading_example(): + """Trading example (DEMO ONLY - DO NOT USE WITH REAL MONEY)""" + print("\n=== Trading Example (DEMO ONLY) ===\n") + print("WARNING: This is for demonstration only. Do not use with real money!") + + config = CryptoTradingConfig() + client = CryptoTradingClient(config) + trading_engine = TradingEngine(client) + + exchanges = client.get_available_exchanges() + if not exchanges: + print("No exchanges configured.") + return + + exchange = exchanges[0] + symbol = get_symbol_for_exchange(config, exchange) + + # Get trading summary + print(f"Getting trading summary for {symbol} on {exchange}...") + summary = trading_engine.get_trading_summary(exchange, symbol) + + if "error" not in summary: + print(f"\nMarket Price: ${summary['market_data'].get('price', 'N/A')}") + print(f"Overall Signal: {summary['technical_analysis']['overall_signal']}") + print(f"Recommended Position Size: ${summary['risk_management']['recommended_position_size_usdt']}") + + # Show individual signals + signals = summary['technical_analysis']['signals'] + print("\nIndividual Signals:") + for indicator, signal in signals.items(): + if indicator != 'overall': + print(f" {indicator}: {signal}") + else: + print(f"Error getting trading summary: {summary['error']}") + + # Example of manual trading (commented out for safety) + """ + # UNCOMMENT ONLY FOR TESTING WITH SANDBOX/TESTNET + + # Manual buy order example + print("\n--- Manual Buy Order Example ---") + buy_result = trading_engine.buy_market(exchange, symbol, 10.0) # $10 worth + print(f"Buy order result: {buy_result}") + + # Manual sell order example + print("\n--- Manual Sell Order Example ---") + sell_result = trading_engine.sell_market(exchange, symbol, 0.001) # 0.001 BTC + print(f"Sell order result: {sell_result}") + + # Auto trading based on signals + print("\n--- Auto Trading Example ---") + auto_trade_result = trading_engine.auto_trade_by_signals(exchange, symbol, 10.0) + print(f"Auto trade result: {auto_trade_result}") + """ + + +def risk_management_example(): + """Risk management example""" + print("\n=== Risk Management Example ===\n") + + config = CryptoTradingConfig() + client = CryptoTradingClient(config) + trading_engine = TradingEngine(client) + + exchanges = client.get_available_exchanges() + if not exchanges: + print("No exchanges configured.") + return + + exchange = exchanges[0] + symbol = get_symbol_for_exchange(config, exchange) + + # Calculate position size + position_size = trading_engine.calculate_position_size(exchange, symbol) + print(f"Recommended position size: ${position_size}") + + # Get current price for stop loss/take profit calculation + ticker = client.get_ticker(exchange, symbol) + if "error" not in ticker: + current_price = ticker["price"] + + # Calculate stop loss and take profit for buy order + sl_tp_buy = trading_engine.calculate_stop_loss_take_profit(current_price, "buy") + print(f"\nFor BUY at ${current_price}:") + print(f"Stop Loss: ${sl_tp_buy['stop_loss']}") + print(f"Take Profit: ${sl_tp_buy['take_profit']}") + + # Calculate stop loss and take profit for sell order + sl_tp_sell = trading_engine.calculate_stop_loss_take_profit(current_price, "sell") + print(f"\nFor SELL at ${current_price}:") + print(f"Stop Loss: ${sl_tp_sell['stop_loss']}") + print(f"Take Profit: ${sl_tp_sell['take_profit']}") + + +def positions_example(): + """Positions query example""" + print("\n=== Positions Query Example ===\n") + + config = CryptoTradingConfig() + client = CryptoTradingClient(config) + + exchanges = client.get_available_exchanges() + if not exchanges: + print("No exchanges configured.") + return + + # 查询所有交易所的持仓 + print("=== All Exchanges Positions ===") + all_positions = client.get_all_positions() + + if "error" in all_positions: + print(f"Error getting all positions: {all_positions['error']}") + return + + for exchange_name, positions in all_positions.get("all_exchanges", {}).items(): + print(f"\n--- {exchange_name.upper()} Exchange ---") + + # 现货持仓 + spot_positions = positions.get("spot", {}) + if "error" in spot_positions: + print(f"Spot positions error: {spot_positions['error']}") + else: + # 修复:使用正确的数据字段 + spot_data = spot_positions.get("data", []) + if spot_data: + print("Spot Positions:") + for position in spot_data: + currency = position['currency'] + balance = position['balance'] + available = position['available'] + frozen = position['frozen'] + print(f" {currency}: Available={available:.8f}, " + f"Frozen={frozen:.8f}, Total={balance:.8f}") + else: + print("No spot positions found") + + # 合约持仓 + futures_positions = positions.get("futures", {}) + if "error" in futures_positions: + print(f"Futures positions error: {futures_positions['error']}") + else: + # 修复:使用正确的数据字段 + futures_data = futures_positions.get("data", []) + if futures_data: + print("Futures Positions:") + for position in futures_data: + symbol = position['symbol'] + side = position['side'] + size = position['size'] + entry_price = position.get('entry_price', 0) + pnl = position.get('unrealized_pnl', 0) + pnl_str = f"+{pnl:.2f}" if pnl >= 0 else f"{pnl:.2f}" + print(f" {symbol}: Side={side}, Size={size:.8f}, " + f"Entry=${entry_price:.4f}, PnL=${pnl_str}") + else: + print("No futures positions found") + + # 查询单个交易所的详细持仓 + print(f"\n=== Detailed Positions for {exchanges[0].upper()} ===") + exchange = exchanges[0] + + # 现货持仓详情 + spot_positions = client.get_spot_positions(exchange) + print("\nSpot Positions Detail:") + if "error" in spot_positions: + print(f"Error: {spot_positions['error']}") + else: + # 修复:使用正确的数据字段 + positions_data = spot_positions.get("data", []) + if positions_data: + for position in positions_data: + currency = position['currency'] + balance = position['balance'] + available = position['available'] + frozen = position['frozen'] + print(f"Currency: {currency}") + print(f" Available: {available:.8f}") + print(f" Frozen: {frozen:.8f}") + print(f" Total: {balance:.8f}") + print() + else: + print("No spot positions") + + # 合约持仓详情 + futures_positions = client.get_futures_positions(exchange) + print("Futures Positions Detail:") + if "error" in futures_positions: + print(f"Error: {futures_positions['error']}") + else: + # 修复:使用正确的数据字段 + positions_data = futures_positions.get("data", []) + if positions_data: + for position in positions_data: + symbol = position['symbol'] + side = position['side'] + size = position['size'] + entry_price = position.get('entry_price', 0) + mark_price = position.get('mark_price', 0) + pnl = position.get('unrealized_pnl', 0) + leverage = position.get('leverage', 'N/A') + + print(f"Symbol: {symbol}") + print(f" Side: {side}") + print(f" Size: {size:.8f}") + print(f" Entry Price: ${entry_price:.4f}") + print(f" Mark Price: ${mark_price:.4f}") + print(f" Unrealized PnL: ${pnl:.2f}") + if leverage != 'N/A': + print(f" Leverage: {leverage}x") + print() + else: + print("No futures positions") + + +def futures_metrics_example(): + """合约持仓指标计算示例""" + print("\n=== 合约持仓指标计算示例 ===\n") + + config = CryptoTradingConfig() + client = CryptoTradingClient(config) + + exchanges = client.get_available_exchanges() + if not exchanges: + print("No exchanges configured.") + return + + # 收集所有交易所的合约持仓 + all_futures_positions = [] + total_balance = 0 + + for exchange in exchanges: + print(f"\n--- 分析 {exchange.upper()} 合约持仓指标 ---") + + # 获取合约持仓 + futures_positions = client.get_futures_positions(exchange) + + if "error" in futures_positions: + print(f"❌ 获取 {exchange} 合约持仓失败: {futures_positions['error']}") + continue + + positions_data = futures_positions.get("data", []) + + if not positions_data: + print(f"📭 {exchange} 暂无合约持仓") + continue + + print(f"📊 找到 {len(positions_data)} 个合约持仓") + + # 计算每个持仓的指标 + for position in positions_data: + metrics = PositionMetrics.calculate_position_metrics(position, exchange) + if "error" not in metrics: + all_futures_positions.append(metrics) + print(PositionMetrics.format_metrics_display(metrics)) + + # 尝试获取账户余额(用于计算资产利用率) + try: + account_info = client.get_balance(exchange) + if "error" not in account_info and "total_balance" in account_info: + total_balance += account_info["total_balance"] + except: + pass + + # 计算投资组合综合指标 + if all_futures_positions: + print("\n" + "="*60) + print("🎯 计算投资组合综合指标...") + print("="*60) + + portfolio_metrics = PositionMetrics.calculate_portfolio_metrics( + all_futures_positions, total_balance + ) + + if "error" not in portfolio_metrics: + print(PositionMetrics.format_metrics_display(portfolio_metrics)) + + # 风险提醒 + print("\n⚠️ 风险提醒:") + risk_dist = portfolio_metrics.get('风险等级分布', {}) + + if risk_dist.get('extreme', 0) > 0: + print(f"🔴 发现 {risk_dist['extreme']} 个极高风险持仓,建议立即关注!") + + if risk_dist.get('high', 0) > 0: + print(f"🟠 发现 {risk_dist['high']} 个高风险持仓,请密切监控") + + # 杠杆提醒 + avg_leverage = float(portfolio_metrics.get('平均杠杆', '0x').replace('x', '')) + if avg_leverage > 10: + print(f"⚡ 平均杠杆较高 ({avg_leverage:.1f}x),请注意风险控制") + + # 盈亏提醒 + total_pnl_pct = float(portfolio_metrics.get('总盈亏率', '0%').replace('%', '')) + if total_pnl_pct < -20: + print(f"📉 总盈亏率为 {total_pnl_pct:.1f}%,建议考虑止损策略") + elif total_pnl_pct > 50: + print(f"📈 总盈亏率为 {total_pnl_pct:.1f}%,建议考虑止盈策略") + else: + print(f"❌ 计算投资组合指标失败: {portfolio_metrics['error']}") + else: + print("\n📭 未找到任何合约持仓,无法计算指标") + + +def main(): + """Main function to run all examples""" + print("🚀 Crypto Trading Plugin Examples") + print("=" * 50) + + try: + # Basic usage + basic_usage_example() + + # Technical analysis + technical_analysis_example() + + # Trading example + trading_example() + + # Risk management + risk_management_example() + + # Positions query + positions_example() + + # Futures metrics calculation + futures_metrics_example() + + except Exception as e: + print(f"Error running examples: {e}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/exchanges.py b/vertex_flow/plugins/crypto_trading/exchanges.py new file mode 100644 index 0000000..927dcad --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/exchanges.py @@ -0,0 +1,497 @@ +""" +Exchange API clients for OKX and Binance +""" + +import hashlib +import hmac +import base64 +import time +from abc import ABC, abstractmethod +from typing import Dict, List, Any, Optional +import requests +import json +from urllib.parse import urlencode +from error_handler import ErrorHandler + +try: + from .config import ExchangeConfig +except ImportError: + from config import ExchangeConfig + + +class BaseExchange(ABC): + """Base class for exchange API clients""" + + def __init__(self, config: ExchangeConfig): + self.config = config + self.session = requests.Session() + + @abstractmethod + def get_account_info(self) -> Dict[str, Any]: + """Get account information""" + pass + + @abstractmethod + def get_trading_fees(self, symbol: str) -> Dict[str, float]: + """Get trading fees for a symbol""" + pass + + @abstractmethod + def get_ticker(self, symbol: str) -> Dict[str, Any]: + """Get ticker information""" + pass + + @abstractmethod + def get_klines(self, symbol: str, interval: str, limit: int = 100) -> List[List]: + """Get kline/candlestick data""" + pass + + @abstractmethod + def place_order(self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None) -> Dict[str, Any]: + """Place an order""" + pass + + @abstractmethod + def get_spot_positions(self) -> Dict[str, Any]: + """Get spot positions/balances""" + pass + + @abstractmethod + def get_futures_positions(self) -> Dict[str, Any]: + """Get futures positions""" + pass + + +class OKXClient(BaseExchange): + """OKX exchange API client""" + + def __init__(self, config: ExchangeConfig): + super().__init__(config) + # 根据沙盒模式选择API URL + if config.sandbox: + self.base_url = "https://www.okx.com" # OKX沙盒环境使用相同URL + else: + self.base_url = config.base_url or "https://www.okx.com" + self.api_url = f"{self.base_url}/api/v5" + + def _generate_signature(self, timestamp: str, method: str, request_path: str, body: str = "") -> str: + """Generate OKX API signature""" + message = timestamp + method + request_path + body + signature = base64.b64encode( + hmac.new( + self.config.secret_key.encode('utf-8'), + message.encode('utf-8'), + hashlib.sha256 + ).digest() + ).decode('utf-8') + return signature + + def _get_server_time(self) -> str: + """获取OKX服务器时间""" + try: + response = self.session.get(f"{self.api_url}/public/time") + response.raise_for_status() + data = response.json() + if data.get("code") == "0" and data.get("data"): + # OKX返回的时间戳是毫秒级的字符串 + timestamp_ms = data["data"][0]["ts"] + # 转换为ISO8601格式 + import datetime + dt = datetime.datetime.fromtimestamp(int(timestamp_ms) / 1000, tz=datetime.timezone.utc) + return dt.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' + except Exception as e: + print(f"获取服务器时间失败: {e}") + + # 如果获取服务器时间失败,使用本地UTC时间 + import datetime + return datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' + + def _make_request(self, method: str, endpoint: str, params: Optional[Dict] = None, data: Optional[Dict] = None) -> Dict[str, Any]: + """Make authenticated request to OKX API""" + # 使用服务器时间确保时间戳准确 + timestamp = self._get_server_time() + # endpoint已经包含完整路径,不需要额外处理 + request_path = endpoint + + if params: + request_path += "?" + urlencode(params) + + body = json.dumps(data, separators=(',', ':')) if data else "" + signature = self._generate_signature(timestamp, method.upper(), request_path, body) + + headers = { + "OK-ACCESS-KEY": self.config.api_key, + "OK-ACCESS-SIGN": signature, + "OK-ACCESS-TIMESTAMP": timestamp, + "OK-ACCESS-PASSPHRASE": self.config.passphrase, + "Content-Type": "application/json" + } + + url = self.base_url + endpoint + + try: + if method.upper() == "GET": + response = self.session.get(url, headers=headers, params=params) + else: + response = self.session.post(url, headers=headers, json=data) + + response.raise_for_status() + + # 检查响应内容是否为空 + if not response.text.strip(): + return {"error": "Empty response from server"} + + return response.json() + except requests.exceptions.RequestException as e: + error_info = ErrorHandler.format_api_error("OKX", e, getattr(e, 'response', None)) + print(f"❌ OKX API请求失败: {ErrorHandler.get_user_friendly_message(error_info)}") + return error_info + except json.JSONDecodeError as e: + error_info = ErrorHandler.format_api_error("OKX", e, response if 'response' in locals() else None) + print(f"❌ OKX数据解析失败: {ErrorHandler.get_user_friendly_message(error_info)}") + return error_info + except Exception as e: + error_info = ErrorHandler.format_api_error("OKX", e) + print(f"❌ OKX未知错误: {ErrorHandler.get_user_friendly_message(error_info)}") + return error_info + + def get_account_info(self) -> Dict[str, Any]: + """Get OKX account information""" + return self._make_request("GET", "/account/balance") + + def get_trading_fees(self, symbol: str) -> Dict[str, float]: + """Get OKX trading fees""" + response = self._make_request("GET", "/account/trade-fee", {"instType": "SPOT", "instId": symbol}) + if response.get("data"): + fee_data = response["data"][0] + return { + "maker_fee": float(fee_data.get("maker", 0)), + "taker_fee": float(fee_data.get("taker", 0)) + } + return {"maker_fee": 0.001, "taker_fee": 0.001} # Default fees + + def get_ticker(self, symbol: str) -> Dict[str, Any]: + """Get OKX ticker information""" + response = self._make_request("GET", "/market/ticker", {"instId": symbol}) + if response.get("data"): + ticker = response["data"][0] + return { + "symbol": ticker["instId"], + "price": float(ticker["last"]), + "bid": float(ticker["bidPx"]), + "ask": float(ticker["askPx"]), + "volume": float(ticker["vol24h"]), + "change": float(ticker["sodUtc8"]) + } + # 如果没有数据或请求失败,返回错误信息 + if "error" in response: + # 如果response本身就是错误信息字典,直接返回 + return response + else: + return {"error": "Failed to get ticker data"} + + def get_klines(self, symbol: str, interval: str, limit: int = 100) -> List[List]: + """Get OKX kline data""" + # OKX interval mapping + interval_map = { + "1m": "1m", "5m": "5m", "15m": "15m", "30m": "30m", + "1h": "1H", "4h": "4H", "1d": "1D", "1w": "1W" + } + + okx_interval = interval_map.get(interval, "1m") + response = self._make_request("GET", "/market/candles", { + "instId": symbol, + "bar": okx_interval, + "limit": str(limit) + }) + + if response.get("data"): + return [[float(x) for x in candle] for candle in response["data"]] + return [] + + def place_order(self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None) -> Dict[str, Any]: + """Place order on OKX""" + order_data = { + "instId": symbol, + "tdMode": "cash", + "side": side.lower(), + "ordType": "market" if order_type.lower() == "market" else "limit", + "sz": str(quantity) + } + + if price and order_type.lower() == "limit": + order_data["px"] = str(price) + + return self._make_request("POST", "/api/v5/trade/order", data=order_data) + + def get_order_status(self, order_id: str, symbol: str) -> Dict[str, Any]: + """查询订单状态""" + try: + params = { + "ordId": order_id, + "instId": symbol + } + + response = self._make_request("GET", "/api/v5/trade/order", params) + + if response.get("code") == "0" and response.get("data"): + order_info = response["data"][0] + return { + "success": True, + "data": order_info + } + else: + return {"error": f"OKX API error: {response.get('msg', 'Unknown error')}"} + + except Exception as e: + return {"error": f"Failed to get OKX order status: {str(e)}"} + + def get_spot_positions(self) -> Dict[str, Any]: + """Get spot account balance""" + try: + response = self._make_request("GET", "/api/v5/account/balance") + if response.get("code") == "0" and response.get("data"): + positions = [] + for account in response["data"]: + for detail in account.get("details", []): + if float(detail.get("eq", 0) or 0) > 0: # 只返回有余额的币种 + positions.append({ + "currency": detail["ccy"], + "balance": float(detail.get("eq", 0) or 0), + "available": float(detail.get("availEq", 0) or 0), + "frozen": float(detail.get("frozenBal", 0) or 0) + }) + return { + "success": True, + "data": positions + } + else: + return {"error": f"OKX API error: {response.get('msg', 'Unknown error')}"} + except Exception as e: + return {"error": f"Failed to get OKX spot positions: {str(e)}"} + + def get_futures_positions(self) -> Dict[str, Any]: + """Get futures positions""" + try: + response = self._make_request("GET", "/api/v5/account/positions") + if response.get("code") == "0" and response.get("data"): + positions = [] + for position in response["data"]: + pos_size = position.get("pos", "0") + if pos_size and pos_size != "0" and float(pos_size) != 0: # 只显示有持仓的合约 + positions.append({ + "symbol": position["instId"], + "side": position["posSide"], + "size": float(pos_size), + "notional": float(position.get("notionalUsd", 0) or 0), + "unrealized_pnl": float(position.get("upl", 0) or 0), + "margin": float(position.get("margin", 0) or 0) + }) + return { + "success": True, + "data": positions + } + else: + return {"error": f"OKX API error: {response.get('msg', 'Unknown error')}"} + + except Exception as e: + return {"error": f"Failed to get OKX futures positions: {str(e)}"} + + +class BinanceClient(BaseExchange): + """Binance exchange API client""" + + def __init__(self, config: ExchangeConfig): + super().__init__(config) + self.base_url = config.base_url or ("https://api.binance.com" if not config.sandbox else "https://testnet.binance.vision") + self.api_url = f"{self.base_url}/api/v3" + + def _generate_signature(self, query_string: str) -> str: + """Generate Binance API signature""" + return hmac.new( + self.config.secret_key.encode('utf-8'), + query_string.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + + def _make_request(self, method: str, endpoint: str, params: Optional[Dict] = None, signed: bool = False) -> Dict[str, Any]: + """Make request to Binance API""" + params = params or {} + + if signed: + params['timestamp'] = int(time.time() * 1000) + query_string = urlencode(params) + signature = self._generate_signature(query_string) + params['signature'] = signature + + headers = {"X-MBX-APIKEY": self.config.api_key} if signed else {} + url = self.api_url + endpoint + + try: + if method.upper() == "GET": + response = self.session.get(url, headers=headers, params=params) + else: + response = self.session.post(url, headers=headers, params=params) + + response.raise_for_status() + return response.json() + + except requests.exceptions.RequestException as e: + error_info = ErrorHandler.format_api_error("Binance", e, getattr(e, 'response', None)) + print(f"❌ Binance API请求失败: {ErrorHandler.get_user_friendly_message(error_info)}") + return error_info + except json.JSONDecodeError as e: + error_info = ErrorHandler.format_api_error("Binance", e, response if 'response' in locals() else None) + print(f"❌ Binance数据解析失败: {ErrorHandler.get_user_friendly_message(error_info)}") + return error_info + except Exception as e: + error_info = ErrorHandler.format_api_error("Binance", e) + print(f"❌ Binance未知错误: {ErrorHandler.get_user_friendly_message(error_info)}") + return error_info + + def get_account_info(self) -> Dict[str, Any]: + """Get Binance account information""" + return self._make_request("GET", "/account", signed=True) + + def get_trading_fees(self, symbol: str) -> Dict[str, float]: + """Get Binance trading fees""" + response = self._make_request("GET", "/account", signed=True) + maker_commission = response.get("makerCommission", 10) / 10000 + taker_commission = response.get("takerCommission", 10) / 10000 + + return { + "maker_fee": maker_commission, + "taker_fee": taker_commission + } + + def get_ticker(self, symbol: str) -> Dict[str, Any]: + """Get Binance ticker information""" + try: + response = self._make_request("GET", "/ticker/24hr", {"symbol": symbol}) + return { + "symbol": response["symbol"], + "price": float(response["lastPrice"]), + "bid": float(response["bidPrice"]), + "ask": float(response["askPrice"]), + "volume": float(response["volume"]), + "change": float(response["priceChangePercent"]) + } + except Exception as e: + return {"error": f"Failed to get ticker: {str(e)}"} + + def get_klines(self, symbol: str, interval: str, limit: int = 100) -> List[List]: + """Get Binance kline data""" + # Binance interval mapping + interval_map = { + "1m": "1m", "5m": "5m", "15m": "15m", "30m": "30m", + "1h": "1h", "4h": "4h", "1d": "1d", "1w": "1w" + } + + binance_interval = interval_map.get(interval, "1m") + response = self._make_request("GET", "/klines", { + "symbol": symbol, + "interval": binance_interval, + "limit": limit + }) + + return [[float(x) for x in candle[:6]] for candle in response] + + def place_order(self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None) -> Dict[str, Any]: + """Place order on Binance""" + order_params = { + "symbol": symbol, + "side": side.upper(), + "type": order_type.upper(), + "quantity": quantity + } + + if price and order_type.upper() == "LIMIT": + order_params["price"] = price + order_params["timeInForce"] = "GTC" + + return self._make_request("POST", "/order", order_params, signed=True) + + def get_spot_positions(self) -> Dict[str, Any]: + """Get spot positions/balances from Binance""" + try: + response = self._make_request("GET", "/account", signed=True) + + positions = {} + balances = response.get("balances", []) + + for balance in balances: + asset = balance.get("asset", "") + free = float(balance.get("free", "0")) + locked = float(balance.get("locked", "0")) + total = free + locked + + if total > 0: # 只显示有余额的币种 + positions[asset] = { + "currency": asset, + "available": free, + "frozen": locked, + "total": total + } + + return { + "exchange": "binance", + "type": "spot", + "positions": positions, + "timestamp": time.time() + } + + except Exception as e: + return {"error": f"Failed to get Binance spot positions: {str(e)}"} + + def get_futures_positions(self) -> Dict[str, Any]: + """Get futures positions from Binance""" + try: + # Binance futures API endpoint + futures_base_url = "https://fapi.binance.com/fapi/v2" + + # 构建请求参数 + params = {"timestamp": int(time.time() * 1000)} + query_string = urlencode(params) + signature = self._generate_signature(query_string) + params["signature"] = signature + + headers = { + "X-MBX-APIKEY": self.config.api_key + } + + url = f"{futures_base_url}/positionRisk" + response = self.session.get(url, headers=headers, params=params) + response.raise_for_status() + positions_data = response.json() + + positions = {} + + for position in positions_data: + symbol = position.get("symbol", "") + position_amt = float(position.get("positionAmt", "0")) + + if position_amt != 0: # 只显示有持仓的合约 + positions[symbol] = { + "symbol": symbol, + "side": "long" if position_amt > 0 else "short", + "size": abs(position_amt), + "contracts": position_amt, + "notional": float(position.get("notional", "0")), + "entry_price": float(position.get("entryPrice", "0")), + "mark_price": float(position.get("markPrice", "0")), + "unrealized_pnl": float(position.get("unRealizedProfit", "0")), + "percentage": float(position.get("percentage", "0")), + "leverage": float(position.get("leverage", "0")), + "margin_type": position.get("marginType", ""), + "isolated_margin": float(position.get("isolatedMargin", "0")) + } + + return { + "exchange": "binance", + "type": "futures", + "positions": positions, + "timestamp": time.time() + } + + except Exception as e: + return {"error": f"Failed to get Binance futures positions: {str(e)}"} \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/indicators.py b/vertex_flow/plugins/crypto_trading/indicators.py new file mode 100644 index 0000000..d7f4ec6 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/indicators.py @@ -0,0 +1,332 @@ +""" +Technical indicators calculation module for crypto trading +""" + +import pandas as pd +import numpy as np +from typing import List, Dict, Any, Optional, Tuple + + +class TechnicalIndicators: + """Technical indicators calculator""" + + @staticmethod + def prepare_dataframe(klines: List[List]) -> pd.DataFrame: + """ + Convert klines data to pandas DataFrame + + Args: + klines: List of kline data from exchange API + + Returns: + DataFrame with OHLCV data + """ + if not klines: + return pd.DataFrame() + + # Handle different exchange formats + # OKX: [timestamp, open, high, low, close, volume, volCcy, volCcyQuote, confirm] + # Binance: [timestamp, open, high, low, close, volume, ...] + + # Extract only the first 6 columns we need: timestamp, open, high, low, close, volume + processed_klines = [] + for kline in klines: + if len(kline) >= 6: + processed_klines.append(kline[:6]) + else: + # Skip incomplete data + continue + + if not processed_klines: + return pd.DataFrame() + + df = pd.DataFrame(processed_klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + df.set_index('timestamp', inplace=True) + + # Convert to numeric + for col in ['open', 'high', 'low', 'close', 'volume']: + df[col] = pd.to_numeric(df[col]) + + return df.sort_index() + + @staticmethod + def sma(data: pd.Series, period: int) -> pd.Series: + """Simple Moving Average""" + return data.rolling(window=period).mean() + + @staticmethod + def ema(data: pd.Series, period: int) -> pd.Series: + """Exponential Moving Average""" + return data.ewm(span=period).mean() + + @staticmethod + def rsi(data: pd.Series, period: int = 14) -> pd.Series: + """Relative Strength Index""" + delta = data.diff() + gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() + loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() + rs = gain / loss + rsi = 100 - (100 / (1 + rs)) + return rsi + + @staticmethod + def macd(data: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> Dict[str, pd.Series]: + """MACD (Moving Average Convergence Divergence)""" + ema_fast = TechnicalIndicators.ema(data, fast) + ema_slow = TechnicalIndicators.ema(data, slow) + macd_line = ema_fast - ema_slow + signal_line = TechnicalIndicators.ema(macd_line, signal) + histogram = macd_line - signal_line + + return { + 'macd': macd_line, + 'signal': signal_line, + 'histogram': histogram + } + + @staticmethod + def bollinger_bands(data: pd.Series, period: int = 20, std_dev: float = 2) -> Dict[str, pd.Series]: + """Bollinger Bands""" + sma = TechnicalIndicators.sma(data, period) + std = data.rolling(window=period).std() + + return { + 'upper': sma + (std * std_dev), + 'middle': sma, + 'lower': sma - (std * std_dev) + } + + @staticmethod + def stochastic(high: pd.Series, low: pd.Series, close: pd.Series, + k_period: int = 14, d_period: int = 3) -> Dict[str, pd.Series]: + """Stochastic Oscillator""" + lowest_low = low.rolling(window=k_period).min() + highest_high = high.rolling(window=k_period).max() + + k_percent = 100 * ((close - lowest_low) / (highest_high - lowest_low)) + d_percent = k_percent.rolling(window=d_period).mean() + + return { + 'k': k_percent, + 'd': d_percent + } + + @staticmethod + def atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series: + """Average True Range""" + high_low = high - low + high_close = np.abs(high - close.shift()) + low_close = np.abs(low - close.shift()) + + true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1) + return true_range.rolling(window=period).mean() + + @staticmethod + def williams_r(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series: + """Williams %R""" + highest_high = high.rolling(window=period).max() + lowest_low = low.rolling(window=period).min() + + return -100 * ((highest_high - close) / (highest_high - lowest_low)) + + @staticmethod + def cci(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 20) -> pd.Series: + """Commodity Channel Index""" + typical_price = (high + low + close) / 3 + sma_tp = typical_price.rolling(window=period).mean() + mean_deviation = typical_price.rolling(window=period).apply( + lambda x: np.abs(x - x.mean()).mean() + ) + + return (typical_price - sma_tp) / (0.015 * mean_deviation) + + @staticmethod + def obv(close: pd.Series, volume: pd.Series) -> pd.Series: + """On-Balance Volume""" + obv = pd.Series(index=close.index, dtype=float) + obv.iloc[0] = volume.iloc[0] + + for i in range(1, len(close)): + if close.iloc[i] > close.iloc[i-1]: + obv.iloc[i] = obv.iloc[i-1] + volume.iloc[i] + elif close.iloc[i] < close.iloc[i-1]: + obv.iloc[i] = obv.iloc[i-1] - volume.iloc[i] + else: + obv.iloc[i] = obv.iloc[i-1] + + return obv + + @staticmethod + def support_resistance(data: pd.Series, window: int = 20) -> Dict[str, List[float]]: + """Find support and resistance levels""" + highs = data.rolling(window=window, center=True).max() + lows = data.rolling(window=window, center=True).min() + + resistance_levels = [] + support_levels = [] + + for i in range(window, len(data) - window): + if data.iloc[i] == highs.iloc[i]: + resistance_levels.append(data.iloc[i]) + if data.iloc[i] == lows.iloc[i]: + support_levels.append(data.iloc[i]) + + return { + 'resistance': sorted(list(set(resistance_levels)), reverse=True)[:5], + 'support': sorted(list(set(support_levels)))[:5] + } + + @classmethod + def calculate_all_indicators(cls, klines: List[List]) -> Dict[str, Any]: + """ + Calculate all technical indicators for given klines data + + Args: + klines: List of kline data + + Returns: + Dictionary containing all calculated indicators + """ + if not klines: + return {} + + df = cls.prepare_dataframe(klines) + if df.empty: + return {} + + indicators = {} + + try: + # Moving Averages + indicators['sma_20'] = cls.sma(df['close'], 20).iloc[-1] if len(df) >= 20 else None + indicators['sma_50'] = cls.sma(df['close'], 50).iloc[-1] if len(df) >= 50 else None + indicators['ema_12'] = cls.ema(df['close'], 12).iloc[-1] if len(df) >= 12 else None + indicators['ema_26'] = cls.ema(df['close'], 26).iloc[-1] if len(df) >= 26 else None + + # RSI + if len(df) >= 14: + rsi_values = cls.rsi(df['close'], 14) + indicators['rsi'] = rsi_values.iloc[-1] + + # MACD + if len(df) >= 26: + macd_data = cls.macd(df['close']) + indicators['macd'] = { + 'macd': macd_data['macd'].iloc[-1], + 'signal': macd_data['signal'].iloc[-1], + 'histogram': macd_data['histogram'].iloc[-1] + } + + # Bollinger Bands + if len(df) >= 20: + bb_data = cls.bollinger_bands(df['close']) + indicators['bollinger_bands'] = { + 'upper': bb_data['upper'].iloc[-1], + 'middle': bb_data['middle'].iloc[-1], + 'lower': bb_data['lower'].iloc[-1] + } + + # Stochastic + if len(df) >= 14: + stoch_data = cls.stochastic(df['high'], df['low'], df['close']) + indicators['stochastic'] = { + 'k': stoch_data['k'].iloc[-1], + 'd': stoch_data['d'].iloc[-1] + } + + # ATR + if len(df) >= 14: + atr_values = cls.atr(df['high'], df['low'], df['close']) + indicators['atr'] = atr_values.iloc[-1] + + # Williams %R + if len(df) >= 14: + williams_r_values = cls.williams_r(df['high'], df['low'], df['close']) + indicators['williams_r'] = williams_r_values.iloc[-1] + + # Support and Resistance + if len(df) >= 40: + sr_levels = cls.support_resistance(df['close']) + indicators['support_resistance'] = sr_levels + + # Current price info + indicators['current_price'] = df['close'].iloc[-1] + indicators['volume'] = df['volume'].iloc[-1] + indicators['high_24h'] = df['high'].max() + indicators['low_24h'] = df['low'].min() + + except Exception as e: + indicators['error'] = f"Error calculating indicators: {str(e)}" + + return indicators + + @classmethod + def get_trading_signals(cls, indicators: Dict[str, Any]) -> Dict[str, str]: + """ + Generate trading signals based on technical indicators + + Args: + indicators: Dictionary of calculated indicators + + Returns: + Dictionary of trading signals + """ + signals = {} + + try: + # RSI signals + if 'rsi' in indicators and indicators['rsi'] is not None: + rsi = indicators['rsi'] + if rsi > 70: + signals['rsi'] = 'SELL' + elif rsi < 30: + signals['rsi'] = 'BUY' + else: + signals['rsi'] = 'HOLD' + + # MACD signals + if 'macd' in indicators and indicators['macd'] is not None: + macd_data = indicators['macd'] + if macd_data['macd'] > macd_data['signal']: + signals['macd'] = 'BUY' + else: + signals['macd'] = 'SELL' + + # Bollinger Bands signals + if 'bollinger_bands' in indicators and 'current_price' in indicators: + bb = indicators['bollinger_bands'] + price = indicators['current_price'] + + if price > bb['upper']: + signals['bollinger_bands'] = 'SELL' + elif price < bb['lower']: + signals['bollinger_bands'] = 'BUY' + else: + signals['bollinger_bands'] = 'HOLD' + + # Stochastic signals + if 'stochastic' in indicators: + stoch = indicators['stochastic'] + if stoch['k'] > 80: + signals['stochastic'] = 'SELL' + elif stoch['k'] < 20: + signals['stochastic'] = 'BUY' + else: + signals['stochastic'] = 'HOLD' + + # Overall signal (simple majority vote) + buy_signals = sum(1 for signal in signals.values() if signal == 'BUY') + sell_signals = sum(1 for signal in signals.values() if signal == 'SELL') + + if buy_signals > sell_signals: + signals['overall'] = 'BUY' + elif sell_signals > buy_signals: + signals['overall'] = 'SELL' + else: + signals['overall'] = 'HOLD' + + except Exception as e: + signals['error'] = f"Error generating signals: {str(e)}" + + return signals \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/position_metrics.py b/vertex_flow/plugins/crypto_trading/position_metrics.py new file mode 100644 index 0000000..6f4916f --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/position_metrics.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +""" +合约持仓指标计算模块 +计算各种持仓相关的风险和收益指标 +""" + +from typing import Dict, List, Any, Optional +import math + + +class PositionMetrics: + """合约持仓指标计算类""" + + @staticmethod + def calculate_position_metrics(position: Dict[str, Any], exchange: str = "okx") -> Dict[str, Any]: + """ + 计算单个持仓的各项指标 + + Args: + position: 持仓数据 + exchange: 交易所名称 + + Returns: + 包含各项指标的字典 + """ + try: + # 基础数据提取 + symbol = position.get("symbol", "") + side = position.get("side", "") + size = float(position.get("size", 0)) + unrealized_pnl = float(position.get("unrealized_pnl", 0)) + + # 根据交易所获取不同字段 + if exchange.lower() == "okx": + notional = float(position.get("notional", 0)) + margin = float(position.get("margin", 0)) + entry_price = 0 # OKX API 没有直接提供入场价格 + mark_price = 0 # OKX API 没有直接提供标记价格 + leverage = notional / margin if margin > 0 else 0 + else: # binance + notional = float(position.get("notional", 0)) + margin = float(position.get("isolated_margin", 0)) + entry_price = float(position.get("entry_price", 0)) + mark_price = float(position.get("mark_price", 0)) + leverage = float(position.get("leverage", 0)) + + # 计算各项指标 + metrics = { + "symbol": symbol, + "side": side, + "size": size, + "notional_value": abs(notional), + "margin_used": margin, + "leverage": leverage, + "unrealized_pnl": unrealized_pnl, + "entry_price": entry_price, + "mark_price": mark_price, + } + + # 计算盈亏率 + if margin > 0: + metrics["pnl_percentage"] = (unrealized_pnl / margin) * 100 + else: + metrics["pnl_percentage"] = 0 + + # 计算价格变化率(仅当有入场价格和标记价格时) + if entry_price > 0 and mark_price > 0: + price_change = ((mark_price - entry_price) / entry_price) * 100 + if side.lower() == "short": + price_change = -price_change + metrics["price_change_percentage"] = price_change + else: + metrics["price_change_percentage"] = 0 + + # 风险等级评估 + metrics["risk_level"] = PositionMetrics._assess_risk_level( + leverage, abs(metrics["pnl_percentage"]), abs(notional) + ) + + # 持仓价值占比(需要总资产信息,这里先设为0) + metrics["position_weight"] = 0 + + return metrics + + except Exception as e: + return {"error": f"Failed to calculate metrics for position: {str(e)}"} + + @staticmethod + def calculate_portfolio_metrics(positions: List[Dict[str, Any]], total_balance: float = 0) -> Dict[str, Any]: + """ + 计算整个投资组合的指标 + + Args: + positions: 所有持仓的指标列表 + total_balance: 总资产余额 + + Returns: + 投资组合指标字典 + """ + try: + if not positions: + return {"error": "No positions to calculate"} + + # 过滤掉有错误的持仓 + valid_positions = [p for p in positions if "error" not in p] + + if not valid_positions: + return {"error": "No valid positions to calculate"} + + # 基础统计 + total_positions = len(valid_positions) + long_positions = len([p for p in valid_positions if p["side"].lower() == "long"]) + short_positions = len([p for p in valid_positions if p["side"].lower() == "short"]) + + # 盈亏统计 + total_unrealized_pnl = sum(p["unrealized_pnl"] for p in valid_positions) + profitable_positions = len([p for p in valid_positions if p["unrealized_pnl"] > 0]) + losing_positions = len([p for p in valid_positions if p["unrealized_pnl"] < 0]) + + # 保证金和名义价值统计 + total_margin = sum(p["margin_used"] for p in valid_positions) + total_notional = sum(p["notional_value"] for p in valid_positions) + + # 计算平均杠杆 + leverages = [p["leverage"] for p in valid_positions if p["leverage"] > 0] + avg_leverage = sum(leverages) / len(leverages) if leverages else 0 + + # 计算胜率 + win_rate = (profitable_positions / total_positions) * 100 if total_positions > 0 else 0 + + # 计算总盈亏率 + total_pnl_percentage = (total_unrealized_pnl / total_margin) * 100 if total_margin > 0 else 0 + + # 风险指标 + max_single_loss = min([p["unrealized_pnl"] for p in valid_positions], default=0) + max_single_gain = max([p["unrealized_pnl"] for p in valid_positions], default=0) + + # 持仓集中度(按名义价值) + if total_notional > 0: + position_weights = [(p["notional_value"] / total_notional) * 100 for p in valid_positions] + max_position_weight = max(position_weights, default=0) + + # 更新每个持仓的权重 + for i, position in enumerate(valid_positions): + position["position_weight"] = position_weights[i] + else: + max_position_weight = 0 + + # 风险等级分布 + risk_distribution = {"low": 0, "medium": 0, "high": 0, "extreme": 0} + for position in valid_positions: + risk_level = position.get("risk_level", "medium") + risk_distribution[risk_level] += 1 + + portfolio_metrics = { + "总持仓数量": total_positions, + "多头持仓": long_positions, + "空头持仓": short_positions, + "盈利持仓": profitable_positions, + "亏损持仓": losing_positions, + "胜率": f"{win_rate:.2f}%", + "总未实现盈亏": f"${total_unrealized_pnl:.2f}", + "总盈亏率": f"{total_pnl_percentage:.2f}%", + "总保证金": f"${total_margin:.2f}", + "总名义价值": f"${total_notional:.2f}", + "平均杠杆": f"{avg_leverage:.2f}x", + "最大单笔亏损": f"${max_single_loss:.2f}", + "最大单笔盈利": f"${max_single_gain:.2f}", + "最大持仓权重": f"{max_position_weight:.2f}%", + "风险等级分布": risk_distribution, + "详细持仓": valid_positions + } + + # 如果有总资产信息,计算资产利用率 + if total_balance > 0: + margin_utilization = (total_margin / total_balance) * 100 + portfolio_metrics["保证金利用率"] = f"{margin_utilization:.2f}%" + portfolio_metrics["账户总资产"] = f"${total_balance:.2f}" + + return portfolio_metrics + + except Exception as e: + return {"error": f"Failed to calculate portfolio metrics: {str(e)}"} + + @staticmethod + def _assess_risk_level(leverage: float, pnl_percentage: float, notional_value: float) -> str: + """ + 评估持仓风险等级 + + Args: + leverage: 杠杆倍数 + pnl_percentage: 盈亏百分比(绝对值) + notional_value: 名义价值 + + Returns: + 风险等级: low, medium, high, extreme + """ + risk_score = 0 + + # 杠杆风险评分 + if leverage >= 20: + risk_score += 3 + elif leverage >= 10: + risk_score += 2 + elif leverage >= 5: + risk_score += 1 + + # 盈亏风险评分 + if pnl_percentage >= 50: + risk_score += 3 + elif pnl_percentage >= 20: + risk_score += 2 + elif pnl_percentage >= 10: + risk_score += 1 + + # 持仓规模风险评分 + if notional_value >= 10000: + risk_score += 2 + elif notional_value >= 5000: + risk_score += 1 + + # 风险等级判定 + if risk_score >= 6: + return "extreme" + elif risk_score >= 4: + return "high" + elif risk_score >= 2: + return "medium" + else: + return "low" + + @staticmethod + def format_metrics_display(metrics: Dict[str, Any]) -> str: + """ + 格式化指标显示 + + Args: + metrics: 指标字典 + + Returns: + 格式化的显示字符串 + """ + if "error" in metrics: + return f"❌ 计算错误: {metrics['error']}" + + # 如果是投资组合指标 + if "总持仓数量" in metrics: + return PositionMetrics._format_portfolio_display(metrics) + else: + return PositionMetrics._format_position_display(metrics) + + @staticmethod + def _format_portfolio_display(metrics: Dict[str, Any]) -> str: + """格式化投资组合指标显示""" + display = "\n" + "="*60 + "\n" + display += "📊 投资组合综合指标\n" + display += "="*60 + "\n" + + # 基础统计 + display += f"📈 持仓概况:\n" + display += f" • 总持仓数量: {metrics['总持仓数量']}\n" + display += f" • 多头/空头: {metrics['多头持仓']}/{metrics['空头持仓']}\n" + display += f" • 盈利/亏损: {metrics['盈利持仓']}/{metrics['亏损持仓']}\n" + display += f" • 胜率: {metrics['胜率']}\n\n" + + # 盈亏指标 + display += f"💰 盈亏指标:\n" + display += f" • 总未实现盈亏: {metrics['总未实现盈亏']}\n" + display += f" • 总盈亏率: {metrics['总盈亏率']}\n" + display += f" • 最大单笔盈利: {metrics['最大单笔盈利']}\n" + display += f" • 最大单笔亏损: {metrics['最大单笔亏损']}\n\n" + + # 风险指标 + display += f"⚠️ 风险指标:\n" + display += f" • 总保证金: {metrics['总保证金']}\n" + display += f" • 总名义价值: {metrics['总名义价值']}\n" + display += f" • 平均杠杆: {metrics['平均杠杆']}\n" + display += f" • 最大持仓权重: {metrics['最大持仓权重']}\n" + + if "保证金利用率" in metrics: + display += f" • 保证金利用率: {metrics['保证金利用率']}\n" + display += f" • 账户总资产: {metrics['账户总资产']}\n" + + # 风险分布 + risk_dist = metrics['风险等级分布'] + display += f"\n🎯 风险等级分布:\n" + display += f" • 低风险: {risk_dist['low']} 个\n" + display += f" • 中风险: {risk_dist['medium']} 个\n" + display += f" • 高风险: {risk_dist['high']} 个\n" + display += f" • 极高风险: {risk_dist['extreme']} 个\n" + + return display + + @staticmethod + def _format_position_display(metrics: Dict[str, Any]) -> str: + """格式化单个持仓指标显示""" + risk_emoji = { + "low": "🟢", + "medium": "🟡", + "high": "🟠", + "extreme": "🔴" + } + + risk_level = metrics.get("risk_level", "medium") + emoji = risk_emoji.get(risk_level, "🟡") + + display = f"\n{emoji} {metrics['symbol']} ({metrics['side'].upper()})\n" + display += f" • 持仓大小: {metrics['size']:.8f}\n" + display += f" • 名义价值: ${metrics['notional_value']:.2f}\n" + display += f" • 保证金: ${metrics['margin_used']:.2f}\n" + display += f" • 杠杆: {metrics['leverage']:.2f}x\n" + display += f" • 未实现盈亏: ${metrics['unrealized_pnl']:.2f}\n" + display += f" • 盈亏率: {metrics['pnl_percentage']:.2f}%\n" + + if metrics['price_change_percentage'] != 0: + display += f" • 价格变化: {metrics['price_change_percentage']:.2f}%\n" + + if metrics['position_weight'] > 0: + display += f" • 持仓权重: {metrics['position_weight']:.2f}%\n" + + display += f" • 风险等级: {risk_level.upper()}\n" + + return display \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/requirements.txt b/vertex_flow/plugins/crypto_trading/requirements.txt new file mode 100644 index 0000000..e585193 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/requirements.txt @@ -0,0 +1,12 @@ +requests>=2.28.0 +websocket-client>=1.6.0 +pandas>=2.0.0 +numpy>=1.24.0 +ta-lib>=0.4.0 +python-dotenv>=1.0.0 +python-binance>=1.0.16 +okx>=1.0.0 +aiohttp>=3.8.0 +asyncio-throttle>=1.0.2 +cryptography>=3.4.8 +pydantic>=1.10.0 \ No newline at end of file diff --git a/vertex_flow/plugins/crypto_trading/trading.py b/vertex_flow/plugins/crypto_trading/trading.py new file mode 100644 index 0000000..d847bb3 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/trading.py @@ -0,0 +1,443 @@ +""" +Trading engine for crypto trading operations +""" + +import time +from typing import Dict, Any, List, Optional, Union +from decimal import Decimal, ROUND_DOWN + +try: + from .client import CryptoTradingClient + from .indicators import TechnicalIndicators +except ImportError: + from client import CryptoTradingClient + from indicators import TechnicalIndicators + + +class TradingEngine: + """Trading engine with risk management and order execution""" + + def __init__(self, client: CryptoTradingClient): + self.client = client + self.config = client.config + + def calculate_position_size(self, exchange: str, symbol: str, risk_percentage: float = None) -> float: + """ + Calculate position size based on risk management rules + + Args: + exchange: Exchange name + symbol: Trading symbol + risk_percentage: Risk percentage of total balance (default from config) + + Returns: + Position size in base currency + """ + if risk_percentage is None: + risk_percentage = self.config.trading_config.risk_percentage + + # Get account balance + balance_info = self.client.get_balance(exchange) + if isinstance(balance_info, dict) and "USDT" in balance_info: + usdt_balance = balance_info["USDT"].get("available", 0) if isinstance(balance_info["USDT"], dict) else balance_info["USDT"] + else: + usdt_balance = 1000 # Default fallback + + # Calculate position size + risk_amount = usdt_balance * risk_percentage + max_position = min(risk_amount, self.config.trading_config.max_position_size) + + return max_position + + def calculate_stop_loss_take_profit(self, entry_price: float, side: str) -> Dict[str, float]: + """ + Calculate stop loss and take profit levels + + Args: + entry_price: Entry price + side: 'buy' or 'sell' + + Returns: + Dictionary with stop_loss and take_profit prices + """ + stop_loss_pct = self.config.trading_config.stop_loss_percentage + take_profit_pct = self.config.trading_config.take_profit_percentage + + if side.lower() == 'buy': + stop_loss = entry_price * (1 - stop_loss_pct) + take_profit = entry_price * (1 + take_profit_pct) + else: # sell + stop_loss = entry_price * (1 + stop_loss_pct) + take_profit = entry_price * (1 - take_profit_pct) + + return { + 'stop_loss': round(stop_loss, 8), + 'take_profit': round(take_profit, 8) + } + + def format_quantity(self, exchange: str, symbol: str, quantity: float) -> float: + """Format quantity according to exchange requirements""" + # This is a simplified version - in production, you'd get this from exchange info + if exchange == "binance": + # Binance typically requires specific decimal places + return round(quantity, 6) + elif exchange == "okx": + # OKX has different requirements + return round(quantity, 8) + + return round(quantity, 8) + + def buy_market(self, exchange: str, symbol: str, amount_usdt: float) -> Dict[str, Any]: + """ + Execute market buy order + + Args: + exchange: Exchange name + symbol: Trading symbol + amount_usdt: Amount in USDT to buy + + Returns: + Order result dictionary + """ + try: + # Get current price + ticker = self.client.get_ticker(exchange, symbol) + if "error" in ticker: + return {"error": f"Failed to get ticker: {ticker['error']}"} + + current_price = ticker["price"] + quantity = amount_usdt / current_price + quantity = self.format_quantity(exchange, symbol, quantity) + + # Place market buy order + if exchange not in self.client.exchanges: + return {"error": f"Exchange {exchange} not configured"} + + order_result = self.client.exchanges[exchange].place_order( + symbol=symbol, + side="buy", + order_type="market", + quantity=quantity + ) + + # Calculate stop loss and take profit + sl_tp = self.calculate_stop_loss_take_profit(current_price, "buy") + + result = { + "status": "success", + "exchange": exchange, + "symbol": symbol, + "side": "buy", + "type": "market", + "quantity": quantity, + "amount_usdt": amount_usdt, + "estimated_price": current_price, + "stop_loss": sl_tp["stop_loss"], + "take_profit": sl_tp["take_profit"], + "order_result": order_result, + "timestamp": time.time() + } + + return result + + except Exception as e: + return { + "error": f"Failed to execute buy order: {str(e)}", + "exchange": exchange, + "symbol": symbol + } + + def sell_market(self, exchange: str, symbol: str, quantity: float) -> Dict[str, Any]: + """ + Execute market sell order + + Args: + exchange: Exchange name + symbol: Trading symbol + quantity: Quantity to sell + + Returns: + Order result dictionary + """ + try: + # Get current price + ticker = self.client.get_ticker(exchange, symbol) + if "error" in ticker: + return {"error": f"Failed to get ticker: {ticker['error']}"} + + current_price = ticker["price"] + quantity = self.format_quantity(exchange, symbol, quantity) + + # Place market sell order + if exchange not in self.client.exchanges: + return {"error": f"Exchange {exchange} not configured"} + + order_result = self.client.exchanges[exchange].place_order( + symbol=symbol, + side="sell", + order_type="market", + quantity=quantity + ) + + # Calculate stop loss and take profit + sl_tp = self.calculate_stop_loss_take_profit(current_price, "sell") + + result = { + "status": "success", + "exchange": exchange, + "symbol": symbol, + "side": "sell", + "type": "market", + "quantity": quantity, + "estimated_price": current_price, + "estimated_amount_usdt": quantity * current_price, + "stop_loss": sl_tp["stop_loss"], + "take_profit": sl_tp["take_profit"], + "order_result": order_result, + "timestamp": time.time() + } + + return result + + except Exception as e: + return { + "error": f"Failed to execute sell order: {str(e)}", + "exchange": exchange, + "symbol": symbol + } + + def buy_limit(self, exchange: str, symbol: str, quantity: float, price: float) -> Dict[str, Any]: + """ + Execute limit buy order + + Args: + exchange: Exchange name + symbol: Trading symbol + quantity: Quantity to buy + price: Limit price + + Returns: + Order result dictionary + """ + try: + quantity = self.format_quantity(exchange, symbol, quantity) + + if exchange not in self.client.exchanges: + return {"error": f"Exchange {exchange} not configured"} + + order_result = self.client.exchanges[exchange].place_order( + symbol=symbol, + side="buy", + order_type="limit", + quantity=quantity, + price=price + ) + + # Calculate stop loss and take profit + sl_tp = self.calculate_stop_loss_take_profit(price, "buy") + + result = { + "status": "success", + "exchange": exchange, + "symbol": symbol, + "side": "buy", + "type": "limit", + "quantity": quantity, + "price": price, + "amount_usdt": quantity * price, + "stop_loss": sl_tp["stop_loss"], + "take_profit": sl_tp["take_profit"], + "order_result": order_result, + "timestamp": time.time() + } + + return result + + except Exception as e: + return { + "error": f"Failed to execute limit buy order: {str(e)}", + "exchange": exchange, + "symbol": symbol + } + + def sell_limit(self, exchange: str, symbol: str, quantity: float, price: float) -> Dict[str, Any]: + """ + Execute limit sell order + + Args: + exchange: Exchange name + symbol: Trading symbol + quantity: Quantity to sell + price: Limit price + + Returns: + Order result dictionary + """ + try: + quantity = self.format_quantity(exchange, symbol, quantity) + + if exchange not in self.client.exchanges: + return {"error": f"Exchange {exchange} not configured"} + + order_result = self.client.exchanges[exchange].place_order( + symbol=symbol, + side="sell", + order_type="limit", + quantity=quantity, + price=price + ) + + # Calculate stop loss and take profit + sl_tp = self.calculate_stop_loss_take_profit(price, "sell") + + result = { + "status": "success", + "exchange": exchange, + "symbol": symbol, + "side": "sell", + "type": "limit", + "quantity": quantity, + "price": price, + "amount_usdt": quantity * price, + "stop_loss": sl_tp["stop_loss"], + "take_profit": sl_tp["take_profit"], + "order_result": order_result, + "timestamp": time.time() + } + + return result + + except Exception as e: + return { + "error": f"Failed to execute limit sell order: {str(e)}", + "exchange": exchange, + "symbol": symbol + } + + def auto_trade_by_signals(self, exchange: str, symbol: str, amount_usdt: float = None) -> Dict[str, Any]: + """ + Execute trade based on technical analysis signals + + Args: + exchange: Exchange name + symbol: Trading symbol + amount_usdt: Amount to trade (optional, uses risk management if not provided) + + Returns: + Trade result dictionary + """ + try: + # Get klines data + klines = self.client.get_klines(exchange, symbol, "1h", 100) + if not klines: + return {"error": "Failed to get klines data"} + + # Calculate indicators + indicators = TechnicalIndicators.calculate_all_indicators(klines) + if "error" in indicators: + return {"error": f"Failed to calculate indicators: {indicators['error']}"} + + # Get trading signals + signals = TechnicalIndicators.get_trading_signals(indicators) + overall_signal = signals.get("overall", "HOLD") + + if overall_signal == "HOLD": + return { + "status": "no_action", + "signal": overall_signal, + "signals": signals, + "indicators": indicators, + "message": "No clear trading signal, holding position" + } + + # Calculate position size if not provided + if amount_usdt is None: + amount_usdt = self.calculate_position_size(exchange, symbol) + + # Execute trade based on signal + if overall_signal == "BUY": + result = self.buy_market(exchange, symbol, amount_usdt) + else: # SELL + # For sell signal, we need to determine quantity to sell + # This is simplified - in practice, you'd track your positions + ticker = self.client.get_ticker(exchange, symbol) + if "error" not in ticker: + quantity = amount_usdt / ticker["price"] + result = self.sell_market(exchange, symbol, quantity) + else: + result = {"error": "Failed to get current price for sell order"} + + # Add signal information to result + if "error" not in result: + result["signal_info"] = { + "overall_signal": overall_signal, + "signals": signals, + "key_indicators": { + "rsi": indicators.get("rsi"), + "macd": indicators.get("macd"), + "current_price": indicators.get("current_price") + } + } + + return result + + except Exception as e: + return { + "error": f"Failed to execute auto trade: {str(e)}", + "exchange": exchange, + "symbol": symbol + } + + def get_trading_summary(self, exchange: str, symbol: str) -> Dict[str, Any]: + """ + Get comprehensive trading summary including market data and signals + + Args: + exchange: Exchange name + symbol: Trading symbol + + Returns: + Trading summary dictionary + """ + try: + # Get market data + ticker = self.client.get_ticker(exchange, symbol) + klines = self.client.get_klines(exchange, symbol, "1h", 100) + + # Calculate indicators and signals + indicators = TechnicalIndicators.calculate_all_indicators(klines) + signals = TechnicalIndicators.get_trading_signals(indicators) + + # Get account info + balance = self.client.get_balance(exchange) + + # Calculate recommended position size + position_size = self.calculate_position_size(exchange, symbol) + + summary = { + "exchange": exchange, + "symbol": symbol, + "market_data": ticker, + "technical_analysis": { + "indicators": indicators, + "signals": signals, + "overall_signal": signals.get("overall", "HOLD") + }, + "risk_management": { + "recommended_position_size_usdt": position_size, + "risk_percentage": self.config.trading_config.risk_percentage, + "stop_loss_percentage": self.config.trading_config.stop_loss_percentage, + "take_profit_percentage": self.config.trading_config.take_profit_percentage + }, + "account_balance": balance, + "timestamp": time.time() + } + + return summary + + except Exception as e: + return { + "error": f"Failed to generate trading summary: {str(e)}", + "exchange": exchange, + "symbol": symbol + } \ No newline at end of file From 4079fd55444e18b08a181625b302e703eef1e92e Mon Sep 17 00:00:00 2001 From: ashione Date: Mon, 22 Sep 2025 01:37:23 +0800 Subject: [PATCH 2/5] fix make request --- vertex_flow/plugins/crypto_trading/exchanges.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vertex_flow/plugins/crypto_trading/exchanges.py b/vertex_flow/plugins/crypto_trading/exchanges.py index 927dcad..9e444c7 100644 --- a/vertex_flow/plugins/crypto_trading/exchanges.py +++ b/vertex_flow/plugins/crypto_trading/exchanges.py @@ -110,8 +110,8 @@ def _make_request(self, method: str, endpoint: str, params: Optional[Dict] = Non """Make authenticated request to OKX API""" # 使用服务器时间确保时间戳准确 timestamp = self._get_server_time() - # endpoint已经包含完整路径,不需要额外处理 - request_path = endpoint + # 签名需要完整的API路径,包含/api/v5前缀 + request_path = "/api/v5" + endpoint if params: request_path += "?" + urlencode(params) @@ -127,7 +127,7 @@ def _make_request(self, method: str, endpoint: str, params: Optional[Dict] = Non "Content-Type": "application/json" } - url = self.base_url + endpoint + url = self.api_url + endpoint try: if method.upper() == "GET": @@ -222,7 +222,7 @@ def place_order(self, symbol: str, side: str, order_type: str, quantity: float, if price and order_type.lower() == "limit": order_data["px"] = str(price) - return self._make_request("POST", "/api/v5/trade/order", data=order_data) + return self._make_request("POST", "/trade/order", data=order_data) def get_order_status(self, order_id: str, symbol: str) -> Dict[str, Any]: """查询订单状态""" @@ -232,7 +232,7 @@ def get_order_status(self, order_id: str, symbol: str) -> Dict[str, Any]: "instId": symbol } - response = self._make_request("GET", "/api/v5/trade/order", params) + response = self._make_request("GET", "/trade/order", params) if response.get("code") == "0" and response.get("data"): order_info = response["data"][0] @@ -249,7 +249,7 @@ def get_order_status(self, order_id: str, symbol: str) -> Dict[str, Any]: def get_spot_positions(self) -> Dict[str, Any]: """Get spot account balance""" try: - response = self._make_request("GET", "/api/v5/account/balance") + response = self._make_request("GET", "/account/balance") if response.get("code") == "0" and response.get("data"): positions = [] for account in response["data"]: @@ -273,7 +273,7 @@ def get_spot_positions(self) -> Dict[str, Any]: def get_futures_positions(self) -> Dict[str, Any]: """Get futures positions""" try: - response = self._make_request("GET", "/api/v5/account/positions") + response = self._make_request("GET", "/account/positions") if response.get("code") == "0" and response.get("data"): positions = [] for position in response["data"]: From a0474f8e9b2c4aa120bfb667e25ccc94c5e70a8e Mon Sep 17 00:00:00 2001 From: ashione Date: Mon, 22 Sep 2025 01:56:50 +0800 Subject: [PATCH 3/5] lint --- .../plugins/crypto_trading/__init__.py | 4 +- vertex_flow/plugins/crypto_trading/client.py | 149 ++++---- vertex_flow/plugins/crypto_trading/config.py | 52 ++- .../plugins/crypto_trading/error_handler.py | 95 +++-- vertex_flow/plugins/crypto_trading/example.py | 255 +++++++------ .../plugins/crypto_trading/exchanges.py | 345 +++++++++--------- .../plugins/crypto_trading/indicators.py | 337 +++++++++-------- .../crypto_trading/position_metrics.py | 135 ++++--- .../plugins/crypto_trading/show_indicators.py | 182 +++++++++ vertex_flow/plugins/crypto_trading/trading.py | 239 +++++------- 10 files changed, 949 insertions(+), 844 deletions(-) create mode 100644 vertex_flow/plugins/crypto_trading/show_indicators.py diff --git a/vertex_flow/plugins/crypto_trading/__init__.py b/vertex_flow/plugins/crypto_trading/__init__.py index 54bf336..342b219 100644 --- a/vertex_flow/plugins/crypto_trading/__init__.py +++ b/vertex_flow/plugins/crypto_trading/__init__.py @@ -6,9 +6,9 @@ """ from .client import CryptoTradingClient -from .exchanges import OKXClient, BinanceClient +from .exchanges import BinanceClient, OKXClient from .indicators import TechnicalIndicators from .trading import TradingEngine __version__ = "1.0.0" -__all__ = ["CryptoTradingClient", "OKXClient", "BinanceClient", "TechnicalIndicators", "TradingEngine"] \ No newline at end of file +__all__ = ["CryptoTradingClient", "OKXClient", "BinanceClient", "TechnicalIndicators", "TradingEngine"] diff --git a/vertex_flow/plugins/crypto_trading/client.py b/vertex_flow/plugins/crypto_trading/client.py index c800338..80e7787 100644 --- a/vertex_flow/plugins/crypto_trading/client.py +++ b/vertex_flow/plugins/crypto_trading/client.py @@ -3,154 +3,149 @@ """ import time -from typing import Dict, Any, List, Optional, Union +from typing import Any, Dict, List, Optional, Union try: from .config import CryptoTradingConfig - from .exchanges import OKXClient, BinanceClient, BaseExchange + from .exchanges import BaseExchange, BinanceClient, OKXClient except ImportError: from config import CryptoTradingConfig - from exchanges import OKXClient, BinanceClient, BaseExchange + from exchanges import BaseExchange, BinanceClient, OKXClient class CryptoTradingClient: """Main client for crypto trading operations""" - + def __init__(self, config: Optional[CryptoTradingConfig] = None): self.config = config or CryptoTradingConfig() self.exchanges: Dict[str, BaseExchange] = {} self._initialize_exchanges() - + def _initialize_exchanges(self): """Initialize exchange clients based on configuration""" if self.config.okx_config: self.exchanges["okx"] = OKXClient(self.config.okx_config) - + if self.config.binance_config: self.exchanges["binance"] = BinanceClient(self.config.binance_config) - + def get_available_exchanges(self) -> List[str]: """Get list of available exchanges""" return list(self.exchanges.keys()) - + def get_account_info(self, exchange: str) -> Dict[str, Any]: """ Get account information from specified exchange - + Args: exchange: Exchange name ('okx' or 'binance') - + Returns: Account information dictionary """ if exchange not in self.exchanges: raise ValueError(f"Exchange '{exchange}' not configured or not supported") - + try: account_info = self.exchanges[exchange].get_account_info() return self._normalize_account_info(exchange, account_info) except Exception as e: return {"error": str(e), "exchange": exchange} - + def get_all_account_info(self) -> Dict[str, Dict[str, Any]]: """Get account information from all configured exchanges""" results = {} for exchange_name in self.exchanges: results[exchange_name] = self.get_account_info(exchange_name) return results - + def get_trading_fees(self, exchange: str, symbol: str) -> Dict[str, float]: """ Get trading fees for a symbol on specified exchange - + Args: exchange: Exchange name ('okx' or 'binance') symbol: Trading symbol (e.g., 'BTC-USDT' for OKX, 'BTCUSDT' for Binance) - + Returns: Dictionary with maker_fee and taker_fee """ if exchange not in self.exchanges: raise ValueError(f"Exchange '{exchange}' not configured or not supported") - + try: return self.exchanges[exchange].get_trading_fees(symbol) except Exception as e: return {"error": str(e), "maker_fee": 0, "taker_fee": 0} - + def get_ticker(self, exchange: str, symbol: str) -> Dict[str, Any]: """ Get ticker information for a symbol - + Args: exchange: Exchange name ('okx' or 'binance') symbol: Trading symbol - + Returns: Ticker information dictionary """ if exchange not in self.exchanges: raise ValueError(f"Exchange '{exchange}' not configured or not supported") - + try: return self.exchanges[exchange].get_ticker(symbol) except Exception as e: return {"error": str(e), "exchange": exchange, "symbol": symbol} - + def get_klines(self, exchange: str, symbol: str, interval: str = "1h", limit: int = 100) -> List[List]: """ Get kline/candlestick data - + Args: exchange: Exchange name ('okx' or 'binance') symbol: Trading symbol interval: Time interval ('1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w') limit: Number of klines to retrieve - + Returns: List of kline data [timestamp, open, high, low, close, volume] """ if exchange not in self.exchanges: raise ValueError(f"Exchange '{exchange}' not configured or not supported") - + try: return self.exchanges[exchange].get_klines(symbol, interval, limit) except Exception as e: print(f"Error getting klines: {e}") return [] - + def get_balance(self, exchange: str, currency: Optional[str] = None) -> Union[Dict[str, float], float]: """ Get balance for specific currency or all currencies - + Args: exchange: Exchange name currency: Currency symbol (optional, if None returns all balances) - + Returns: Balance information """ account_info = self.get_account_info(exchange) - + if "error" in account_info: return 0.0 if currency else {} - + balances = account_info.get("balances", {}) - + if currency: return balances.get(currency, 0.0) - + return balances - + def _normalize_account_info(self, exchange: str, raw_info: Dict[str, Any]) -> Dict[str, Any]: """Normalize account information across different exchanges""" - normalized = { - "exchange": exchange, - "balances": {}, - "total_value_usdt": 0.0, - "raw_data": raw_info - } - + normalized = {"exchange": exchange, "balances": {}, "total_value_usdt": 0.0, "raw_data": raw_info} + try: if exchange == "okx": if raw_info.get("data"): @@ -159,36 +154,32 @@ def _normalize_account_info(self, exchange: str, raw_info: Dict[str, Any]) -> Di currency = detail.get("ccy") available = float(detail.get("availBal", 0)) frozen = float(detail.get("frozenBal", 0)) - + if available > 0 or frozen > 0: normalized["balances"][currency] = { "available": available, "frozen": frozen, - "total": available + frozen + "total": available + frozen, } - + elif exchange == "binance": for balance in raw_info.get("balances", []): currency = balance.get("asset") free = float(balance.get("free", 0)) locked = float(balance.get("locked", 0)) - + if free > 0 or locked > 0: - normalized["balances"][currency] = { - "available": free, - "frozen": locked, - "total": free + locked - } - + normalized["balances"][currency] = {"available": free, "frozen": locked, "total": free + locked} + except Exception as e: normalized["error"] = f"Error normalizing account info: {str(e)}" - + return normalized - + def get_exchange_status(self) -> Dict[str, Dict[str, Any]]: """Get status of all configured exchanges""" status = {} - + for exchange_name in self.exchanges: try: # Test connection by getting account info @@ -196,60 +187,56 @@ def get_exchange_status(self) -> Dict[str, Dict[str, Any]]: status[exchange_name] = { "connected": "error" not in account_info, "error": account_info.get("error"), - "last_check": "now" + "last_check": "now", } except Exception as e: - status[exchange_name] = { - "connected": False, - "error": str(e), - "last_check": "now" - } - + status[exchange_name] = {"connected": False, "error": str(e), "last_check": "now"} + return status - + def get_spot_positions(self, exchange: str) -> Dict[str, Any]: """ Get spot positions for a specific exchange - + Args: exchange: Exchange name ('okx' or 'binance') - + Returns: Spot positions information """ if exchange not in self.exchanges: return {"error": f"Exchange '{exchange}' not configured or not supported"} - + try: return self.exchanges[exchange].get_spot_positions() except Exception as e: return {"error": f"Failed to get spot positions: {str(e)}"} - + def get_futures_positions(self, exchange: str) -> Dict[str, Any]: """ Get futures positions for a specific exchange - + Args: exchange: Exchange name ('okx' or 'binance') - + Returns: Futures positions information """ if exchange not in self.exchanges: return {"error": f"Exchange '{exchange}' not configured or not supported"} - + try: return self.exchanges[exchange].get_futures_positions() except Exception as e: return {"error": f"Failed to get futures positions: {str(e)}"} - + def get_all_positions(self, exchange: str = None) -> Dict[str, Any]: """ Get both spot and futures positions for one or all exchanges - + Args: exchange: Exchange name (optional, if None returns all exchanges) - + Returns: All positions information """ @@ -257,30 +244,24 @@ def get_all_positions(self, exchange: str = None) -> Dict[str, Any]: # Get positions for specific exchange if exchange not in self.exchanges: return {"error": f"Exchange '{exchange}' not configured or not supported"} - + spot_positions = self.get_spot_positions(exchange) futures_positions = self.get_futures_positions(exchange) - + return { "exchange": exchange, "spot": spot_positions, "futures": futures_positions, - "timestamp": time.time() + "timestamp": time.time(), } else: # Get positions for all exchanges all_positions = {} - + for exchange_name in self.exchanges: spot_positions = self.get_spot_positions(exchange_name) futures_positions = self.get_futures_positions(exchange_name) - - all_positions[exchange_name] = { - "spot": spot_positions, - "futures": futures_positions - } - - return { - "all_exchanges": all_positions, - "timestamp": time.time() - } \ No newline at end of file + + all_positions[exchange_name] = {"spot": spot_positions, "futures": futures_positions} + + return {"all_exchanges": all_positions, "timestamp": time.time()} diff --git a/vertex_flow/plugins/crypto_trading/config.py b/vertex_flow/plugins/crypto_trading/config.py index 2c81031..e70374b 100644 --- a/vertex_flow/plugins/crypto_trading/config.py +++ b/vertex_flow/plugins/crypto_trading/config.py @@ -3,8 +3,9 @@ """ import os -from typing import Dict, Any, Optional from dataclasses import dataclass +from typing import Any, Dict, Optional + from dotenv import load_dotenv # 加载.env文件 @@ -14,6 +15,7 @@ @dataclass class ExchangeConfig: """Exchange configuration""" + api_key: str secret_key: str passphrase: Optional[str] = None # For OKX @@ -24,6 +26,7 @@ class ExchangeConfig: @dataclass class TradingConfig: """Trading configuration""" + default_symbol: str = "BTC-USDT" max_position_size: float = 1000.0 risk_percentage: float = 0.02 @@ -33,93 +36,84 @@ class TradingConfig: class CryptoTradingConfig: """Main configuration class for crypto trading plugin""" - + def __init__(self): self.okx_config: Optional[ExchangeConfig] = None self.binance_config: Optional[ExchangeConfig] = None self.trading_config = TradingConfig() self._load_from_env() - + def _load_from_env(self): """Load configuration from environment variables""" # OKX Configuration okx_api_key = os.getenv("OKX_API_KEY") okx_secret_key = os.getenv("OKX_SECRET_KEY") okx_passphrase = os.getenv("OKX_PASSPHRASE") - + if okx_api_key and okx_secret_key and okx_passphrase: self.okx_config = ExchangeConfig( api_key=okx_api_key, secret_key=okx_secret_key, passphrase=okx_passphrase, - sandbox=os.getenv("OKX_SANDBOX", "false").lower() == "true" + sandbox=os.getenv("OKX_SANDBOX", "false").lower() == "true", ) - + # Binance Configuration binance_api_key = os.getenv("BINANCE_API_KEY") binance_secret_key = os.getenv("BINANCE_SECRET_KEY") - + if binance_api_key and binance_secret_key: self.binance_config = ExchangeConfig( api_key=binance_api_key, secret_key=binance_secret_key, - sandbox=os.getenv("BINANCE_SANDBOX", "false").lower() == "true" + sandbox=os.getenv("BINANCE_SANDBOX", "false").lower() == "true", ) - + # Trading Configuration default_symbol = os.getenv("DEFAULT_SYMBOL") if default_symbol: self.trading_config.default_symbol = default_symbol - + max_position_size = os.getenv("MAX_POSITION_SIZE") if max_position_size: try: self.trading_config.max_position_size = float(max_position_size) except ValueError: pass - + risk_percentage = os.getenv("RISK_PERCENTAGE") if risk_percentage: try: self.trading_config.risk_percentage = float(risk_percentage) except ValueError: pass - + stop_loss_percentage = os.getenv("STOP_LOSS_PERCENTAGE") if stop_loss_percentage: try: self.trading_config.stop_loss_percentage = float(stop_loss_percentage) except ValueError: pass - + take_profit_percentage = os.getenv("TAKE_PROFIT_PERCENTAGE") if take_profit_percentage: try: self.trading_config.take_profit_percentage = float(take_profit_percentage) except ValueError: pass - + def set_okx_config(self, api_key: str, secret_key: str, passphrase: str, sandbox: bool = False): """Set OKX configuration""" - self.okx_config = ExchangeConfig( - api_key=api_key, - secret_key=secret_key, - passphrase=passphrase, - sandbox=sandbox - ) - + self.okx_config = ExchangeConfig(api_key=api_key, secret_key=secret_key, passphrase=passphrase, sandbox=sandbox) + def set_binance_config(self, api_key: str, secret_key: str, sandbox: bool = False): """Set Binance configuration""" - self.binance_config = ExchangeConfig( - api_key=api_key, - secret_key=secret_key, - sandbox=sandbox - ) - + self.binance_config = ExchangeConfig(api_key=api_key, secret_key=secret_key, sandbox=sandbox) + def get_config_dict(self) -> Dict[str, Any]: """Get configuration as dictionary""" return { "okx": self.okx_config.__dict__ if self.okx_config else None, "binance": self.binance_config.__dict__ if self.binance_config else None, - "trading": self.trading_config.__dict__ - } \ No newline at end of file + "trading": self.trading_config.__dict__, + } diff --git a/vertex_flow/plugins/crypto_trading/error_handler.py b/vertex_flow/plugins/crypto_trading/error_handler.py index 75bf225..bd1d314 100644 --- a/vertex_flow/plugins/crypto_trading/error_handler.py +++ b/vertex_flow/plugins/crypto_trading/error_handler.py @@ -5,44 +5,38 @@ """ import re -from typing import Dict, Any, Optional +from typing import Any, Dict, Optional + import requests class ErrorHandler: """统一的错误处理工具类""" - + @staticmethod def format_api_error( - service_name: str, - error: Exception, - response: Optional[requests.Response] = None + service_name: str, error: Exception, response: Optional[requests.Response] = None ) -> Dict[str, Any]: """ 格式化API错误信息 - + Args: service_name: 服务名称 (如 "OKX", "Binance") error: 异常对象 response: HTTP响应对象 (可选) - + Returns: 格式化的错误信息字典 """ - error_info = { - "error": True, - "service": service_name, - "type": type(error).__name__, - "message": str(error) - } - + error_info = {"error": True, "service": service_name, "type": type(error).__name__, "message": str(error)} + if isinstance(error, requests.exceptions.RequestException): error_info["category"] = "network" - - if hasattr(error, 'response') and error.response is not None: + + if hasattr(error, "response") and error.response is not None: response = error.response error_info["status_code"] = response.status_code - + # 根据状态码提供更友好的错误信息 if response.status_code == 401: error_info["user_message"] = "API密钥无效或已过期,请检查配置" @@ -54,10 +48,10 @@ def format_api_error( error_info["user_message"] = f"{service_name}服务器暂时不可用,请稍后重试" else: error_info["user_message"] = f"{service_name}API请求失败 (状态码: {response.status_code})" - + # 处理响应内容 - content_type = response.headers.get('content-type', '').lower() - if 'html' in content_type: + content_type = response.headers.get("content-type", "").lower() + if "html" in content_type: error_info["response_type"] = "html" error_info["user_message"] += " - 服务器返回了错误页面" elif response.text: @@ -68,74 +62,74 @@ def format_api_error( error_info["response_preview"] = clean_text[:100] else: error_info["user_message"] = f"网络连接失败,无法访问{service_name}服务" - + elif "json" in str(error).lower() or "JSONDecodeError" in type(error).__name__: error_info["category"] = "parsing" error_info["user_message"] = f"{service_name}返回了无效的数据格式" - + if response: - content_type = response.headers.get('content-type', '').lower() - if 'html' in content_type: + content_type = response.headers.get("content-type", "").lower() + if "html" in content_type: error_info["user_message"] += " (服务器返回了HTML页面)" - + else: error_info["category"] = "unknown" error_info["user_message"] = f"{service_name}服务出现未知错误" - + return error_info - + @staticmethod def _clean_error_text(text: str) -> str: """ 清理错误文本,移除HTML标签和多余的空白字符 - + Args: text: 原始文本 - + Returns: 清理后的文本 """ if not text: return "" - + # 移除HTML标签 - clean_text = re.sub(r'<[^>]+>', '', text) - + clean_text = re.sub(r"<[^>]+>", "", text) + # 移除多余的空白字符 - clean_text = re.sub(r'\s+', ' ', clean_text).strip() - + clean_text = re.sub(r"\s+", " ", clean_text).strip() + # 如果文本太长,只保留开头部分 if len(clean_text) > 200: clean_text = clean_text[:200] + "..." - + return clean_text - + @staticmethod def get_user_friendly_message(error_info: Dict[str, Any]) -> str: """ 获取用户友好的错误信息 - + Args: error_info: 错误信息字典 - + Returns: 用户友好的错误消息 """ return error_info.get("user_message", error_info.get("message", "未知错误")) - + @staticmethod def is_retryable_error(error_info: Dict[str, Any]) -> bool: """ 判断错误是否可以重试 - + Args: error_info: 错误信息字典 - + Returns: 是否可以重试 """ status_code = error_info.get("status_code") - + # 网络错误通常可以重试 if error_info.get("category") == "network": # 401, 403 等认证错误不应该重试 @@ -144,29 +138,24 @@ def is_retryable_error(error_info: Dict[str, Any]) -> bool: # 429 (频率限制) 可以重试,但需要等待 # 5xx 服务器错误可以重试 return status_code is None or status_code >= 429 - + # 解析错误通常不需要重试 if error_info.get("category") == "parsing": return False - + # 其他错误可以尝试重试 return True - + @staticmethod def format_simple_error(service_name: str, message: str) -> Dict[str, Any]: """ 格式化简单错误信息 - + Args: service_name: 服务名称 message: 错误消息 - + Returns: 格式化的错误信息 """ - return { - "error": True, - "service": service_name, - "message": message, - "user_message": message - } \ No newline at end of file + return {"error": True, "service": service_name, "message": message, "user_message": message} diff --git a/vertex_flow/plugins/crypto_trading/example.py b/vertex_flow/plugins/crypto_trading/example.py index ad7fa54..8581bf5 100644 --- a/vertex_flow/plugins/crypto_trading/example.py +++ b/vertex_flow/plugins/crypto_trading/example.py @@ -8,7 +8,7 @@ import os import sys import time -from typing import Dict, Any +from typing import Any, Dict # Add the plugin to Python path sys.path.append(os.path.dirname(os.path.abspath(__file__))) @@ -16,6 +16,7 @@ # Load environment variables from .env file try: from dotenv import load_dotenv + load_dotenv() print("✅ Loaded .env file") except ImportError: @@ -24,54 +25,52 @@ except Exception as e: print(f"⚠️ Could not load .env file: {e}") -from config import CryptoTradingConfig from client import CryptoTradingClient -from trading import TradingEngine +from config import CryptoTradingConfig from indicators import TechnicalIndicators from position_metrics import PositionMetrics +from trading import TradingEngine def setup_config_example(): """Example of setting up configuration programmatically""" config = CryptoTradingConfig() - + # Set OKX configuration (replace with your actual credentials) config.set_okx_config( api_key="your_okx_api_key", secret_key="your_okx_secret_key", passphrase="your_okx_passphrase", - sandbox=True # Use sandbox for testing + sandbox=True, # Use sandbox for testing ) - + # Set Binance configuration (replace with your actual credentials) config.set_binance_config( - api_key="your_binance_api_key", - secret_key="your_binance_secret_key", - sandbox=True # Use sandbox for testing + api_key="your_binance_api_key", secret_key="your_binance_secret_key", sandbox=True # Use sandbox for testing ) - + # Adjust trading parameters - #config.trading_config.default_symbol = "BTC-USDT" + # config.trading_config.default_symbol = "BTC-USDT" config.trading_config.default_symbol = "PUMP-USDT" config.trading_config.risk_percentage = 0.01 # 1% risk per trade config.trading_config.max_position_size = 100.0 # Max $100 per trade - + return config def get_symbol_for_exchange(config: CryptoTradingConfig, exchange: str) -> str: """ Get the symbol formatted for the specific exchange - + Args: config: Trading configuration exchange: Exchange name ('okx' or 'binance') - + Returns: Symbol formatted for the exchange """ default_symbol = config.trading_config.default_symbol - + # If the symbol is already in the correct format for the exchange, return it if exchange == "okx": # OKX uses format like "BTC-USDT" @@ -100,20 +99,20 @@ def get_symbol_for_exchange(config: CryptoTradingConfig, exchange: str) -> str: def basic_usage_example(): """Basic usage example""" print("=== Crypto Trading Plugin Basic Usage Example ===\n") - + # Initialize configuration (will load from environment variables and .env file) config = CryptoTradingConfig() client = CryptoTradingClient(config) - + # Display configuration status print("📋 Configuration Status:") print(f" OKX configured: {'✅' if config.okx_config else '❌'}") print(f" Binance configured: {'✅' if config.binance_config else '❌'}") - + # Check available exchanges exchanges = client.get_available_exchanges() print(f"\n🏢 Available exchanges: {exchanges}") - + if not exchanges: print("\n❌ No exchanges configured. Please set up your API credentials.") print("\n📝 To configure exchanges:") @@ -121,14 +120,14 @@ def basic_usage_example(): print(" 2. Fill in your API credentials") print(" 3. Set sandbox=true for testing") return - + # Use the first available exchange for examples exchange = exchanges[0] symbol = get_symbol_for_exchange(config, exchange) - + print(f"\n🔄 Using exchange: {exchange}") print(f"📊 Trading symbol: {symbol}") - + # Get account information print("\n--- Account Information ---") account_info = client.get_account_info(exchange) @@ -136,13 +135,13 @@ def basic_usage_example(): print(f"💰 Account balances: {account_info.get('balances', {})}") else: print(f"❌ Error getting account info: {account_info['error']}") - + # Get trading fees print("\n--- Trading Fees ---") fees = client.get_trading_fees(exchange, symbol) print(f"📈 Maker fee: {fees.get('maker_fee', 'N/A')}") print(f"📉 Taker fee: {fees.get('taker_fee', 'N/A')}") - + # Get ticker information print("\n--- Market Data ---") ticker = client.get_ticker(exchange, symbol) @@ -152,61 +151,61 @@ def basic_usage_example(): print(f"📈 24h change: {ticker.get('change', 'N/A')}%") else: # 从错误字典中获取用户友好的错误消息 - error_msg = ticker.get('user_message', ticker.get('message', 'Unknown error')) + error_msg = ticker.get("user_message", ticker.get("message", "Unknown error")) print(f"❌ Error getting ticker: {error_msg}") def technical_analysis_example(): """Technical analysis example""" print("\n=== Technical Analysis Example ===\n") - + config = CryptoTradingConfig() client = CryptoTradingClient(config) - + exchanges = client.get_available_exchanges() if not exchanges: print("No exchanges configured.") return - + exchange = exchanges[0] symbol = get_symbol_for_exchange(config, exchange) - + # Get klines data print(f"Getting klines data for {symbol} on {exchange}...") klines = client.get_klines(exchange, symbol, "1h", 100) - + if not klines: print("Failed to get klines data") return - + print(f"Retrieved {len(klines)} klines") - + # Calculate technical indicators print("\n--- Technical Indicators ---") indicators = TechnicalIndicators.calculate_all_indicators(klines) - + if "error" not in indicators: print(f"Current price: ${indicators.get('current_price', 'N/A')}") print(f"RSI (14): {indicators.get('rsi', 'N/A')}") print(f"SMA (20): ${indicators.get('sma_20', 'N/A')}") print(f"EMA (12): ${indicators.get('ema_12', 'N/A')}") - - if 'macd' in indicators: - macd = indicators['macd'] + + if "macd" in indicators: + macd = indicators["macd"] print(f"MACD: {macd.get('macd', 'N/A')}") print(f"MACD Signal: {macd.get('signal', 'N/A')}") - - if 'bollinger_bands' in indicators: - bb = indicators['bollinger_bands'] + + if "bollinger_bands" in indicators: + bb = indicators["bollinger_bands"] print(f"Bollinger Upper: ${bb.get('upper', 'N/A')}") print(f"Bollinger Lower: ${bb.get('lower', 'N/A')}") else: print(f"Error calculating indicators: {indicators['error']}") - + # Generate trading signals print("\n--- Trading Signals ---") signals = TechnicalIndicators.get_trading_signals(indicators) - + for indicator, signal in signals.items(): print(f"{indicator.upper()}: {signal}") @@ -215,37 +214,37 @@ def trading_example(): """Trading example (DEMO ONLY - DO NOT USE WITH REAL MONEY)""" print("\n=== Trading Example (DEMO ONLY) ===\n") print("WARNING: This is for demonstration only. Do not use with real money!") - + config = CryptoTradingConfig() client = CryptoTradingClient(config) trading_engine = TradingEngine(client) - + exchanges = client.get_available_exchanges() if not exchanges: print("No exchanges configured.") return - + exchange = exchanges[0] symbol = get_symbol_for_exchange(config, exchange) - + # Get trading summary print(f"Getting trading summary for {symbol} on {exchange}...") summary = trading_engine.get_trading_summary(exchange, symbol) - + if "error" not in summary: print(f"\nMarket Price: ${summary['market_data'].get('price', 'N/A')}") print(f"Overall Signal: {summary['technical_analysis']['overall_signal']}") print(f"Recommended Position Size: ${summary['risk_management']['recommended_position_size_usdt']}") - + # Show individual signals - signals = summary['technical_analysis']['signals'] + signals = summary["technical_analysis"]["signals"] print("\nIndividual Signals:") for indicator, signal in signals.items(): - if indicator != 'overall': + if indicator != "overall": print(f" {indicator}: {signal}") else: print(f"Error getting trading summary: {summary['error']}") - + # Example of manual trading (commented out for safety) """ # UNCOMMENT ONLY FOR TESTING WITH SANDBOX/TESTNET @@ -270,34 +269,34 @@ def trading_example(): def risk_management_example(): """Risk management example""" print("\n=== Risk Management Example ===\n") - + config = CryptoTradingConfig() client = CryptoTradingClient(config) trading_engine = TradingEngine(client) - + exchanges = client.get_available_exchanges() if not exchanges: print("No exchanges configured.") return - + exchange = exchanges[0] symbol = get_symbol_for_exchange(config, exchange) - + # Calculate position size position_size = trading_engine.calculate_position_size(exchange, symbol) print(f"Recommended position size: ${position_size}") - + # Get current price for stop loss/take profit calculation ticker = client.get_ticker(exchange, symbol) if "error" not in ticker: current_price = ticker["price"] - + # Calculate stop loss and take profit for buy order sl_tp_buy = trading_engine.calculate_stop_loss_take_profit(current_price, "buy") print(f"\nFor BUY at ${current_price}:") print(f"Stop Loss: ${sl_tp_buy['stop_loss']}") print(f"Take Profit: ${sl_tp_buy['take_profit']}") - + # Calculate stop loss and take profit for sell order sl_tp_sell = trading_engine.calculate_stop_loss_take_profit(current_price, "sell") print(f"\nFor SELL at ${current_price}:") @@ -308,26 +307,26 @@ def risk_management_example(): def positions_example(): """Positions query example""" print("\n=== Positions Query Example ===\n") - + config = CryptoTradingConfig() client = CryptoTradingClient(config) - + exchanges = client.get_available_exchanges() if not exchanges: print("No exchanges configured.") return - + # 查询所有交易所的持仓 print("=== All Exchanges Positions ===") all_positions = client.get_all_positions() - + if "error" in all_positions: print(f"Error getting all positions: {all_positions['error']}") return - + for exchange_name, positions in all_positions.get("all_exchanges", {}).items(): print(f"\n--- {exchange_name.upper()} Exchange ---") - + # 现货持仓 spot_positions = positions.get("spot", {}) if "error" in spot_positions: @@ -338,15 +337,14 @@ def positions_example(): if spot_data: print("Spot Positions:") for position in spot_data: - currency = position['currency'] - balance = position['balance'] - available = position['available'] - frozen = position['frozen'] - print(f" {currency}: Available={available:.8f}, " - f"Frozen={frozen:.8f}, Total={balance:.8f}") + currency = position["currency"] + balance = position["balance"] + available = position["available"] + frozen = position["frozen"] + print(f" {currency}: Available={available:.8f}, " f"Frozen={frozen:.8f}, Total={balance:.8f}") else: print("No spot positions found") - + # 合约持仓 futures_positions = positions.get("futures", {}) if "error" in futures_positions: @@ -357,21 +355,20 @@ def positions_example(): if futures_data: print("Futures Positions:") for position in futures_data: - symbol = position['symbol'] - side = position['side'] - size = position['size'] - entry_price = position.get('entry_price', 0) - pnl = position.get('unrealized_pnl', 0) + symbol = position["symbol"] + side = position["side"] + size = position["size"] + entry_price = position.get("entry_price", 0) + pnl = position.get("unrealized_pnl", 0) pnl_str = f"+{pnl:.2f}" if pnl >= 0 else f"{pnl:.2f}" - print(f" {symbol}: Side={side}, Size={size:.8f}, " - f"Entry=${entry_price:.4f}, PnL=${pnl_str}") + print(f" {symbol}: Side={side}, Size={size:.8f}, " f"Entry=${entry_price:.4f}, PnL=${pnl_str}") else: print("No futures positions found") - + # 查询单个交易所的详细持仓 print(f"\n=== Detailed Positions for {exchanges[0].upper()} ===") exchange = exchanges[0] - + # 现货持仓详情 spot_positions = client.get_spot_positions(exchange) print("\nSpot Positions Detail:") @@ -382,10 +379,10 @@ def positions_example(): positions_data = spot_positions.get("data", []) if positions_data: for position in positions_data: - currency = position['currency'] - balance = position['balance'] - available = position['available'] - frozen = position['frozen'] + currency = position["currency"] + balance = position["balance"] + available = position["available"] + frozen = position["frozen"] print(f"Currency: {currency}") print(f" Available: {available:.8f}") print(f" Frozen: {frozen:.8f}") @@ -393,7 +390,7 @@ def positions_example(): print() else: print("No spot positions") - + # 合约持仓详情 futures_positions = client.get_futures_positions(exchange) print("Futures Positions Detail:") @@ -404,21 +401,21 @@ def positions_example(): positions_data = futures_positions.get("data", []) if positions_data: for position in positions_data: - symbol = position['symbol'] - side = position['side'] - size = position['size'] - entry_price = position.get('entry_price', 0) - mark_price = position.get('mark_price', 0) - pnl = position.get('unrealized_pnl', 0) - leverage = position.get('leverage', 'N/A') - + symbol = position["symbol"] + side = position["side"] + size = position["size"] + entry_price = position.get("entry_price", 0) + mark_price = position.get("mark_price", 0) + pnl = position.get("unrealized_pnl", 0) + leverage = position.get("leverage", "N/A") + print(f"Symbol: {symbol}") print(f" Side: {side}") print(f" Size: {size:.8f}") print(f" Entry Price: ${entry_price:.4f}") print(f" Mark Price: ${mark_price:.4f}") print(f" Unrealized PnL: ${pnl:.2f}") - if leverage != 'N/A': + if leverage != "N/A": print(f" Leverage: {leverage}x") print() else: @@ -428,44 +425,44 @@ def positions_example(): def futures_metrics_example(): """合约持仓指标计算示例""" print("\n=== 合约持仓指标计算示例 ===\n") - + config = CryptoTradingConfig() client = CryptoTradingClient(config) - + exchanges = client.get_available_exchanges() if not exchanges: print("No exchanges configured.") return - + # 收集所有交易所的合约持仓 all_futures_positions = [] total_balance = 0 - + for exchange in exchanges: print(f"\n--- 分析 {exchange.upper()} 合约持仓指标 ---") - + # 获取合约持仓 futures_positions = client.get_futures_positions(exchange) - + if "error" in futures_positions: print(f"❌ 获取 {exchange} 合约持仓失败: {futures_positions['error']}") continue - + positions_data = futures_positions.get("data", []) - + if not positions_data: print(f"📭 {exchange} 暂无合约持仓") continue - + print(f"📊 找到 {len(positions_data)} 个合约持仓") - + # 计算每个持仓的指标 for position in positions_data: metrics = PositionMetrics.calculate_position_metrics(position, exchange) if "error" not in metrics: all_futures_positions.append(metrics) print(PositionMetrics.format_metrics_display(metrics)) - + # 尝试获取账户余额(用于计算资产利用率) try: account_info = client.get_balance(exchange) @@ -473,37 +470,35 @@ def futures_metrics_example(): total_balance += account_info["total_balance"] except: pass - + # 计算投资组合综合指标 if all_futures_positions: - print("\n" + "="*60) + print("\n" + "=" * 60) print("🎯 计算投资组合综合指标...") - print("="*60) - - portfolio_metrics = PositionMetrics.calculate_portfolio_metrics( - all_futures_positions, total_balance - ) - + print("=" * 60) + + portfolio_metrics = PositionMetrics.calculate_portfolio_metrics(all_futures_positions, total_balance) + if "error" not in portfolio_metrics: print(PositionMetrics.format_metrics_display(portfolio_metrics)) - + # 风险提醒 print("\n⚠️ 风险提醒:") - risk_dist = portfolio_metrics.get('风险等级分布', {}) - - if risk_dist.get('extreme', 0) > 0: + risk_dist = portfolio_metrics.get("风险等级分布", {}) + + if risk_dist.get("extreme", 0) > 0: print(f"🔴 发现 {risk_dist['extreme']} 个极高风险持仓,建议立即关注!") - - if risk_dist.get('high', 0) > 0: + + if risk_dist.get("high", 0) > 0: print(f"🟠 发现 {risk_dist['high']} 个高风险持仓,请密切监控") - + # 杠杆提醒 - avg_leverage = float(portfolio_metrics.get('平均杠杆', '0x').replace('x', '')) + avg_leverage = float(portfolio_metrics.get("平均杠杆", "0x").replace("x", "")) if avg_leverage > 10: print(f"⚡ 平均杠杆较高 ({avg_leverage:.1f}x),请注意风险控制") - + # 盈亏提醒 - total_pnl_pct = float(portfolio_metrics.get('总盈亏率', '0%').replace('%', '')) + total_pnl_pct = float(portfolio_metrics.get("总盈亏率", "0%").replace("%", "")) if total_pnl_pct < -20: print(f"📉 总盈亏率为 {total_pnl_pct:.1f}%,建议考虑止损策略") elif total_pnl_pct > 50: @@ -518,29 +513,29 @@ def main(): """Main function to run all examples""" print("🚀 Crypto Trading Plugin Examples") print("=" * 50) - + try: # Basic usage basic_usage_example() - + # Technical analysis technical_analysis_example() - + # Trading example trading_example() - + # Risk management risk_management_example() - + # Positions query positions_example() - + # Futures metrics calculation futures_metrics_example() - + except Exception as e: print(f"Error running examples: {e}") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/vertex_flow/plugins/crypto_trading/exchanges.py b/vertex_flow/plugins/crypto_trading/exchanges.py index 9e444c7..60e8a94 100644 --- a/vertex_flow/plugins/crypto_trading/exchanges.py +++ b/vertex_flow/plugins/crypto_trading/exchanges.py @@ -2,15 +2,16 @@ Exchange API clients for OKX and Binance """ +import base64 import hashlib import hmac -import base64 +import json import time from abc import ABC, abstractmethod -from typing import Dict, List, Any, Optional -import requests -import json +from typing import Any, Dict, List, Optional from urllib.parse import urlencode + +import requests from error_handler import ErrorHandler try: @@ -21,41 +22,43 @@ class BaseExchange(ABC): """Base class for exchange API clients""" - + def __init__(self, config: ExchangeConfig): self.config = config self.session = requests.Session() - + @abstractmethod def get_account_info(self) -> Dict[str, Any]: """Get account information""" pass - + @abstractmethod def get_trading_fees(self, symbol: str) -> Dict[str, float]: """Get trading fees for a symbol""" pass - + @abstractmethod def get_ticker(self, symbol: str) -> Dict[str, Any]: """Get ticker information""" pass - + @abstractmethod def get_klines(self, symbol: str, interval: str, limit: int = 100) -> List[List]: """Get kline/candlestick data""" pass - + @abstractmethod - def place_order(self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None) -> Dict[str, Any]: + def place_order( + self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None + ) -> Dict[str, Any]: """Place an order""" pass - + @abstractmethod def get_spot_positions(self) -> Dict[str, Any]: """Get spot positions/balances""" pass - + @abstractmethod def get_futures_positions(self) -> Dict[str, Any]: """Get futures positions""" @@ -64,7 +67,7 @@ def get_futures_positions(self) -> Dict[str, Any]: class OKXClient(BaseExchange): """OKX exchange API client""" - + def __init__(self, config: ExchangeConfig): super().__init__(config) # 根据沙盒模式选择API URL @@ -73,19 +76,15 @@ def __init__(self, config: ExchangeConfig): else: self.base_url = config.base_url or "https://www.okx.com" self.api_url = f"{self.base_url}/api/v5" - + def _generate_signature(self, timestamp: str, method: str, request_path: str, body: str = "") -> str: """Generate OKX API signature""" message = timestamp + method + request_path + body signature = base64.b64encode( - hmac.new( - self.config.secret_key.encode('utf-8'), - message.encode('utf-8'), - hashlib.sha256 - ).digest() - ).decode('utf-8') + hmac.new(self.config.secret_key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).digest() + ).decode("utf-8") return signature - + def _get_server_time(self) -> str: """获取OKX服务器时间""" try: @@ -97,79 +96,80 @@ def _get_server_time(self) -> str: timestamp_ms = data["data"][0]["ts"] # 转换为ISO8601格式 import datetime + dt = datetime.datetime.fromtimestamp(int(timestamp_ms) / 1000, tz=datetime.timezone.utc) - return dt.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' + return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" except Exception as e: print(f"获取服务器时间失败: {e}") - + # 如果获取服务器时间失败,使用本地UTC时间 import datetime - return datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' - def _make_request(self, method: str, endpoint: str, params: Optional[Dict] = None, data: Optional[Dict] = None) -> Dict[str, Any]: + return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + + def _make_request( + self, method: str, endpoint: str, params: Optional[Dict] = None, data: Optional[Dict] = None + ) -> Dict[str, Any]: """Make authenticated request to OKX API""" # 使用服务器时间确保时间戳准确 timestamp = self._get_server_time() # 签名需要完整的API路径,包含/api/v5前缀 request_path = "/api/v5" + endpoint - + if params: request_path += "?" + urlencode(params) - - body = json.dumps(data, separators=(',', ':')) if data else "" + + body = json.dumps(data, separators=(",", ":")) if data else "" signature = self._generate_signature(timestamp, method.upper(), request_path, body) - + headers = { "OK-ACCESS-KEY": self.config.api_key, "OK-ACCESS-SIGN": signature, "OK-ACCESS-TIMESTAMP": timestamp, "OK-ACCESS-PASSPHRASE": self.config.passphrase, - "Content-Type": "application/json" + "Content-Type": "application/json", } - + url = self.api_url + endpoint - + try: if method.upper() == "GET": response = self.session.get(url, headers=headers, params=params) else: response = self.session.post(url, headers=headers, json=data) - + response.raise_for_status() - + # 检查响应内容是否为空 if not response.text.strip(): return {"error": "Empty response from server"} - + return response.json() except requests.exceptions.RequestException as e: - error_info = ErrorHandler.format_api_error("OKX", e, getattr(e, 'response', None)) + error_info = ErrorHandler.format_api_error("OKX", e, getattr(e, "response", None)) print(f"❌ OKX API请求失败: {ErrorHandler.get_user_friendly_message(error_info)}") return error_info except json.JSONDecodeError as e: - error_info = ErrorHandler.format_api_error("OKX", e, response if 'response' in locals() else None) + error_info = ErrorHandler.format_api_error("OKX", e, response if "response" in locals() else None) print(f"❌ OKX数据解析失败: {ErrorHandler.get_user_friendly_message(error_info)}") return error_info except Exception as e: error_info = ErrorHandler.format_api_error("OKX", e) print(f"❌ OKX未知错误: {ErrorHandler.get_user_friendly_message(error_info)}") return error_info - + def get_account_info(self) -> Dict[str, Any]: """Get OKX account information""" return self._make_request("GET", "/account/balance") - + def get_trading_fees(self, symbol: str) -> Dict[str, float]: """Get OKX trading fees""" response = self._make_request("GET", "/account/trade-fee", {"instType": "SPOT", "instId": symbol}) if response.get("data"): fee_data = response["data"][0] - return { - "maker_fee": float(fee_data.get("maker", 0)), - "taker_fee": float(fee_data.get("taker", 0)) - } + return {"maker_fee": float(fee_data.get("maker", 0)), "taker_fee": float(fee_data.get("taker", 0))} return {"maker_fee": 0.001, "taker_fee": 0.001} # Default fees - + def get_ticker(self, symbol: str) -> Dict[str, Any]: """Get OKX ticker information""" response = self._make_request("GET", "/market/ticker", {"instId": symbol}) @@ -181,7 +181,7 @@ def get_ticker(self, symbol: str) -> Dict[str, Any]: "bid": float(ticker["bidPx"]), "ask": float(ticker["askPx"]), "volume": float(ticker["vol24h"]), - "change": float(ticker["sodUtc8"]) + "change": float(ticker["sodUtc8"]), } # 如果没有数据或请求失败,返回错误信息 if "error" in response: @@ -189,63 +189,63 @@ def get_ticker(self, symbol: str) -> Dict[str, Any]: return response else: return {"error": "Failed to get ticker data"} - + def get_klines(self, symbol: str, interval: str, limit: int = 100) -> List[List]: """Get OKX kline data""" # OKX interval mapping interval_map = { - "1m": "1m", "5m": "5m", "15m": "15m", "30m": "30m", - "1h": "1H", "4h": "4H", "1d": "1D", "1w": "1W" + "1m": "1m", + "5m": "5m", + "15m": "15m", + "30m": "30m", + "1h": "1H", + "4h": "4H", + "1d": "1D", + "1w": "1W", } - + okx_interval = interval_map.get(interval, "1m") - response = self._make_request("GET", "/market/candles", { - "instId": symbol, - "bar": okx_interval, - "limit": str(limit) - }) - + response = self._make_request( + "GET", "/market/candles", {"instId": symbol, "bar": okx_interval, "limit": str(limit)} + ) + if response.get("data"): return [[float(x) for x in candle] for candle in response["data"]] return [] - - def place_order(self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None) -> Dict[str, Any]: + + def place_order( + self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None + ) -> Dict[str, Any]: """Place order on OKX""" order_data = { "instId": symbol, "tdMode": "cash", "side": side.lower(), "ordType": "market" if order_type.lower() == "market" else "limit", - "sz": str(quantity) + "sz": str(quantity), } - + if price and order_type.lower() == "limit": order_data["px"] = str(price) - + return self._make_request("POST", "/trade/order", data=order_data) - + def get_order_status(self, order_id: str, symbol: str) -> Dict[str, Any]: """查询订单状态""" try: - params = { - "ordId": order_id, - "instId": symbol - } - + params = {"ordId": order_id, "instId": symbol} + response = self._make_request("GET", "/trade/order", params) - + if response.get("code") == "0" and response.get("data"): order_info = response["data"][0] - return { - "success": True, - "data": order_info - } + return {"success": True, "data": order_info} else: return {"error": f"OKX API error: {response.get('msg', 'Unknown error')}"} - + except Exception as e: return {"error": f"Failed to get OKX order status: {str(e)}"} - + def get_spot_positions(self) -> Dict[str, Any]: """Get spot account balance""" try: @@ -255,21 +255,20 @@ def get_spot_positions(self) -> Dict[str, Any]: for account in response["data"]: for detail in account.get("details", []): if float(detail.get("eq", 0) or 0) > 0: # 只返回有余额的币种 - positions.append({ - "currency": detail["ccy"], - "balance": float(detail.get("eq", 0) or 0), - "available": float(detail.get("availEq", 0) or 0), - "frozen": float(detail.get("frozenBal", 0) or 0) - }) - return { - "success": True, - "data": positions - } + positions.append( + { + "currency": detail["ccy"], + "balance": float(detail.get("eq", 0) or 0), + "available": float(detail.get("availEq", 0) or 0), + "frozen": float(detail.get("frozenBal", 0) or 0), + } + ) + return {"success": True, "data": positions} else: return {"error": f"OKX API error: {response.get('msg', 'Unknown error')}"} except Exception as e: return {"error": f"Failed to get OKX spot positions: {str(e)}"} - + def get_futures_positions(self) -> Dict[str, Any]: """Get futures positions""" try: @@ -279,91 +278,89 @@ def get_futures_positions(self) -> Dict[str, Any]: for position in response["data"]: pos_size = position.get("pos", "0") if pos_size and pos_size != "0" and float(pos_size) != 0: # 只显示有持仓的合约 - positions.append({ - "symbol": position["instId"], - "side": position["posSide"], - "size": float(pos_size), - "notional": float(position.get("notionalUsd", 0) or 0), - "unrealized_pnl": float(position.get("upl", 0) or 0), - "margin": float(position.get("margin", 0) or 0) - }) - return { - "success": True, - "data": positions - } + positions.append( + { + "symbol": position["instId"], + "side": position["posSide"], + "size": float(pos_size), + "notional": float(position.get("notionalUsd", 0) or 0), + "unrealized_pnl": float(position.get("upl", 0) or 0), + "margin": float(position.get("margin", 0) or 0), + } + ) + return {"success": True, "data": positions} else: return {"error": f"OKX API error: {response.get('msg', 'Unknown error')}"} - + except Exception as e: return {"error": f"Failed to get OKX futures positions: {str(e)}"} class BinanceClient(BaseExchange): """Binance exchange API client""" - + def __init__(self, config: ExchangeConfig): super().__init__(config) - self.base_url = config.base_url or ("https://api.binance.com" if not config.sandbox else "https://testnet.binance.vision") + self.base_url = config.base_url or ( + "https://api.binance.com" if not config.sandbox else "https://testnet.binance.vision" + ) self.api_url = f"{self.base_url}/api/v3" - + def _generate_signature(self, query_string: str) -> str: """Generate Binance API signature""" return hmac.new( - self.config.secret_key.encode('utf-8'), - query_string.encode('utf-8'), - hashlib.sha256 + self.config.secret_key.encode("utf-8"), query_string.encode("utf-8"), hashlib.sha256 ).hexdigest() - - def _make_request(self, method: str, endpoint: str, params: Optional[Dict] = None, signed: bool = False) -> Dict[str, Any]: + + def _make_request( + self, method: str, endpoint: str, params: Optional[Dict] = None, signed: bool = False + ) -> Dict[str, Any]: """Make request to Binance API""" params = params or {} - + if signed: - params['timestamp'] = int(time.time() * 1000) + params["timestamp"] = int(time.time() * 1000) query_string = urlencode(params) signature = self._generate_signature(query_string) - params['signature'] = signature - + params["signature"] = signature + headers = {"X-MBX-APIKEY": self.config.api_key} if signed else {} url = self.api_url + endpoint - + try: if method.upper() == "GET": response = self.session.get(url, headers=headers, params=params) else: response = self.session.post(url, headers=headers, params=params) - + response.raise_for_status() return response.json() - + except requests.exceptions.RequestException as e: - error_info = ErrorHandler.format_api_error("Binance", e, getattr(e, 'response', None)) + error_info = ErrorHandler.format_api_error("Binance", e, getattr(e, "response", None)) print(f"❌ Binance API请求失败: {ErrorHandler.get_user_friendly_message(error_info)}") return error_info except json.JSONDecodeError as e: - error_info = ErrorHandler.format_api_error("Binance", e, response if 'response' in locals() else None) + error_info = ErrorHandler.format_api_error("Binance", e, response if "response" in locals() else None) print(f"❌ Binance数据解析失败: {ErrorHandler.get_user_friendly_message(error_info)}") return error_info except Exception as e: error_info = ErrorHandler.format_api_error("Binance", e) print(f"❌ Binance未知错误: {ErrorHandler.get_user_friendly_message(error_info)}") return error_info - + def get_account_info(self) -> Dict[str, Any]: """Get Binance account information""" return self._make_request("GET", "/account", signed=True) - + def get_trading_fees(self, symbol: str) -> Dict[str, float]: """Get Binance trading fees""" response = self._make_request("GET", "/account", signed=True) maker_commission = response.get("makerCommission", 10) / 10000 taker_commission = response.get("takerCommission", 10) / 10000 - - return { - "maker_fee": maker_commission, - "taker_fee": taker_commission - } - + + return {"maker_fee": maker_commission, "taker_fee": taker_commission} + def get_ticker(self, symbol: str) -> Dict[str, Any]: """Get Binance ticker information""" try: @@ -374,102 +371,91 @@ def get_ticker(self, symbol: str) -> Dict[str, Any]: "bid": float(response["bidPrice"]), "ask": float(response["askPrice"]), "volume": float(response["volume"]), - "change": float(response["priceChangePercent"]) + "change": float(response["priceChangePercent"]), } except Exception as e: return {"error": f"Failed to get ticker: {str(e)}"} - + def get_klines(self, symbol: str, interval: str, limit: int = 100) -> List[List]: """Get Binance kline data""" # Binance interval mapping interval_map = { - "1m": "1m", "5m": "5m", "15m": "15m", "30m": "30m", - "1h": "1h", "4h": "4h", "1d": "1d", "1w": "1w" + "1m": "1m", + "5m": "5m", + "15m": "15m", + "30m": "30m", + "1h": "1h", + "4h": "4h", + "1d": "1d", + "1w": "1w", } - + binance_interval = interval_map.get(interval, "1m") - response = self._make_request("GET", "/klines", { - "symbol": symbol, - "interval": binance_interval, - "limit": limit - }) - + response = self._make_request( + "GET", "/klines", {"symbol": symbol, "interval": binance_interval, "limit": limit} + ) + return [[float(x) for x in candle[:6]] for candle in response] - - def place_order(self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None) -> Dict[str, Any]: + + def place_order( + self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None + ) -> Dict[str, Any]: """Place order on Binance""" - order_params = { - "symbol": symbol, - "side": side.upper(), - "type": order_type.upper(), - "quantity": quantity - } - + order_params = {"symbol": symbol, "side": side.upper(), "type": order_type.upper(), "quantity": quantity} + if price and order_type.upper() == "LIMIT": order_params["price"] = price order_params["timeInForce"] = "GTC" - + return self._make_request("POST", "/order", order_params, signed=True) - + def get_spot_positions(self) -> Dict[str, Any]: """Get spot positions/balances from Binance""" try: response = self._make_request("GET", "/account", signed=True) - + positions = {} balances = response.get("balances", []) - + for balance in balances: asset = balance.get("asset", "") free = float(balance.get("free", "0")) locked = float(balance.get("locked", "0")) total = free + locked - + if total > 0: # 只显示有余额的币种 - positions[asset] = { - "currency": asset, - "available": free, - "frozen": locked, - "total": total - } - - return { - "exchange": "binance", - "type": "spot", - "positions": positions, - "timestamp": time.time() - } - + positions[asset] = {"currency": asset, "available": free, "frozen": locked, "total": total} + + return {"exchange": "binance", "type": "spot", "positions": positions, "timestamp": time.time()} + except Exception as e: return {"error": f"Failed to get Binance spot positions: {str(e)}"} - + def get_futures_positions(self) -> Dict[str, Any]: """Get futures positions from Binance""" try: # Binance futures API endpoint futures_base_url = "https://fapi.binance.com/fapi/v2" - + # 构建请求参数 params = {"timestamp": int(time.time() * 1000)} query_string = urlencode(params) signature = self._generate_signature(query_string) params["signature"] = signature - - headers = { - "X-MBX-APIKEY": self.config.api_key - } - + + headers = {"X-MBX-APIKEY": self.config.api_key} + url = f"{futures_base_url}/positionRisk" response = self.session.get(url, headers=headers, params=params) response.raise_for_status() positions_data = response.json() - + positions = {} - + for position in positions_data: symbol = position.get("symbol", "") position_amt = float(position.get("positionAmt", "0")) - + if position_amt != 0: # 只显示有持仓的合约 positions[symbol] = { "symbol": symbol, @@ -483,15 +469,10 @@ def get_futures_positions(self) -> Dict[str, Any]: "percentage": float(position.get("percentage", "0")), "leverage": float(position.get("leverage", "0")), "margin_type": position.get("marginType", ""), - "isolated_margin": float(position.get("isolatedMargin", "0")) + "isolated_margin": float(position.get("isolatedMargin", "0")), } - - return { - "exchange": "binance", - "type": "futures", - "positions": positions, - "timestamp": time.time() - } - + + return {"exchange": "binance", "type": "futures", "positions": positions, "timestamp": time.time()} + except Exception as e: - return {"error": f"Failed to get Binance futures positions: {str(e)}"} \ No newline at end of file + return {"error": f"Failed to get Binance futures positions: {str(e)}"} diff --git a/vertex_flow/plugins/crypto_trading/indicators.py b/vertex_flow/plugins/crypto_trading/indicators.py index d7f4ec6..bd086ea 100644 --- a/vertex_flow/plugins/crypto_trading/indicators.py +++ b/vertex_flow/plugins/crypto_trading/indicators.py @@ -2,32 +2,33 @@ Technical indicators calculation module for crypto trading """ -import pandas as pd +from typing import Any, Dict, List, Optional, Tuple + import numpy as np -from typing import List, Dict, Any, Optional, Tuple +import pandas as pd class TechnicalIndicators: """Technical indicators calculator""" - + @staticmethod def prepare_dataframe(klines: List[List]) -> pd.DataFrame: """ Convert klines data to pandas DataFrame - + Args: klines: List of kline data from exchange API - + Returns: DataFrame with OHLCV data """ if not klines: return pd.DataFrame() - + # Handle different exchange formats # OKX: [timestamp, open, high, low, close, volume, volCcy, volCcyQuote, confirm] # Binance: [timestamp, open, high, low, close, volume, ...] - + # Extract only the first 6 columns we need: timestamp, open, high, low, close, volume processed_klines = [] for kline in klines: @@ -36,30 +37,30 @@ def prepare_dataframe(klines: List[List]) -> pd.DataFrame: else: # Skip incomplete data continue - + if not processed_klines: return pd.DataFrame() - - df = pd.DataFrame(processed_klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) - df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') - df.set_index('timestamp', inplace=True) - + + df = pd.DataFrame(processed_klines, columns=["timestamp", "open", "high", "low", "close", "volume"]) + df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms") + df.set_index("timestamp", inplace=True) + # Convert to numeric - for col in ['open', 'high', 'low', 'close', 'volume']: + for col in ["open", "high", "low", "close", "volume"]: df[col] = pd.to_numeric(df[col]) - + return df.sort_index() - + @staticmethod def sma(data: pd.Series, period: int) -> pd.Series: """Simple Moving Average""" return data.rolling(window=period).mean() - + @staticmethod def ema(data: pd.Series, period: int) -> pd.Series: """Exponential Moving Average""" return data.ewm(span=period).mean() - + @staticmethod def rsi(data: pd.Series, period: int = 14) -> pd.Series: """Relative Strength Index""" @@ -69,7 +70,7 @@ def rsi(data: pd.Series, period: int = 14) -> pd.Series: rs = gain / loss rsi = 100 - (100 / (1 + rs)) return rsi - + @staticmethod def macd(data: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> Dict[str, pd.Series]: """MACD (Moving Average Convergence Divergence)""" @@ -78,255 +79,279 @@ def macd(data: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> Di macd_line = ema_fast - ema_slow signal_line = TechnicalIndicators.ema(macd_line, signal) histogram = macd_line - signal_line - - return { - 'macd': macd_line, - 'signal': signal_line, - 'histogram': histogram - } - + + return {"macd": macd_line, "signal": signal_line, "histogram": histogram} + @staticmethod def bollinger_bands(data: pd.Series, period: int = 20, std_dev: float = 2) -> Dict[str, pd.Series]: """Bollinger Bands""" sma = TechnicalIndicators.sma(data, period) std = data.rolling(window=period).std() - - return { - 'upper': sma + (std * std_dev), - 'middle': sma, - 'lower': sma - (std * std_dev) - } - + + return {"upper": sma + (std * std_dev), "middle": sma, "lower": sma - (std * std_dev)} + @staticmethod - def stochastic(high: pd.Series, low: pd.Series, close: pd.Series, - k_period: int = 14, d_period: int = 3) -> Dict[str, pd.Series]: + def stochastic( + high: pd.Series, low: pd.Series, close: pd.Series, k_period: int = 14, d_period: int = 3 + ) -> Dict[str, pd.Series]: """Stochastic Oscillator""" lowest_low = low.rolling(window=k_period).min() highest_high = high.rolling(window=k_period).max() - + k_percent = 100 * ((close - lowest_low) / (highest_high - lowest_low)) d_percent = k_percent.rolling(window=d_period).mean() - - return { - 'k': k_percent, - 'd': d_percent - } - + + return {"k": k_percent, "d": d_percent} + @staticmethod def atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series: """Average True Range""" high_low = high - low high_close = np.abs(high - close.shift()) low_close = np.abs(low - close.shift()) - + true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1) return true_range.rolling(window=period).mean() - + @staticmethod def williams_r(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series: """Williams %R""" highest_high = high.rolling(window=period).max() lowest_low = low.rolling(window=period).min() - + return -100 * ((highest_high - close) / (highest_high - lowest_low)) - + @staticmethod def cci(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 20) -> pd.Series: """Commodity Channel Index""" typical_price = (high + low + close) / 3 sma_tp = typical_price.rolling(window=period).mean() - mean_deviation = typical_price.rolling(window=period).apply( - lambda x: np.abs(x - x.mean()).mean() - ) - + mean_deviation = typical_price.rolling(window=period).apply(lambda x: np.abs(x - x.mean()).mean()) + return (typical_price - sma_tp) / (0.015 * mean_deviation) - + @staticmethod def obv(close: pd.Series, volume: pd.Series) -> pd.Series: """On-Balance Volume""" obv = pd.Series(index=close.index, dtype=float) obv.iloc[0] = volume.iloc[0] - + for i in range(1, len(close)): - if close.iloc[i] > close.iloc[i-1]: - obv.iloc[i] = obv.iloc[i-1] + volume.iloc[i] - elif close.iloc[i] < close.iloc[i-1]: - obv.iloc[i] = obv.iloc[i-1] - volume.iloc[i] + if close.iloc[i] > close.iloc[i - 1]: + obv.iloc[i] = obv.iloc[i - 1] + volume.iloc[i] + elif close.iloc[i] < close.iloc[i - 1]: + obv.iloc[i] = obv.iloc[i - 1] - volume.iloc[i] else: - obv.iloc[i] = obv.iloc[i-1] - + obv.iloc[i] = obv.iloc[i - 1] + return obv - + @staticmethod def support_resistance(data: pd.Series, window: int = 20) -> Dict[str, List[float]]: """Find support and resistance levels""" highs = data.rolling(window=window, center=True).max() lows = data.rolling(window=window, center=True).min() - + resistance_levels = [] support_levels = [] - + for i in range(window, len(data) - window): if data.iloc[i] == highs.iloc[i]: resistance_levels.append(data.iloc[i]) if data.iloc[i] == lows.iloc[i]: support_levels.append(data.iloc[i]) - + return { - 'resistance': sorted(list(set(resistance_levels)), reverse=True)[:5], - 'support': sorted(list(set(support_levels)))[:5] + "resistance": sorted(list(set(resistance_levels)), reverse=True)[:5], + "support": sorted(list(set(support_levels)))[:5], } - + @classmethod def calculate_all_indicators(cls, klines: List[List]) -> Dict[str, Any]: """ Calculate all technical indicators for given klines data - + Args: klines: List of kline data - + Returns: Dictionary containing all calculated indicators """ if not klines: return {} - + df = cls.prepare_dataframe(klines) if df.empty: return {} - + indicators = {} - + try: + # Helper function to safely get last value + def safe_get_last(series_or_value): + if hasattr(series_or_value, "iloc"): + return series_or_value.iloc[-1] + return series_or_value + # Moving Averages - indicators['sma_20'] = cls.sma(df['close'], 20).iloc[-1] if len(df) >= 20 else None - indicators['sma_50'] = cls.sma(df['close'], 50).iloc[-1] if len(df) >= 50 else None - indicators['ema_12'] = cls.ema(df['close'], 12).iloc[-1] if len(df) >= 12 else None - indicators['ema_26'] = cls.ema(df['close'], 26).iloc[-1] if len(df) >= 26 else None - + if len(df) >= 20: + sma_20 = cls.sma(df["close"], 20) + indicators["sma_20"] = safe_get_last(sma_20) + else: + indicators["sma_20"] = None + + if len(df) >= 50: + sma_50 = cls.sma(df["close"], 50) + indicators["sma_50"] = safe_get_last(sma_50) + else: + indicators["sma_50"] = None + + if len(df) >= 12: + ema_12 = cls.ema(df["close"], 12) + indicators["ema_12"] = safe_get_last(ema_12) + else: + indicators["ema_12"] = None + + if len(df) >= 26: + ema_26 = cls.ema(df["close"], 26) + indicators["ema_26"] = safe_get_last(ema_26) + else: + indicators["ema_26"] = None + # RSI if len(df) >= 14: - rsi_values = cls.rsi(df['close'], 14) - indicators['rsi'] = rsi_values.iloc[-1] - + rsi_values = cls.rsi(df["close"], 14) + indicators["rsi"] = safe_get_last(rsi_values) + else: + indicators["rsi"] = None + # MACD if len(df) >= 26: - macd_data = cls.macd(df['close']) - indicators['macd'] = { - 'macd': macd_data['macd'].iloc[-1], - 'signal': macd_data['signal'].iloc[-1], - 'histogram': macd_data['histogram'].iloc[-1] + macd_data = cls.macd(df["close"]) + indicators["macd"] = { + "macd": safe_get_last(macd_data["macd"]), + "signal": safe_get_last(macd_data["signal"]), + "histogram": safe_get_last(macd_data["histogram"]), } - + else: + indicators["macd"] = None + # Bollinger Bands if len(df) >= 20: - bb_data = cls.bollinger_bands(df['close']) - indicators['bollinger_bands'] = { - 'upper': bb_data['upper'].iloc[-1], - 'middle': bb_data['middle'].iloc[-1], - 'lower': bb_data['lower'].iloc[-1] + bb_data = cls.bollinger_bands(df["close"]) + indicators["bollinger_bands"] = { + "upper": safe_get_last(bb_data["upper"]), + "middle": safe_get_last(bb_data["middle"]), + "lower": safe_get_last(bb_data["lower"]), } - + else: + indicators["bollinger_bands"] = None + # Stochastic if len(df) >= 14: - stoch_data = cls.stochastic(df['high'], df['low'], df['close']) - indicators['stochastic'] = { - 'k': stoch_data['k'].iloc[-1], - 'd': stoch_data['d'].iloc[-1] - } - + stoch_data = cls.stochastic(df["high"], df["low"], df["close"]) + indicators["stochastic"] = {"k": safe_get_last(stoch_data["k"]), "d": safe_get_last(stoch_data["d"])} + else: + indicators["stochastic"] = None + # ATR if len(df) >= 14: - atr_values = cls.atr(df['high'], df['low'], df['close']) - indicators['atr'] = atr_values.iloc[-1] - + atr_values = cls.atr(df["high"], df["low"], df["close"]) + indicators["atr"] = safe_get_last(atr_values) + else: + indicators["atr"] = None + # Williams %R if len(df) >= 14: - williams_r_values = cls.williams_r(df['high'], df['low'], df['close']) - indicators['williams_r'] = williams_r_values.iloc[-1] - + williams_r_values = cls.williams_r(df["high"], df["low"], df["close"]) + indicators["williams_r"] = safe_get_last(williams_r_values) + else: + indicators["williams_r"] = None + # Support and Resistance if len(df) >= 40: - sr_levels = cls.support_resistance(df['close']) - indicators['support_resistance'] = sr_levels - + sr_levels = cls.support_resistance(df["close"]) + indicators["support_resistance"] = sr_levels + else: + indicators["support_resistance"] = None + # Current price info - indicators['current_price'] = df['close'].iloc[-1] - indicators['volume'] = df['volume'].iloc[-1] - indicators['high_24h'] = df['high'].max() - indicators['low_24h'] = df['low'].min() - + indicators["current_price"] = safe_get_last(df["close"]) + indicators["volume"] = safe_get_last(df["volume"]) + indicators["high_24h"] = df["high"].max() + indicators["low_24h"] = df["low"].min() + except Exception as e: - indicators['error'] = f"Error calculating indicators: {str(e)}" - + indicators["error"] = f"Error calculating indicators: {str(e)}" + return indicators - + @classmethod def get_trading_signals(cls, indicators: Dict[str, Any]) -> Dict[str, str]: """ Generate trading signals based on technical indicators - + Args: indicators: Dictionary of calculated indicators - + Returns: Dictionary of trading signals """ signals = {} - + try: # RSI signals - if 'rsi' in indicators and indicators['rsi'] is not None: - rsi = indicators['rsi'] + if "rsi" in indicators and indicators["rsi"] is not None: + rsi = indicators["rsi"] if rsi > 70: - signals['rsi'] = 'SELL' + signals["rsi"] = "SELL" elif rsi < 30: - signals['rsi'] = 'BUY' + signals["rsi"] = "BUY" else: - signals['rsi'] = 'HOLD' - + signals["rsi"] = "HOLD" + # MACD signals - if 'macd' in indicators and indicators['macd'] is not None: - macd_data = indicators['macd'] - if macd_data['macd'] > macd_data['signal']: - signals['macd'] = 'BUY' + if "macd" in indicators and indicators["macd"] is not None: + macd_data = indicators["macd"] + if macd_data["macd"] > macd_data["signal"]: + signals["macd"] = "BUY" else: - signals['macd'] = 'SELL' - + signals["macd"] = "SELL" + # Bollinger Bands signals - if 'bollinger_bands' in indicators and 'current_price' in indicators: - bb = indicators['bollinger_bands'] - price = indicators['current_price'] - - if price > bb['upper']: - signals['bollinger_bands'] = 'SELL' - elif price < bb['lower']: - signals['bollinger_bands'] = 'BUY' + if "bollinger_bands" in indicators and "current_price" in indicators: + bb = indicators["bollinger_bands"] + price = indicators["current_price"] + + if price > bb["upper"]: + signals["bollinger_bands"] = "SELL" + elif price < bb["lower"]: + signals["bollinger_bands"] = "BUY" else: - signals['bollinger_bands'] = 'HOLD' - + signals["bollinger_bands"] = "HOLD" + # Stochastic signals - if 'stochastic' in indicators: - stoch = indicators['stochastic'] - if stoch['k'] > 80: - signals['stochastic'] = 'SELL' - elif stoch['k'] < 20: - signals['stochastic'] = 'BUY' + if "stochastic" in indicators: + stoch = indicators["stochastic"] + if stoch["k"] > 80: + signals["stochastic"] = "SELL" + elif stoch["k"] < 20: + signals["stochastic"] = "BUY" else: - signals['stochastic'] = 'HOLD' - + signals["stochastic"] = "HOLD" + # Overall signal (simple majority vote) - buy_signals = sum(1 for signal in signals.values() if signal == 'BUY') - sell_signals = sum(1 for signal in signals.values() if signal == 'SELL') - + buy_signals = sum(1 for signal in signals.values() if signal == "BUY") + sell_signals = sum(1 for signal in signals.values() if signal == "SELL") + if buy_signals > sell_signals: - signals['overall'] = 'BUY' + signals["overall"] = "BUY" elif sell_signals > buy_signals: - signals['overall'] = 'SELL' + signals["overall"] = "SELL" else: - signals['overall'] = 'HOLD' - + signals["overall"] = "HOLD" + except Exception as e: - signals['error'] = f"Error generating signals: {str(e)}" - - return signals \ No newline at end of file + signals["error"] = f"Error generating signals: {str(e)}" + + return signals diff --git a/vertex_flow/plugins/crypto_trading/position_metrics.py b/vertex_flow/plugins/crypto_trading/position_metrics.py index 6f4916f..ef9c77d 100644 --- a/vertex_flow/plugins/crypto_trading/position_metrics.py +++ b/vertex_flow/plugins/crypto_trading/position_metrics.py @@ -4,22 +4,22 @@ 计算各种持仓相关的风险和收益指标 """ -from typing import Dict, List, Any, Optional import math +from typing import Any, Dict, List, Optional class PositionMetrics: """合约持仓指标计算类""" - + @staticmethod def calculate_position_metrics(position: Dict[str, Any], exchange: str = "okx") -> Dict[str, Any]: """ 计算单个持仓的各项指标 - + Args: position: 持仓数据 exchange: 交易所名称 - + Returns: 包含各项指标的字典 """ @@ -29,13 +29,13 @@ def calculate_position_metrics(position: Dict[str, Any], exchange: str = "okx") side = position.get("side", "") size = float(position.get("size", 0)) unrealized_pnl = float(position.get("unrealized_pnl", 0)) - + # 根据交易所获取不同字段 if exchange.lower() == "okx": notional = float(position.get("notional", 0)) margin = float(position.get("margin", 0)) entry_price = 0 # OKX API 没有直接提供入场价格 - mark_price = 0 # OKX API 没有直接提供标记价格 + mark_price = 0 # OKX API 没有直接提供标记价格 leverage = notional / margin if margin > 0 else 0 else: # binance notional = float(position.get("notional", 0)) @@ -43,7 +43,7 @@ def calculate_position_metrics(position: Dict[str, Any], exchange: str = "okx") entry_price = float(position.get("entry_price", 0)) mark_price = float(position.get("mark_price", 0)) leverage = float(position.get("leverage", 0)) - + # 计算各项指标 metrics = { "symbol": symbol, @@ -56,13 +56,13 @@ def calculate_position_metrics(position: Dict[str, Any], exchange: str = "okx") "entry_price": entry_price, "mark_price": mark_price, } - + # 计算盈亏率 if margin > 0: metrics["pnl_percentage"] = (unrealized_pnl / margin) * 100 else: metrics["pnl_percentage"] = 0 - + # 计算价格变化率(仅当有入场价格和标记价格时) if entry_price > 0 and mark_price > 0: price_change = ((mark_price - entry_price) / entry_price) * 100 @@ -71,87 +71,87 @@ def calculate_position_metrics(position: Dict[str, Any], exchange: str = "okx") metrics["price_change_percentage"] = price_change else: metrics["price_change_percentage"] = 0 - + # 风险等级评估 metrics["risk_level"] = PositionMetrics._assess_risk_level( leverage, abs(metrics["pnl_percentage"]), abs(notional) ) - + # 持仓价值占比(需要总资产信息,这里先设为0) metrics["position_weight"] = 0 - + return metrics - + except Exception as e: return {"error": f"Failed to calculate metrics for position: {str(e)}"} - + @staticmethod def calculate_portfolio_metrics(positions: List[Dict[str, Any]], total_balance: float = 0) -> Dict[str, Any]: """ 计算整个投资组合的指标 - + Args: positions: 所有持仓的指标列表 total_balance: 总资产余额 - + Returns: 投资组合指标字典 """ try: if not positions: return {"error": "No positions to calculate"} - + # 过滤掉有错误的持仓 valid_positions = [p for p in positions if "error" not in p] - + if not valid_positions: return {"error": "No valid positions to calculate"} - + # 基础统计 total_positions = len(valid_positions) long_positions = len([p for p in valid_positions if p["side"].lower() == "long"]) short_positions = len([p for p in valid_positions if p["side"].lower() == "short"]) - + # 盈亏统计 total_unrealized_pnl = sum(p["unrealized_pnl"] for p in valid_positions) profitable_positions = len([p for p in valid_positions if p["unrealized_pnl"] > 0]) losing_positions = len([p for p in valid_positions if p["unrealized_pnl"] < 0]) - + # 保证金和名义价值统计 total_margin = sum(p["margin_used"] for p in valid_positions) total_notional = sum(p["notional_value"] for p in valid_positions) - + # 计算平均杠杆 leverages = [p["leverage"] for p in valid_positions if p["leverage"] > 0] avg_leverage = sum(leverages) / len(leverages) if leverages else 0 - + # 计算胜率 win_rate = (profitable_positions / total_positions) * 100 if total_positions > 0 else 0 - + # 计算总盈亏率 total_pnl_percentage = (total_unrealized_pnl / total_margin) * 100 if total_margin > 0 else 0 - + # 风险指标 max_single_loss = min([p["unrealized_pnl"] for p in valid_positions], default=0) max_single_gain = max([p["unrealized_pnl"] for p in valid_positions], default=0) - + # 持仓集中度(按名义价值) if total_notional > 0: position_weights = [(p["notional_value"] / total_notional) * 100 for p in valid_positions] max_position_weight = max(position_weights, default=0) - + # 更新每个持仓的权重 for i, position in enumerate(valid_positions): position["position_weight"] = position_weights[i] else: max_position_weight = 0 - + # 风险等级分布 risk_distribution = {"low": 0, "medium": 0, "high": 0, "extreme": 0} for position in valid_positions: risk_level = position.get("risk_level", "medium") risk_distribution[risk_level] += 1 - + portfolio_metrics = { "总持仓数量": total_positions, "多头持仓": long_positions, @@ -168,35 +168,35 @@ def calculate_portfolio_metrics(positions: List[Dict[str, Any]], total_balance: "最大单笔盈利": f"${max_single_gain:.2f}", "最大持仓权重": f"{max_position_weight:.2f}%", "风险等级分布": risk_distribution, - "详细持仓": valid_positions + "详细持仓": valid_positions, } - + # 如果有总资产信息,计算资产利用率 if total_balance > 0: margin_utilization = (total_margin / total_balance) * 100 portfolio_metrics["保证金利用率"] = f"{margin_utilization:.2f}%" portfolio_metrics["账户总资产"] = f"${total_balance:.2f}" - + return portfolio_metrics - + except Exception as e: return {"error": f"Failed to calculate portfolio metrics: {str(e)}"} - + @staticmethod def _assess_risk_level(leverage: float, pnl_percentage: float, notional_value: float) -> str: """ 评估持仓风险等级 - + Args: leverage: 杠杆倍数 pnl_percentage: 盈亏百分比(绝对值) notional_value: 名义价值 - + Returns: 风险等级: low, medium, high, extreme """ risk_score = 0 - + # 杠杆风险评分 if leverage >= 20: risk_score += 3 @@ -204,7 +204,7 @@ def _assess_risk_level(leverage: float, pnl_percentage: float, notional_value: f risk_score += 2 elif leverage >= 5: risk_score += 1 - + # 盈亏风险评分 if pnl_percentage >= 50: risk_score += 3 @@ -212,13 +212,13 @@ def _assess_risk_level(leverage: float, pnl_percentage: float, notional_value: f risk_score += 2 elif pnl_percentage >= 10: risk_score += 1 - + # 持仓规模风险评分 if notional_value >= 10000: risk_score += 2 elif notional_value >= 5000: risk_score += 1 - + # 风险等级判定 if risk_score >= 6: return "extreme" @@ -228,82 +228,77 @@ def _assess_risk_level(leverage: float, pnl_percentage: float, notional_value: f return "medium" else: return "low" - + @staticmethod def format_metrics_display(metrics: Dict[str, Any]) -> str: """ 格式化指标显示 - + Args: metrics: 指标字典 - + Returns: 格式化的显示字符串 """ if "error" in metrics: return f"❌ 计算错误: {metrics['error']}" - + # 如果是投资组合指标 if "总持仓数量" in metrics: return PositionMetrics._format_portfolio_display(metrics) else: return PositionMetrics._format_position_display(metrics) - + @staticmethod def _format_portfolio_display(metrics: Dict[str, Any]) -> str: """格式化投资组合指标显示""" - display = "\n" + "="*60 + "\n" + display = "\n" + "=" * 60 + "\n" display += "📊 投资组合综合指标\n" - display += "="*60 + "\n" - + display += "=" * 60 + "\n" + # 基础统计 display += f"📈 持仓概况:\n" display += f" • 总持仓数量: {metrics['总持仓数量']}\n" display += f" • 多头/空头: {metrics['多头持仓']}/{metrics['空头持仓']}\n" display += f" • 盈利/亏损: {metrics['盈利持仓']}/{metrics['亏损持仓']}\n" display += f" • 胜率: {metrics['胜率']}\n\n" - + # 盈亏指标 display += f"💰 盈亏指标:\n" display += f" • 总未实现盈亏: {metrics['总未实现盈亏']}\n" display += f" • 总盈亏率: {metrics['总盈亏率']}\n" display += f" • 最大单笔盈利: {metrics['最大单笔盈利']}\n" display += f" • 最大单笔亏损: {metrics['最大单笔亏损']}\n\n" - + # 风险指标 display += f"⚠️ 风险指标:\n" display += f" • 总保证金: {metrics['总保证金']}\n" display += f" • 总名义价值: {metrics['总名义价值']}\n" display += f" • 平均杠杆: {metrics['平均杠杆']}\n" display += f" • 最大持仓权重: {metrics['最大持仓权重']}\n" - + if "保证金利用率" in metrics: display += f" • 保证金利用率: {metrics['保证金利用率']}\n" display += f" • 账户总资产: {metrics['账户总资产']}\n" - + # 风险分布 - risk_dist = metrics['风险等级分布'] + risk_dist = metrics["风险等级分布"] display += f"\n🎯 风险等级分布:\n" display += f" • 低风险: {risk_dist['low']} 个\n" display += f" • 中风险: {risk_dist['medium']} 个\n" display += f" • 高风险: {risk_dist['high']} 个\n" display += f" • 极高风险: {risk_dist['extreme']} 个\n" - + return display - + @staticmethod def _format_position_display(metrics: Dict[str, Any]) -> str: """格式化单个持仓指标显示""" - risk_emoji = { - "low": "🟢", - "medium": "🟡", - "high": "🟠", - "extreme": "🔴" - } - + risk_emoji = {"low": "🟢", "medium": "🟡", "high": "🟠", "extreme": "🔴"} + risk_level = metrics.get("risk_level", "medium") emoji = risk_emoji.get(risk_level, "🟡") - + display = f"\n{emoji} {metrics['symbol']} ({metrics['side'].upper()})\n" display += f" • 持仓大小: {metrics['size']:.8f}\n" display += f" • 名义价值: ${metrics['notional_value']:.2f}\n" @@ -311,13 +306,13 @@ def _format_position_display(metrics: Dict[str, Any]) -> str: display += f" • 杠杆: {metrics['leverage']:.2f}x\n" display += f" • 未实现盈亏: ${metrics['unrealized_pnl']:.2f}\n" display += f" • 盈亏率: {metrics['pnl_percentage']:.2f}%\n" - - if metrics['price_change_percentage'] != 0: + + if metrics["price_change_percentage"] != 0: display += f" • 价格变化: {metrics['price_change_percentage']:.2f}%\n" - - if metrics['position_weight'] > 0: + + if metrics["position_weight"] > 0: display += f" • 持仓权重: {metrics['position_weight']:.2f}%\n" - + display += f" • 风险等级: {risk_level.upper()}\n" - - return display \ No newline at end of file + + return display diff --git a/vertex_flow/plugins/crypto_trading/show_indicators.py b/vertex_flow/plugins/crypto_trading/show_indicators.py new file mode 100644 index 0000000..842f1ec --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/show_indicators.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +""" +展示持仓技术指标数据 +""" + +import traceback + +from client import CryptoTradingClient +from config import CryptoTradingConfig +from indicators import TechnicalIndicators + + +def format_number(value): + """格式化数字显示""" + if value is None: + return "N/A" + if isinstance(value, (int, float)): + if abs(value) >= 1000: + return f"{value:,.2f}" + else: + return f"{value:.4f}" + return str(value) + + +def get_signal_emoji(rsi, macd_histogram): + """根据RSI和MACD获取信号表情""" + if rsi is None or macd_histogram is None: + return "⚪" + + if rsi > 70 and macd_histogram < 0: + return "🔴" # 超买且MACD下降,卖出信号 + elif rsi < 30 and macd_histogram > 0: + return "🟢" # 超卖且MACD上升,买入信号 + elif rsi > 50 and macd_histogram > 0: + return "🟡" # 中性偏多 + else: + return "⚪" # 中性 + + +def display_indicators(symbol, indicators, position_info=""): + """显示单个币种的技术指标""" + print(f"\n📊 {symbol}") + if position_info: + print(f" {position_info}") + + if "error" in indicators: + print(f" ❌ 计算指标出错: {indicators['error']}") + return + + current_price = indicators.get("current_price") + if current_price: + print(f" 当前价格: ${format_number(current_price)}") + + # RSI + rsi = indicators.get("rsi") + if rsi is not None: + rsi_status = "超买" if rsi > 70 else "超卖" if rsi < 30 else "正常" + print(f" 📈 RSI(14): {format_number(rsi)} ({rsi_status})") + + # MACD + macd = indicators.get("macd") + if macd and isinstance(macd, dict): + macd_line = macd.get("macd") + signal_line = macd.get("signal") + histogram = macd.get("histogram") + + if all(v is not None for v in [macd_line, signal_line, histogram]): + trend = "上升" if histogram > 0 else "下降" + print( + f" 📊 MACD: {format_number(macd_line)} | 信号: {format_number(signal_line)} | 柱状: {format_number(histogram)} ({trend})" + ) + + # 移动平均线 + sma_20 = indicators.get("sma_20") + ema_12 = indicators.get("ema_12") + if sma_20 is not None: + print(f" 📉 SMA(20): {format_number(sma_20)}") + if ema_12 is not None: + print(f" 📈 EMA(12): {format_number(ema_12)}") + + # 布林带 + bb = indicators.get("bollinger_bands") + if bb and isinstance(bb, dict): + upper = bb.get("upper") + middle = bb.get("middle") + lower = bb.get("lower") + if all(v is not None for v in [upper, middle, lower]): + print( + f" 🎯 布林带: 上轨 {format_number(upper)} | 中轨 {format_number(middle)} | 下轨 {format_number(lower)}" + ) + + # 交易信号 + signal_emoji = get_signal_emoji(rsi, macd.get("histogram") if macd else None) + print(f" {signal_emoji} 综合信号") + + +def main(): + try: + # 初始化客户端 + config = CryptoTradingConfig() + client = CryptoTradingClient(config) + + print("🔸 现货持仓技术指标") + print("=" * 40) + + # 获取现货持仓 + spot_positions = client.get_spot_positions("okx") + + if spot_positions.get("success") and spot_positions.get("data"): + for position in spot_positions["data"]: + currency = position["currency"] + balance = position["balance"] + + # 跳过USDT和余额为0的币种 + if currency != "USDT" and float(balance) > 0: + symbol = f"{currency}-USDT" + position_info = f"持仓量: {format_number(float(balance))} {currency}" + + try: + # 获取K线数据 + klines = client.get_klines("okx", symbol, "1h", 100) + if klines and len(klines) > 26: # 确保有足够数据计算MACD + indicators = TechnicalIndicators.calculate_all_indicators(klines) + display_indicators(symbol, indicators, position_info) + else: + print(f"\n📊 {symbol}") + print(f" {position_info}") + print(f" ❌ K线数据不足 (需要至少26条)") + except Exception as e: + print(f"\n📊 {symbol}") + print(f" {position_info}") + print(f" ❌ 获取数据失败: {str(e)}") + else: + print(" ❌ 无法获取现货持仓数据") + + print("\n\n🔸 合约持仓技术指标") + print("=" * 40) + + # 获取合约持仓 + futures_positions = client.get_futures_positions("okx") + + if futures_positions.get("success") and futures_positions.get("data"): + for position in futures_positions["data"]: + symbol = position["symbol"] + size = position["size"] + side = position["side"] + unrealized_pnl = position.get("unrealized_pnl", 0) + + if float(size) != 0: + position_info = f"方向: {side}, 数量: {format_number(float(size))}" + if unrealized_pnl: + pnl_str = f"${format_number(float(unrealized_pnl))}" + position_info += f"\n 未实现盈亏: {pnl_str}" + + try: + # 获取K线数据 + klines = client.get_klines("okx", symbol, "1h", 100) + if klines and len(klines) > 26: + indicators = TechnicalIndicators.calculate_all_indicators(klines) + display_indicators(symbol, indicators, position_info) + else: + print(f"\n📊 {symbol}") + print(f" {position_info}") + print(f" ❌ K线数据不足 (需要至少26条)") + except Exception as e: + print(f"\n📊 {symbol}") + print(f" {position_info}") + print(f" ❌ 获取数据失败: {str(e)}") + else: + print(" ❌ 无法获取合约持仓数据") + + print("\n" + "=" * 60) + print("报告生成完成 ✅") + print("=" * 60) + + except Exception as e: + print(f"❌ 程序执行出错: {str(e)}") + traceback.print_exc() + + +if __name__ == "__main__": + main() diff --git a/vertex_flow/plugins/crypto_trading/trading.py b/vertex_flow/plugins/crypto_trading/trading.py index d847bb3..e31e962 100644 --- a/vertex_flow/plugins/crypto_trading/trading.py +++ b/vertex_flow/plugins/crypto_trading/trading.py @@ -3,8 +3,8 @@ """ import time -from typing import Dict, Any, List, Optional, Union -from decimal import Decimal, ROUND_DOWN +from decimal import ROUND_DOWN, Decimal +from typing import Any, Dict, List, Optional, Union try: from .client import CryptoTradingClient @@ -16,65 +16,66 @@ class TradingEngine: """Trading engine with risk management and order execution""" - + def __init__(self, client: CryptoTradingClient): self.client = client self.config = client.config - + def calculate_position_size(self, exchange: str, symbol: str, risk_percentage: float = None) -> float: """ Calculate position size based on risk management rules - + Args: exchange: Exchange name symbol: Trading symbol risk_percentage: Risk percentage of total balance (default from config) - + Returns: Position size in base currency """ if risk_percentage is None: risk_percentage = self.config.trading_config.risk_percentage - + # Get account balance balance_info = self.client.get_balance(exchange) if isinstance(balance_info, dict) and "USDT" in balance_info: - usdt_balance = balance_info["USDT"].get("available", 0) if isinstance(balance_info["USDT"], dict) else balance_info["USDT"] + usdt_balance = ( + balance_info["USDT"].get("available", 0) + if isinstance(balance_info["USDT"], dict) + else balance_info["USDT"] + ) else: usdt_balance = 1000 # Default fallback - + # Calculate position size risk_amount = usdt_balance * risk_percentage max_position = min(risk_amount, self.config.trading_config.max_position_size) - + return max_position - + def calculate_stop_loss_take_profit(self, entry_price: float, side: str) -> Dict[str, float]: """ Calculate stop loss and take profit levels - + Args: entry_price: Entry price side: 'buy' or 'sell' - + Returns: Dictionary with stop_loss and take_profit prices """ stop_loss_pct = self.config.trading_config.stop_loss_percentage take_profit_pct = self.config.trading_config.take_profit_percentage - - if side.lower() == 'buy': + + if side.lower() == "buy": stop_loss = entry_price * (1 - stop_loss_pct) take_profit = entry_price * (1 + take_profit_pct) else: # sell stop_loss = entry_price * (1 + stop_loss_pct) take_profit = entry_price * (1 - take_profit_pct) - - return { - 'stop_loss': round(stop_loss, 8), - 'take_profit': round(take_profit, 8) - } - + + return {"stop_loss": round(stop_loss, 8), "take_profit": round(take_profit, 8)} + def format_quantity(self, exchange: str, symbol: str, quantity: float) -> float: """Format quantity according to exchange requirements""" # This is a simplified version - in production, you'd get this from exchange info @@ -84,18 +85,18 @@ def format_quantity(self, exchange: str, symbol: str, quantity: float) -> float: elif exchange == "okx": # OKX has different requirements return round(quantity, 8) - + return round(quantity, 8) - + def buy_market(self, exchange: str, symbol: str, amount_usdt: float) -> Dict[str, Any]: """ Execute market buy order - + Args: exchange: Exchange name symbol: Trading symbol amount_usdt: Amount in USDT to buy - + Returns: Order result dictionary """ @@ -104,25 +105,22 @@ def buy_market(self, exchange: str, symbol: str, amount_usdt: float) -> Dict[str ticker = self.client.get_ticker(exchange, symbol) if "error" in ticker: return {"error": f"Failed to get ticker: {ticker['error']}"} - + current_price = ticker["price"] quantity = amount_usdt / current_price quantity = self.format_quantity(exchange, symbol, quantity) - + # Place market buy order if exchange not in self.client.exchanges: return {"error": f"Exchange {exchange} not configured"} - + order_result = self.client.exchanges[exchange].place_order( - symbol=symbol, - side="buy", - order_type="market", - quantity=quantity + symbol=symbol, side="buy", order_type="market", quantity=quantity ) - + # Calculate stop loss and take profit sl_tp = self.calculate_stop_loss_take_profit(current_price, "buy") - + result = { "status": "success", "exchange": exchange, @@ -135,27 +133,23 @@ def buy_market(self, exchange: str, symbol: str, amount_usdt: float) -> Dict[str "stop_loss": sl_tp["stop_loss"], "take_profit": sl_tp["take_profit"], "order_result": order_result, - "timestamp": time.time() + "timestamp": time.time(), } - + return result - + except Exception as e: - return { - "error": f"Failed to execute buy order: {str(e)}", - "exchange": exchange, - "symbol": symbol - } - + return {"error": f"Failed to execute buy order: {str(e)}", "exchange": exchange, "symbol": symbol} + def sell_market(self, exchange: str, symbol: str, quantity: float) -> Dict[str, Any]: """ Execute market sell order - + Args: exchange: Exchange name symbol: Trading symbol quantity: Quantity to sell - + Returns: Order result dictionary """ @@ -164,24 +158,21 @@ def sell_market(self, exchange: str, symbol: str, quantity: float) -> Dict[str, ticker = self.client.get_ticker(exchange, symbol) if "error" in ticker: return {"error": f"Failed to get ticker: {ticker['error']}"} - + current_price = ticker["price"] quantity = self.format_quantity(exchange, symbol, quantity) - + # Place market sell order if exchange not in self.client.exchanges: return {"error": f"Exchange {exchange} not configured"} - + order_result = self.client.exchanges[exchange].place_order( - symbol=symbol, - side="sell", - order_type="market", - quantity=quantity + symbol=symbol, side="sell", order_type="market", quantity=quantity ) - + # Calculate stop loss and take profit sl_tp = self.calculate_stop_loss_take_profit(current_price, "sell") - + result = { "status": "success", "exchange": exchange, @@ -194,48 +185,40 @@ def sell_market(self, exchange: str, symbol: str, quantity: float) -> Dict[str, "stop_loss": sl_tp["stop_loss"], "take_profit": sl_tp["take_profit"], "order_result": order_result, - "timestamp": time.time() + "timestamp": time.time(), } - + return result - + except Exception as e: - return { - "error": f"Failed to execute sell order: {str(e)}", - "exchange": exchange, - "symbol": symbol - } - + return {"error": f"Failed to execute sell order: {str(e)}", "exchange": exchange, "symbol": symbol} + def buy_limit(self, exchange: str, symbol: str, quantity: float, price: float) -> Dict[str, Any]: """ Execute limit buy order - + Args: exchange: Exchange name symbol: Trading symbol quantity: Quantity to buy price: Limit price - + Returns: Order result dictionary """ try: quantity = self.format_quantity(exchange, symbol, quantity) - + if exchange not in self.client.exchanges: return {"error": f"Exchange {exchange} not configured"} - + order_result = self.client.exchanges[exchange].place_order( - symbol=symbol, - side="buy", - order_type="limit", - quantity=quantity, - price=price + symbol=symbol, side="buy", order_type="limit", quantity=quantity, price=price ) - + # Calculate stop loss and take profit sl_tp = self.calculate_stop_loss_take_profit(price, "buy") - + result = { "status": "success", "exchange": exchange, @@ -248,48 +231,40 @@ def buy_limit(self, exchange: str, symbol: str, quantity: float, price: float) - "stop_loss": sl_tp["stop_loss"], "take_profit": sl_tp["take_profit"], "order_result": order_result, - "timestamp": time.time() + "timestamp": time.time(), } - + return result - + except Exception as e: - return { - "error": f"Failed to execute limit buy order: {str(e)}", - "exchange": exchange, - "symbol": symbol - } - + return {"error": f"Failed to execute limit buy order: {str(e)}", "exchange": exchange, "symbol": symbol} + def sell_limit(self, exchange: str, symbol: str, quantity: float, price: float) -> Dict[str, Any]: """ Execute limit sell order - + Args: exchange: Exchange name symbol: Trading symbol quantity: Quantity to sell price: Limit price - + Returns: Order result dictionary """ try: quantity = self.format_quantity(exchange, symbol, quantity) - + if exchange not in self.client.exchanges: return {"error": f"Exchange {exchange} not configured"} - + order_result = self.client.exchanges[exchange].place_order( - symbol=symbol, - side="sell", - order_type="limit", - quantity=quantity, - price=price + symbol=symbol, side="sell", order_type="limit", quantity=quantity, price=price ) - + # Calculate stop loss and take profit sl_tp = self.calculate_stop_loss_take_profit(price, "sell") - + result = { "status": "success", "exchange": exchange, @@ -302,27 +277,23 @@ def sell_limit(self, exchange: str, symbol: str, quantity: float, price: float) "stop_loss": sl_tp["stop_loss"], "take_profit": sl_tp["take_profit"], "order_result": order_result, - "timestamp": time.time() + "timestamp": time.time(), } - + return result - + except Exception as e: - return { - "error": f"Failed to execute limit sell order: {str(e)}", - "exchange": exchange, - "symbol": symbol - } - + return {"error": f"Failed to execute limit sell order: {str(e)}", "exchange": exchange, "symbol": symbol} + def auto_trade_by_signals(self, exchange: str, symbol: str, amount_usdt: float = None) -> Dict[str, Any]: """ Execute trade based on technical analysis signals - + Args: exchange: Exchange name symbol: Trading symbol amount_usdt: Amount to trade (optional, uses risk management if not provided) - + Returns: Trade result dictionary """ @@ -331,29 +302,29 @@ def auto_trade_by_signals(self, exchange: str, symbol: str, amount_usdt: float = klines = self.client.get_klines(exchange, symbol, "1h", 100) if not klines: return {"error": "Failed to get klines data"} - + # Calculate indicators indicators = TechnicalIndicators.calculate_all_indicators(klines) if "error" in indicators: return {"error": f"Failed to calculate indicators: {indicators['error']}"} - + # Get trading signals signals = TechnicalIndicators.get_trading_signals(indicators) overall_signal = signals.get("overall", "HOLD") - + if overall_signal == "HOLD": return { "status": "no_action", "signal": overall_signal, "signals": signals, "indicators": indicators, - "message": "No clear trading signal, holding position" + "message": "No clear trading signal, holding position", } - + # Calculate position size if not provided if amount_usdt is None: amount_usdt = self.calculate_position_size(exchange, symbol) - + # Execute trade based on signal if overall_signal == "BUY": result = self.buy_market(exchange, symbol, amount_usdt) @@ -366,7 +337,7 @@ def auto_trade_by_signals(self, exchange: str, symbol: str, amount_usdt: float = result = self.sell_market(exchange, symbol, quantity) else: result = {"error": "Failed to get current price for sell order"} - + # Add signal information to result if "error" not in result: result["signal_info"] = { @@ -375,27 +346,23 @@ def auto_trade_by_signals(self, exchange: str, symbol: str, amount_usdt: float = "key_indicators": { "rsi": indicators.get("rsi"), "macd": indicators.get("macd"), - "current_price": indicators.get("current_price") - } + "current_price": indicators.get("current_price"), + }, } - + return result - + except Exception as e: - return { - "error": f"Failed to execute auto trade: {str(e)}", - "exchange": exchange, - "symbol": symbol - } - + return {"error": f"Failed to execute auto trade: {str(e)}", "exchange": exchange, "symbol": symbol} + def get_trading_summary(self, exchange: str, symbol: str) -> Dict[str, Any]: """ Get comprehensive trading summary including market data and signals - + Args: exchange: Exchange name symbol: Trading symbol - + Returns: Trading summary dictionary """ @@ -403,17 +370,17 @@ def get_trading_summary(self, exchange: str, symbol: str) -> Dict[str, Any]: # Get market data ticker = self.client.get_ticker(exchange, symbol) klines = self.client.get_klines(exchange, symbol, "1h", 100) - + # Calculate indicators and signals indicators = TechnicalIndicators.calculate_all_indicators(klines) signals = TechnicalIndicators.get_trading_signals(indicators) - + # Get account info balance = self.client.get_balance(exchange) - + # Calculate recommended position size position_size = self.calculate_position_size(exchange, symbol) - + summary = { "exchange": exchange, "symbol": symbol, @@ -421,23 +388,19 @@ def get_trading_summary(self, exchange: str, symbol: str) -> Dict[str, Any]: "technical_analysis": { "indicators": indicators, "signals": signals, - "overall_signal": signals.get("overall", "HOLD") + "overall_signal": signals.get("overall", "HOLD"), }, "risk_management": { "recommended_position_size_usdt": position_size, "risk_percentage": self.config.trading_config.risk_percentage, "stop_loss_percentage": self.config.trading_config.stop_loss_percentage, - "take_profit_percentage": self.config.trading_config.take_profit_percentage + "take_profit_percentage": self.config.trading_config.take_profit_percentage, }, "account_balance": balance, - "timestamp": time.time() + "timestamp": time.time(), } - + return summary - + except Exception as e: - return { - "error": f"Failed to generate trading summary: {str(e)}", - "exchange": exchange, - "symbol": symbol - } \ No newline at end of file + return {"error": f"Failed to generate trading summary: {str(e)}", "exchange": exchange, "symbol": symbol} From 4721f0b8bd05e2ea205841942ef21c4809ea3b8d Mon Sep 17 00:00:00 2001 From: zuolingxuan Date: Mon, 22 Sep 2025 17:49:12 +0800 Subject: [PATCH 4/5] add trading exposing --- vertex_flow/plugins/crypto_trading/README.md | 15 +- vertex_flow/plugins/crypto_trading/client.py | 56 +++- vertex_flow/plugins/crypto_trading/example.py | 3 +- .../plugins/crypto_trading/exchanges.py | 135 ++++++++++ .../crypto_trading/manage_futures_orders.py | 242 ++++++++++++++++++ .../plugins/crypto_trading/show_indicators.py | 11 +- 6 files changed, 456 insertions(+), 6 deletions(-) create mode 100644 vertex_flow/plugins/crypto_trading/manage_futures_orders.py diff --git a/vertex_flow/plugins/crypto_trading/README.md b/vertex_flow/plugins/crypto_trading/README.md index e35064a..4c90aee 100644 --- a/vertex_flow/plugins/crypto_trading/README.md +++ b/vertex_flow/plugins/crypto_trading/README.md @@ -213,6 +213,19 @@ python example.py - 交易操作示例 - 风险管理示例 +合约订单辅助脚本: + +```bash +# 查看当前合约持仓并关联订单ID +python manage_futures_orders.py --show-positions --symbol BTC-USDT-SWAP + +# 列出指定合约的未完成订单(可用 state=filled 等历史状态) +python manage_futures_orders.py --list --symbol BTC-USDT-SWAP --state open + +# 查询单个订单(ordId或clOrdId)并按需平仓,终端会二次确认 +python manage_futures_orders.py --symbol BTC-USDT-SWAP --order-id --close --position-side long +``` + ## 故障排除 ### 常见问题 @@ -246,4 +259,4 @@ logging.basicConfig(level=logging.DEBUG) ## 免责声明 -本插件仅供学习和研究使用。数字货币交易存在高风险,可能导致资金损失。使用本插件进行实际交易的风险由用户自行承担。 \ No newline at end of file +本插件仅供学习和研究使用。数字货币交易存在高风险,可能导致资金损失。使用本插件进行实际交易的风险由用户自行承担。 diff --git a/vertex_flow/plugins/crypto_trading/client.py b/vertex_flow/plugins/crypto_trading/client.py index 80e7787..99a4b6d 100644 --- a/vertex_flow/plugins/crypto_trading/client.py +++ b/vertex_flow/plugins/crypto_trading/client.py @@ -9,9 +9,10 @@ from .config import CryptoTradingConfig from .exchanges import BaseExchange, BinanceClient, OKXClient except ImportError: - from config import CryptoTradingConfig from exchanges import BaseExchange, BinanceClient, OKXClient + from config import CryptoTradingConfig + class CryptoTradingClient: """Main client for crypto trading operations""" @@ -230,6 +231,59 @@ def get_futures_positions(self, exchange: str) -> Dict[str, Any]: except Exception as e: return {"error": f"Failed to get futures positions: {str(e)}"} + def get_futures_order( + self, + exchange: str, + symbol: str, + order_id: Optional[str] = None, + client_order_id: Optional[str] = None, + ) -> Dict[str, Any]: + """Fetch a single futures order from the specified exchange""" + if exchange not in self.exchanges: + return {"error": f"Exchange '{exchange}' not configured or not supported"} + + if not order_id and not client_order_id: + return {"error": "order_id or client_order_id is required"} + + try: + return self.exchanges[exchange].get_futures_order(symbol, order_id, client_order_id) + except Exception as e: + return {"error": f"Failed to get futures order: {str(e)}"} + + def close_futures_position( + self, + exchange: str, + symbol: str, + position_side: str, + margin_mode: str = "cross", + size: Optional[float] = None, + currency: Optional[str] = None, + ) -> Dict[str, Any]: + """Request a futures position close on the specified exchange""" + if exchange not in self.exchanges: + return {"error": f"Exchange '{exchange}' not configured or not supported"} + + try: + return self.exchanges[exchange].close_futures_position(symbol, position_side, margin_mode, size, currency) + except Exception as e: + return {"error": f"Failed to close futures position: {str(e)}"} + + def list_futures_orders( + self, + exchange: str, + symbol: Optional[str] = None, + state: str = "open", + limit: int = 100, + ) -> Dict[str, Any]: + """List futures orders for the specified exchange""" + if exchange not in self.exchanges: + return {"error": f"Exchange '{exchange}' not configured or not supported"} + + try: + return self.exchanges[exchange].list_futures_orders(symbol, state, limit) + except Exception as e: + return {"error": f"Failed to list futures orders: {str(e)}"} + def get_all_positions(self, exchange: str = None) -> Dict[str, Any]: """ Get both spot and futures positions for one or all exchanges diff --git a/vertex_flow/plugins/crypto_trading/example.py b/vertex_flow/plugins/crypto_trading/example.py index 8581bf5..0aee59f 100644 --- a/vertex_flow/plugins/crypto_trading/example.py +++ b/vertex_flow/plugins/crypto_trading/example.py @@ -26,11 +26,12 @@ print(f"⚠️ Could not load .env file: {e}") from client import CryptoTradingClient -from config import CryptoTradingConfig from indicators import TechnicalIndicators from position_metrics import PositionMetrics from trading import TradingEngine +from config import CryptoTradingConfig + def setup_config_example(): """Example of setting up configuration programmatically""" diff --git a/vertex_flow/plugins/crypto_trading/exchanges.py b/vertex_flow/plugins/crypto_trading/exchanges.py index 60e8a94..784c24b 100644 --- a/vertex_flow/plugins/crypto_trading/exchanges.py +++ b/vertex_flow/plugins/crypto_trading/exchanges.py @@ -64,6 +64,32 @@ def get_futures_positions(self) -> Dict[str, Any]: """Get futures positions""" pass + @abstractmethod + def get_futures_order( + self, symbol: str, order_id: Optional[str] = None, client_order_id: Optional[str] = None + ) -> Dict[str, Any]: + """Get futures order details""" + pass + + @abstractmethod + def close_futures_position( + self, + symbol: str, + position_side: str, + margin_mode: str = "cross", + size: Optional[float] = None, + currency: Optional[str] = None, + ) -> Dict[str, Any]: + """Close a futures position""" + pass + + @abstractmethod + def list_futures_orders( + self, symbol: Optional[str] = None, state: str = "open", limit: int = 100 + ) -> Dict[str, Any]: + """List futures orders for the exchange""" + pass + class OKXClient(BaseExchange): """OKX exchange API client""" @@ -178,10 +204,15 @@ def get_ticker(self, symbol: str) -> Dict[str, Any]: return { "symbol": ticker["instId"], "price": float(ticker["last"]), + "price_str": ticker.get("last"), "bid": float(ticker["bidPx"]), + "bid_str": ticker.get("bidPx"), "ask": float(ticker["askPx"]), + "ask_str": ticker.get("askPx"), "volume": float(ticker["vol24h"]), + "volume_str": ticker.get("vol24h"), "change": float(ticker["sodUtc8"]), + "change_str": ticker.get("sodUtc8"), } # 如果没有数据或请求失败,返回错误信息 if "error" in response: @@ -230,6 +261,74 @@ def place_order( return self._make_request("POST", "/trade/order", data=order_data) + def get_futures_order( + self, symbol: str, order_id: Optional[str] = None, client_order_id: Optional[str] = None + ) -> Dict[str, Any]: + """Get OKX futures order details""" + if not order_id and not client_order_id: + return {"error": "At least one of order_id or client_order_id is required"} + + params: Dict[str, Any] = {"instId": symbol} + if order_id: + params["ordId"] = order_id + if client_order_id: + params["clOrdId"] = client_order_id + + response = self._make_request("GET", "/trade/order", params) + + if response.get("code") == "0" and response.get("data"): + return {"success": True, "data": response["data"][0]} + + return {"error": response.get("msg", "Failed to fetch order"), "raw": response} + + def close_futures_position( + self, + symbol: str, + position_side: str, + margin_mode: str = "cross", + size: Optional[float] = None, + currency: Optional[str] = None, + ) -> Dict[str, Any]: + """Close an OKX futures position""" + payload: Dict[str, Any] = { + "instId": symbol, + "mgnMode": margin_mode, + "posSide": position_side, + } + + if size is not None: + payload["sz"] = str(size) + if currency: + payload["ccy"] = currency + + response = self._make_request("POST", "/trade/close-position", data=payload) + + if response.get("code") == "0": + return {"success": True, "data": response.get("data")} + + return {"error": response.get("msg", "Failed to close position"), "raw": response} + + def list_futures_orders( + self, symbol: Optional[str] = None, state: str = "open", limit: int = 100 + ) -> Dict[str, Any]: + """List OKX futures orders""" + params: Dict[str, Any] = {"instType": "SWAP", "limit": str(limit)} + if symbol: + params["instId"] = symbol + + if state == "open": + endpoint = "/trade/orders-pending" + else: + endpoint = "/trade/orders-history" + params["state"] = state + + response = self._make_request("GET", endpoint, params) + + if response.get("code") == "0": + return {"success": True, "data": response.get("data", [])} + + return {"error": response.get("msg", "Failed to list orders"), "raw": response} + def get_order_status(self, order_id: str, symbol: str) -> Dict[str, Any]: """查询订单状态""" try: @@ -283,9 +382,17 @@ def get_futures_positions(self) -> Dict[str, Any]: "symbol": position["instId"], "side": position["posSide"], "size": float(pos_size), + "size_str": pos_size, + "avg_price": float(position.get("avgPx", 0) or 0), + "avg_price_str": position.get("avgPx"), + "mark_price": float(position.get("markPx", 0) or 0), + "mark_price_str": position.get("markPx"), "notional": float(position.get("notionalUsd", 0) or 0), + "notional_str": position.get("notionalUsd"), "unrealized_pnl": float(position.get("upl", 0) or 0), + "unrealized_pnl_str": position.get("upl"), "margin": float(position.get("margin", 0) or 0), + "margin_str": position.get("margin"), } ) return {"success": True, "data": positions} @@ -368,10 +475,15 @@ def get_ticker(self, symbol: str) -> Dict[str, Any]: return { "symbol": response["symbol"], "price": float(response["lastPrice"]), + "price_str": response.get("lastPrice"), "bid": float(response["bidPrice"]), + "bid_str": response.get("bidPrice"), "ask": float(response["askPrice"]), + "ask_str": response.get("askPrice"), "volume": float(response["volume"]), + "volume_str": response.get("volume"), "change": float(response["priceChangePercent"]), + "change_str": response.get("priceChangePercent"), } except Exception as e: return {"error": f"Failed to get ticker: {str(e)}"} @@ -409,6 +521,29 @@ def place_order( return self._make_request("POST", "/order", order_params, signed=True) + def get_futures_order( + self, symbol: str, order_id: Optional[str] = None, client_order_id: Optional[str] = None + ) -> Dict[str, Any]: + """Binance futures order lookup placeholder""" + return {"error": "Binance futures order lookup not implemented"} + + def close_futures_position( + self, + symbol: str, + position_side: str, + margin_mode: str = "cross", + size: Optional[float] = None, + currency: Optional[str] = None, + ) -> Dict[str, Any]: + """Binance futures close position placeholder""" + return {"error": "Binance futures close position not implemented"} + + def list_futures_orders( + self, symbol: Optional[str] = None, state: str = "open", limit: int = 100 + ) -> Dict[str, Any]: + """Binance futures order list placeholder""" + return {"error": "Binance futures order listing not implemented"} + def get_spot_positions(self) -> Dict[str, Any]: """Get spot positions/balances from Binance""" try: diff --git a/vertex_flow/plugins/crypto_trading/manage_futures_orders.py b/vertex_flow/plugins/crypto_trading/manage_futures_orders.py new file mode 100644 index 0000000..a9c1580 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/manage_futures_orders.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Simple CLI to inspect OKX futures orders and trigger manual closes.""" +import argparse +import json +from typing import Any, Dict, Iterable, List, Optional + +from client import CryptoTradingClient + +from config import CryptoTradingConfig + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Inspect OKX futures orders and positions, optionally close an order's position", + ) + parser.add_argument("--exchange", default="okx", help="Exchange alias, default: okx") + parser.add_argument("--symbol", help="Instrument id, e.g. BTC-USDT-SWAP") + + parser.add_argument("--list", action="store_true", help="List futures orders instead of single lookup") + parser.add_argument("--state", default="open", help="Order state for --list (default: open)") + parser.add_argument("--limit", type=int, default=50, help="Maximum orders to return for --list") + + parser.add_argument("--show-positions", action="store_true", help="Display current futures positions") + + parser.add_argument("--order-id", help="OKX order id (ordId)") + parser.add_argument("--client-order-id", help="Client order id (clOrdId)") + + parser.add_argument( + "--close", + action="store_true", + help="Close the futures position after showing the order details", + ) + parser.add_argument( + "--position-side", + choices=["long", "short"], + help="Position side required when --close is passed", + ) + parser.add_argument("--margin-mode", default="cross", help="OKX margin mode, defaults to cross") + parser.add_argument("--size", type=float, help="Optional close size in contract units") + parser.add_argument( + "--currency", + help="Optional currency when closing by coin value instead of contract size", + ) + + return parser.parse_args() + + +def pretty_print(title: str, payload: Dict[str, Any]) -> None: + print(f"\n{title}") + print("-" * len(title)) + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + +def require_close_prerequisites(args: argparse.Namespace) -> Optional[str]: + if not args.close: + return None + if not args.position_side: + return "--position-side is required when --close is used" + if not (args.order_id or args.client_order_id): + return "--order-id or --client-order-id is required when --close is used" + if not args.symbol: + return "--symbol is required when --close is used" + if args.list: + return "--close cannot be combined with --list" + return None + + +def confirm(prompt: str) -> bool: + answer = input(f"{prompt} [y/N]: ").strip().lower() + return answer in {"y", "yes"} + + +def ensure_action_requested(args: argparse.Namespace) -> Optional[str]: + actions: Iterable[bool] = ( + args.list, + args.show_positions, + bool(args.order_id or args.client_order_id), + ) + if not any(actions): + return "Please specify --list, --show-positions, or an order identifier" + return None + + +def build_order_lookup(order_resp: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]: + if "error" in order_resp: + return {} + + orders = order_resp.get("data") or order_resp.get("orders") or [] + lookup: Dict[str, List[Dict[str, Any]]] = {} + for order in orders: + inst_id = order.get("instId") or order.get("symbol") + if not inst_id: + continue + lookup.setdefault(inst_id, []).append(order) + return lookup + + +def display_positions( + resp: Dict[str, Any], + symbol: Optional[str], + order_lookup: Optional[Dict[str, List[Dict[str, Any]]]] = None, + order_error: Optional[Dict[str, Any]] = None, +) -> None: + if "error" in resp: + pretty_print("Positions Lookup Failed", resp) + return + + positions = resp.get("data") or resp.get("positions") or [] + + if symbol: + positions = [pos for pos in positions if pos.get("symbol") == symbol or pos.get("instId") == symbol] + + enriched: List[Dict[str, Any]] = [] + for position in positions: + inst_id = position.get("instId") or position.get("symbol") + orders = order_lookup.get(inst_id, []) if order_lookup else [] + order_ids = [order.get("ordId") for order in orders if order.get("ordId")] + client_ids = [order.get("clOrdId") for order in orders if order.get("clOrdId")] + + entry = dict(position) + if order_ids: + entry["orderIds"] = order_ids + if client_ids: + entry["clientOrderIds"] = client_ids + + enriched.append(entry) + + payload: Dict[str, Any] = {"count": len(enriched), "positions": enriched} + if order_error: + payload["order_lookup_error"] = order_error + + title = "Positions" if enriched else "No Positions Found" + pretty_print(title, payload) + + +def main() -> None: + args = parse_args() + error = require_close_prerequisites(args) + if error: + raise SystemExit(error) + + error = ensure_action_requested(args) + if error: + raise SystemExit(error) + + config = CryptoTradingConfig() + client = CryptoTradingClient(config) + + if args.exchange not in client.get_available_exchanges(): + raise SystemExit(f"Exchange '{args.exchange}' is not configured") + + print("🔍 Futures Order Inspection") + print("=" * 30) + + if args.list: + order_list = client.list_futures_orders( + exchange=args.exchange, + symbol=args.symbol, + state=args.state, + limit=args.limit, + ) + + if "error" in order_list: + pretty_print("Order List Failed", order_list) + else: + pretty_print("Order List", order_list) + + orders_lookup: Optional[Dict[str, List[Dict[str, Any]]]] = None + order_lookup_error: Optional[Dict[str, Any]] = None + + if args.show_positions: + positions_resp = client.get_futures_positions(args.exchange) + + orders_for_positions = client.list_futures_orders( + exchange=args.exchange, + symbol=args.symbol, + state="open", + limit=args.limit, + ) + + if "error" in orders_for_positions: + order_lookup_error = orders_for_positions + orders_lookup = {} + else: + orders_lookup = build_order_lookup(orders_for_positions) + + display_positions(positions_resp, args.symbol, orders_lookup, order_lookup_error) + + if not (args.order_id or args.client_order_id): + return + + if not args.symbol: + raise SystemExit("--symbol is required when referencing a specific order") + + order_resp = client.get_futures_order( + args.exchange, + args.symbol, + order_id=args.order_id, + client_order_id=args.client_order_id, + ) + + if "error" in order_resp: + pretty_print("Order Lookup Failed", order_resp) + return + + pretty_print("Order Details", order_resp) + + if not args.close: + return + + print("\n⚠️ About to close position using the parameters below:") + close_preview = { + "exchange": args.exchange, + "symbol": args.symbol, + "position_side": args.position_side, + "margin_mode": args.margin_mode, + "size": args.size, + "currency": args.currency, + } + pretty_print("Close Preview", close_preview) + + if not confirm("Proceed with close-position request?"): + print("Close operation cancelled by user.") + return + + close_resp = client.close_futures_position( + exchange=args.exchange, + symbol=args.symbol, + position_side=args.position_side, + margin_mode=args.margin_mode, + size=args.size, + currency=args.currency, + ) + + if "error" in close_resp: + pretty_print("Close Failed", close_resp) + else: + pretty_print("Close Result", close_resp) + + +if __name__ == "__main__": + main() diff --git a/vertex_flow/plugins/crypto_trading/show_indicators.py b/vertex_flow/plugins/crypto_trading/show_indicators.py index 842f1ec..4d8f8be 100644 --- a/vertex_flow/plugins/crypto_trading/show_indicators.py +++ b/vertex_flow/plugins/crypto_trading/show_indicators.py @@ -6,19 +6,24 @@ import traceback from client import CryptoTradingClient -from config import CryptoTradingConfig from indicators import TechnicalIndicators +from config import CryptoTradingConfig + def format_number(value): """格式化数字显示""" if value is None: return "N/A" if isinstance(value, (int, float)): - if abs(value) >= 1000: + magnitude = abs(value) + if magnitude >= 1000: return f"{value:,.2f}" - else: + if magnitude >= 1: return f"{value:.4f}" + if magnitude >= 0.01: + return f"{value:.6f}" + return f"{value:.8f}" return str(value) From 6a52e41b54f5b728822467deb8809ccc53f24bea Mon Sep 17 00:00:00 2001 From: zuolingxuan Date: Sun, 28 Sep 2025 23:40:35 +0800 Subject: [PATCH 5/5] quant trading --- .../plugins/crypto_trading/.env.example | 25 ++ vertex_flow/plugins/crypto_trading/AGENTS.md | 42 +++ vertex_flow/plugins/crypto_trading/README.md | 33 ++- .../plugins/crypto_trading/__init__.py | 18 +- .../plugins/crypto_trading/backtester.py | 141 ++++++++++ vertex_flow/plugins/crypto_trading/config.py | 49 ++++ vertex_flow/plugins/crypto_trading/example.py | 41 +++ .../plugins/crypto_trading/exchanges.py | 1 + .../crypto_trading/execution_controls.py | 48 ++++ .../plugins/crypto_trading/indicators.py | 117 ++++++-- .../plugins/crypto_trading/monitoring.py | 58 ++++ .../plugins/crypto_trading/persistence.py | 75 +++++ .../plugins/crypto_trading/risk_manager.py | 110 ++++++++ .../plugins/crypto_trading/scheduler.py | 67 +++++ .../plugins/crypto_trading/show_indicators.py | 262 +++++++++++++----- vertex_flow/plugins/crypto_trading/trading.py | 227 ++++++++++++++- 16 files changed, 1221 insertions(+), 93 deletions(-) create mode 100644 vertex_flow/plugins/crypto_trading/.env.example create mode 100644 vertex_flow/plugins/crypto_trading/AGENTS.md create mode 100644 vertex_flow/plugins/crypto_trading/backtester.py create mode 100644 vertex_flow/plugins/crypto_trading/execution_controls.py create mode 100644 vertex_flow/plugins/crypto_trading/monitoring.py create mode 100644 vertex_flow/plugins/crypto_trading/persistence.py create mode 100644 vertex_flow/plugins/crypto_trading/risk_manager.py create mode 100644 vertex_flow/plugins/crypto_trading/scheduler.py diff --git a/vertex_flow/plugins/crypto_trading/.env.example b/vertex_flow/plugins/crypto_trading/.env.example new file mode 100644 index 0000000..128553b --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/.env.example @@ -0,0 +1,25 @@ +# Copy this file to .env and replace the placeholder values with your credentials + +# OKX configuration +OKX_API_KEY=YOUR_OKX_API_KEY +OKX_SECRET_KEY=YOUR_OKX_SECRET_KEY +OKX_PASSPHRASE=YOUR_OKX_PASSPHRASE +OKX_SANDBOX=true + +# Binance configuration +BINANCE_API_KEY=YOUR_BINANCE_API_KEY +BINANCE_SECRET_KEY=YOUR_BINANCE_SECRET_KEY +BINANCE_SANDBOX=true + +# Trading parameters +DEFAULT_SYMBOL=BTC-USDT +MAX_POSITION_SIZE=1000.0 +RISK_PERCENTAGE=0.02 +STOP_LOSS_PERCENTAGE=0.05 +TAKE_PROFIT_PERCENTAGE=0.10 + +# Execution & risk limits +SLIPPAGE_BUFFER=0.0005 +MAX_ORDER_NOTIONAL=5000.0 +MAX_DRAWDOWN=0.2 +MAX_DAILY_LOSS=200.0 diff --git a/vertex_flow/plugins/crypto_trading/AGENTS.md b/vertex_flow/plugins/crypto_trading/AGENTS.md new file mode 100644 index 0000000..da0652c --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/AGENTS.md @@ -0,0 +1,42 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `client.py`: main entry for exchange operations; keep orchestration here. +- `trading.py`: trading engine plus risk helpers reused by demos. +- `exchanges.py`: adapter base classes and OKX/Binance clients; extend for new venues. +- `config.py`: dataclasses for credentials, risk defaults, and feature flags. +- `execution_controls.py`, `risk_manager.py`, `monitoring.py`: execution limits, risk checks, and alert fan-out. +- `scheduler.py`, `backtester.py`: reusable automation hooks and reference strategies for simulations. +- `persistence.py`: JSONL datastore for trades, metrics, configs. +- `indicators.py`, `position_metrics.py`: analytics utilities; add metrics here to stay reusable. +- Scripts `example.py` and `show_indicators.py` demonstrate workflows; keep CLI experiments separate from library code. +- Plan future tests in `tests/` mirroring module names (for example, `tests/test_trading.py`). + +## Setup, Build & Run +- `python3 -m venv .venv && source .venv/bin/activate` isolates dependencies. +- `pip install -r requirements.txt` installs exchange SDKs, TA libraries, and logging deps. +- `python example.py` runs a full account→signal→trade sample. +- `python show_indicators.py` prints OKX positions with indicator summaries; mock network calls when offline. + +## Coding Style & Naming Conventions +- Follow PEP 8 with 4-space indentation, snake_case functions, and PascalCase classes. +- Keep modules typed and mirror the concise docstrings used in `client.py`. +- Order imports as stdlib / third-party / local and keep them alphabetical. +- Surface reusable constants in `config.py` or clearly named enums instead of magic literals. + +## Testing Guidelines +- Prefer `pytest`; stage fixtures in `tests/conftest.py` so exchange mocks and timestamps are shared. +- Name tests `test_` and focus on deterministic paths by stubbing HTTP clients. +- Cover branch-heavy risk logic in `trading.py` and adapter fallbacks in `exchanges.py`. +- Document the exact `pytest` invocation and result in every PR. + +## Commit & Pull Request Guidelines +- Match existing history: `: ` (`fix: adjust fee rounding`, `lint`). +- Keep subjects ≤72 chars; explain config or data contract changes in the body. +- Reference related issues with `(#123)` and call out manual test commands in the PR description. +- PRs should outline intent, risk, and screenshots or console snippets when behavior shifts. + +## Security & Configuration Tips +- Secrets live in `.env`; never check concrete keys into the repo or fixtures. +- Default to sandbox credentials (`sandbox=True`) during manual runs and signal when production access is required. +- Sanitize recorded responses before storing them in tests or logs. diff --git a/vertex_flow/plugins/crypto_trading/README.md b/vertex_flow/plugins/crypto_trading/README.md index 4c90aee..df07e67 100644 --- a/vertex_flow/plugins/crypto_trading/README.md +++ b/vertex_flow/plugins/crypto_trading/README.md @@ -65,6 +65,25 @@ config.set_binance_config( ) ``` +### 3. 风控与执行参数 + +`config.py` 支持通过环境变量覆盖交易风控阈值: + +```env +# 交易参数 +DEFAULT_SYMBOL=BTC-USDT +MAX_POSITION_SIZE=1000.0 +RISK_PERCENTAGE=0.02 +STOP_LOSS_PERCENTAGE=0.05 +TAKE_PROFIT_PERCENTAGE=0.10 + +# 执行与风控开关 +SLIPPAGE_BUFFER=0.0005 # 买卖滑点缓冲(5bps) +MAX_ORDER_NOTIONAL=5000.0 # 单笔委托最大名义金额 +MAX_DRAWDOWN=0.2 # 账户最大回撤(20%) +MAX_DAILY_LOSS=200.0 # 当日最大亏损(USDT) +``` + ## 基本使用 ### 1. 初始化客户端 @@ -161,6 +180,18 @@ print(f"推荐信号: {summary['technical_analysis']['overall_signal']}") print(f"推荐仓位: ${summary['risk_management']['recommended_position_size_usdt']}") ``` +## 回测与策略调度 + +- 使用 `TradingEngine.run_backtest(exchange, symbol, strategy)` 快速评估策略,默认提供 `MovingAverageCrossStrategy` 示例。 +- `TradingEngine.schedule_strategy(name, interval, callback)` 可注册定时任务,结合 `run_scheduler`/`run_pending` 实现多策略轮询。 +- 运行 `python example.py` 可查看回测结果和调度任务在控制台输出的示例。 + +## 风控监控与告警 + +- 核心风控由 `RiskMonitor` 驱动,自动跟踪账户权益、回撤与当日亏损,触发 `kill switch` 时会拒绝新订单。 +- `ExecutionControls` 为每笔委托应用滑点缓冲与名义金额上限,避免过量下单。 +- `EventLogger` 与 `AlertManager` 会将订单、风控、告警记录写入 `data/` 目录下的 JSONL 文件,便于后续审计。 + ## 技术指标说明 插件支持以下技术指标: @@ -179,7 +210,7 @@ print(f"推荐仓位: ${summary['risk_management']['recommended_position_size_us - **仓位管理**: 基于账户余额和风险比例计算仓位 - **止损止盈**: 自动计算止损止盈价位 -- **风险控制**: 限制单笔交易最大金额 +- **风险控制**: 限制单笔交易、监控回撤并触发Kill Switch ```python # 风险管理配置 diff --git a/vertex_flow/plugins/crypto_trading/__init__.py b/vertex_flow/plugins/crypto_trading/__init__.py index 342b219..6b964e8 100644 --- a/vertex_flow/plugins/crypto_trading/__init__.py +++ b/vertex_flow/plugins/crypto_trading/__init__.py @@ -5,10 +5,26 @@ supporting OKX and Binance APIs for account management, trading, and technical analysis. """ +from .backtester import Backtester, MovingAverageCrossStrategy from .client import CryptoTradingClient from .exchanges import BinanceClient, OKXClient from .indicators import TechnicalIndicators +from .monitoring import AlertManager, EventLogger +from .risk_manager import RiskMonitor +from .scheduler import StrategyScheduler from .trading import TradingEngine __version__ = "1.0.0" -__all__ = ["CryptoTradingClient", "OKXClient", "BinanceClient", "TechnicalIndicators", "TradingEngine"] +__all__ = [ + "CryptoTradingClient", + "OKXClient", + "BinanceClient", + "TechnicalIndicators", + "TradingEngine", + "Backtester", + "MovingAverageCrossStrategy", + "StrategyScheduler", + "RiskMonitor", + "EventLogger", + "AlertManager", +] diff --git a/vertex_flow/plugins/crypto_trading/backtester.py b/vertex_flow/plugins/crypto_trading/backtester.py new file mode 100644 index 0000000..cdc71cb --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/backtester.py @@ -0,0 +1,141 @@ +"""Naive backtesting harness for quantitative strategies.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Protocol + +from risk_manager import RiskMonitor + + +class Strategy(Protocol): + """Minimal strategy protocol used by the backtester.""" + + name: str + + def on_bar(self, index: int, candles: List[List[float]]) -> Dict[str, Any]: + """Return an action dict such as {"action": "buy", "size": 1}.""" + ... + + +@dataclass +class BacktestTrade: + timestamp: float + action: str + price: float + size: float + pnl: float = 0.0 + + +@dataclass +class BacktestResult: + strategy: str + starting_equity: float + ending_equity: float + trades: List[BacktestTrade] = field(default_factory=list) + max_drawdown: float = 0.0 + + @property + def total_return(self) -> float: + if self.starting_equity == 0: + return 0.0 + return (self.ending_equity - self.starting_equity) / self.starting_equity + + +class Backtester: + """Runs a simple long-only backtest using OHLC candles.""" + + def __init__( + self, + strategy: Strategy, + initial_equity: float = 1000.0, + fee_rate: float = 0.0005, + risk_monitor: Optional[RiskMonitor] = None, + ) -> None: + self.strategy = strategy + self.initial_equity = initial_equity + self.fee_rate = fee_rate + self.risk_monitor = risk_monitor + + def run(self, candles: List[List[float]]) -> BacktestResult: + cash = self.initial_equity + position = 0.0 + entry_price = 0.0 + peak_equity = cash + max_drawdown = 0.0 + trades: List[BacktestTrade] = [] + + for idx, candle in enumerate(candles): + if len(candle) < 5: + continue + timestamp, _, _, _, close, _ = candle[:6] + close = float(close) + action = self.strategy.on_bar(idx, candles) + if not action: + continue + side = action.get("action") + size = float(action.get("size", 0)) + if side not in {"buy", "sell"} or size <= 0: + continue + + if side == "buy" and cash > 0: + price = close + cost = price * size + fee = cost * self.fee_rate + total_cost = cost + fee + if cash >= total_cost: + position += size + cash -= total_cost + entry_price = price + trades.append(BacktestTrade(timestamp=timestamp, action="buy", price=price, size=size, pnl=0)) + elif side == "sell" and position >= size: + price = close + proceeds = price * size + fee = proceeds * self.fee_rate + realized = proceeds - fee + pnl = (price - entry_price) * size - fee + cash += realized + position -= size + trades.append(BacktestTrade(timestamp=timestamp, action="sell", price=price, size=size, pnl=pnl)) + if self.risk_monitor: + self.risk_monitor.record_trade({"timestamp": timestamp, "pnl": pnl, "action": "backtest"}) + + equity = cash + position * close + peak_equity = max(peak_equity, equity) + drawdown = 0.0 + if peak_equity > 0: + drawdown = (peak_equity - equity) / peak_equity + if self.risk_monitor: + self.risk_monitor.update_equity(equity) + max_drawdown = max(max_drawdown, drawdown) + + ending_equity = cash + position * float(candles[-1][4]) if candles else cash + result = BacktestResult( + strategy=self.strategy.name, + starting_equity=self.initial_equity, + ending_equity=ending_equity, + trades=trades, + max_drawdown=max_drawdown if candles else 0.0, + ) + return result + + +class MovingAverageCrossStrategy: + """Very simple MA cross strategy for demonstrations.""" + + def __init__(self, fast: int = 5, slow: int = 20, name: str = "ma_cross") -> None: + self.fast = fast + self.slow = slow + self.name = name + + def on_bar(self, index: int, candles: List[List[float]]) -> Dict[str, Any]: + if index < self.slow: + return {} + closes = [candle[4] for candle in candles[: index + 1]] + fast_ma = sum(closes[-self.fast :]) / self.fast + slow_ma = sum(closes[-self.slow :]) / self.slow + if fast_ma > slow_ma: + return {"action": "buy", "size": 1.0} + elif fast_ma < slow_ma: + return {"action": "sell", "size": 1.0} + return {} diff --git a/vertex_flow/plugins/crypto_trading/config.py b/vertex_flow/plugins/crypto_trading/config.py index e70374b..ef2748a 100644 --- a/vertex_flow/plugins/crypto_trading/config.py +++ b/vertex_flow/plugins/crypto_trading/config.py @@ -32,6 +32,10 @@ class TradingConfig: risk_percentage: float = 0.02 stop_loss_percentage: float = 0.05 take_profit_percentage: float = 0.10 + slippage_buffer: float = 0.0005 + max_order_notional: float = 5000.0 + max_drawdown: float = 0.2 + max_daily_loss: float = 200.0 class CryptoTradingConfig: @@ -102,6 +106,34 @@ def _load_from_env(self): except ValueError: pass + slippage_buffer = os.getenv("SLIPPAGE_BUFFER") + if slippage_buffer: + try: + self.trading_config.slippage_buffer = float(slippage_buffer) + except ValueError: + pass + + max_order_notional = os.getenv("MAX_ORDER_NOTIONAL") + if max_order_notional: + try: + self.trading_config.max_order_notional = float(max_order_notional) + except ValueError: + pass + + max_drawdown = os.getenv("MAX_DRAWDOWN") + if max_drawdown: + try: + self.trading_config.max_drawdown = float(max_drawdown) + except ValueError: + pass + + max_daily_loss = os.getenv("MAX_DAILY_LOSS") + if max_daily_loss: + try: + self.trading_config.max_daily_loss = float(max_daily_loss) + except ValueError: + pass + def set_okx_config(self, api_key: str, secret_key: str, passphrase: str, sandbox: bool = False): """Set OKX configuration""" self.okx_config = ExchangeConfig(api_key=api_key, secret_key=secret_key, passphrase=passphrase, sandbox=sandbox) @@ -117,3 +149,20 @@ def get_config_dict(self) -> Dict[str, Any]: "binance": self.binance_config.__dict__ if self.binance_config else None, "trading": self.trading_config.__dict__, } + + def get_sanitised_config(self) -> Dict[str, Any]: + """Return a copy of the configuration with secrets masked.""" + def mask(value: Optional[ExchangeConfig]) -> Optional[Dict[str, Any]]: + if not value: + return None + data = value.__dict__.copy() + for key in ("api_key", "secret_key", "passphrase"): + if key in data and data[key]: + data[key] = "***masked***" + return data + + return { + "okx": mask(self.okx_config), + "binance": mask(self.binance_config), + "trading": self.trading_config.__dict__.copy(), + } diff --git a/vertex_flow/plugins/crypto_trading/example.py b/vertex_flow/plugins/crypto_trading/example.py index 0aee59f..73d6342 100644 --- a/vertex_flow/plugins/crypto_trading/example.py +++ b/vertex_flow/plugins/crypto_trading/example.py @@ -25,6 +25,7 @@ except Exception as e: print(f"⚠️ Could not load .env file: {e}") +from backtester import MovingAverageCrossStrategy from client import CryptoTradingClient from indicators import TechnicalIndicators from position_metrics import PositionMetrics @@ -510,6 +511,43 @@ def futures_metrics_example(): print("\n📭 未找到任何合约持仓,无法计算指标") +def automation_example(): + """Demonstrate backtesting and scheduler integrations.""" + print("\n=== Automation & Backtest Example ===\n") + + config = CryptoTradingConfig() + client = CryptoTradingClient(config) + engine = TradingEngine(client) + + exchanges = client.get_available_exchanges() + if not exchanges: + print("No exchanges configured.") + return + + exchange = exchanges[0] + symbol = get_symbol_for_exchange(config, exchange) + + strategy = MovingAverageCrossStrategy(fast=5, slow=20) + backtest = engine.run_backtest(exchange, symbol, strategy=strategy, interval="1h", limit=120) + + if "error" in backtest: + print(f"Backtest failed: {backtest['error']}") + else: + print(f"Strategy: {backtest['strategy']}") + print(f"Trades executed: {backtest['trade_count']}") + print(f"Total return: {backtest['total_return']:.2%}") + print(f"Max drawdown: {backtest['max_drawdown']:.2%}") + + # Schedule an equity snapshot task and run it once + engine.schedule_strategy( + "equity_snapshot", + interval=0.1, + callback=lambda: engine.capture_equity_snapshot(exchange), + ) + engine.scheduler.run_pending() + print("Equity snapshot task executed via scheduler.") + + def main(): """Main function to run all examples""" print("🚀 Crypto Trading Plugin Examples") @@ -534,6 +572,9 @@ def main(): # Futures metrics calculation futures_metrics_example() + # Automation helpers + automation_example() + except Exception as e: print(f"Error running examples: {e}") diff --git a/vertex_flow/plugins/crypto_trading/exchanges.py b/vertex_flow/plugins/crypto_trading/exchanges.py index 784c24b..4ca3895 100644 --- a/vertex_flow/plugins/crypto_trading/exchanges.py +++ b/vertex_flow/plugins/crypto_trading/exchanges.py @@ -393,6 +393,7 @@ def get_futures_positions(self) -> Dict[str, Any]: "unrealized_pnl_str": position.get("upl"), "margin": float(position.get("margin", 0) or 0), "margin_str": position.get("margin"), + "leverage": float(position.get("lever", 0) or 0), } ) return {"success": True, "data": positions} diff --git a/vertex_flow/plugins/crypto_trading/execution_controls.py b/vertex_flow/plugins/crypto_trading/execution_controls.py new file mode 100644 index 0000000..7c9d105 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/execution_controls.py @@ -0,0 +1,48 @@ +"""Execution helpers for slippage control and order preparation.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, Tuple + + +@dataclass +class ExecutionParameters: + """Container for execution-specific settings.""" + + slippage_buffer: float = 0.0005 # 5 bps + max_order_notional: float = 5000.0 + + +class ExecutionControls: + """Utility class to sanitize order parameters before submission.""" + + def __init__(self, params: ExecutionParameters) -> None: + self.params = params + + def apply_slippage(self, price: float, side: str) -> float: + """Apply a conservative slippage buffer to the target price.""" + buffer = self.params.slippage_buffer + if side.lower() == "buy": + return price * (1 + buffer) + return price * (1 - buffer) + + def cap_notional(self, quantity: float, price: float) -> Tuple[float, float]: + """Reduce quantity so that notional stays within limits.""" + notional = quantity * price + if notional <= self.params.max_order_notional: + return quantity, notional + scale = self.params.max_order_notional / notional + adjusted_qty = quantity * scale + return adjusted_qty, self.params.max_order_notional + + def prepare_order(self, side: str, quantity: float, price: float) -> Dict[str, float]: + """Return sanitized quantity and safety-adjusted price.""" + safe_price = self.apply_slippage(price, side) + safe_quantity, capped_notional = self.cap_notional(quantity, safe_price) + return { + "quantity": safe_quantity, + "price": safe_price, + "notional": capped_notional, + } + diff --git a/vertex_flow/plugins/crypto_trading/indicators.py b/vertex_flow/plugins/crypto_trading/indicators.py index bd086ea..98cdecc 100644 --- a/vertex_flow/plugins/crypto_trading/indicators.py +++ b/vertex_flow/plugins/crypto_trading/indicators.py @@ -147,27 +147,112 @@ def obv(close: pd.Series, volume: pd.Series) -> pd.Series: return obv @staticmethod - def support_resistance(data: pd.Series, window: int = 20) -> Dict[str, List[float]]: - """Find support and resistance levels""" - highs = data.rolling(window=window, center=True).max() - lows = data.rolling(window=window, center=True).min() + def pivot_levels(high: pd.Series, low: pd.Series, close: pd.Series) -> Dict[str, float]: + """Calculate classic pivot point levels from the latest session""" + if len(high) == 0: + return {} + + recent_high = high.iloc[-1] + recent_low = low.iloc[-1] + recent_close = close.iloc[-1] + + pivot = (recent_high + recent_low + recent_close) / 3 + r1 = 2 * pivot - recent_low + s1 = 2 * pivot - recent_high + r2 = pivot + (recent_high - recent_low) + s2 = pivot - (recent_high - recent_low) + + return {"pivot": pivot, "r1": r1, "r2": r2, "s1": s1, "s2": s2} + + @staticmethod + def volume_profile_levels(df: pd.DataFrame, bins: int = 24) -> List[float]: + """Approximate high-volume price levels using volume profile""" + if df.empty or df["high"].max() == df["low"].min(): + return [] + + price_min = df["low"].min() + price_max = df["high"].max() + bin_edges = np.linspace(price_min, price_max, bins + 1) + volume_distribution = np.zeros(bins) + + for _, row in df.iterrows(): + typical_price = (row["high"] + row["low"] + row["close"]) / 3 + idx = np.searchsorted(bin_edges, typical_price, side="right") - 1 + idx = max(0, min(idx, bins - 1)) + volume_distribution[idx] += row["volume"] + + top_indices = volume_distribution.argsort()[::-1][:5] + levels = [] + for idx in top_indices: + level = (bin_edges[idx] + bin_edges[idx + 1]) / 2 + levels.append(level) + + return levels + + @classmethod + def support_resistance(cls, df: pd.DataFrame, window: int = 20, bins: int = 24) -> Dict[str, List[float]]: + """Combine multiple methods to find support and resistance levels""" + if df.empty: + return {"support": [], "resistance": []} - resistance_levels = [] - support_levels = [] + closes = df["close"] + highs = df["high"] + lows = df["low"] + + rolling_resistance = [] + rolling_support = [] + if len(closes) >= window * 2: + highs_roll = closes.rolling(window=window, center=True).max() + lows_roll = closes.rolling(window=window, center=True).min() + + for i in range(window, len(closes) - window): + if closes.iloc[i] == highs_roll.iloc[i]: + rolling_resistance.append(closes.iloc[i]) + if closes.iloc[i] == lows_roll.iloc[i]: + rolling_support.append(closes.iloc[i]) + + pivot_data = cls.pivot_levels(highs, lows, closes) + volume_nodes = cls.volume_profile_levels(df, bins) + + support_levels: List[float] = [] + resistance_levels: List[float] = [] + + pivot_point = pivot_data.get("pivot") if pivot_data else None + + if pivot_data: + support_levels.extend([pivot_data.get("s1"), pivot_data.get("s2")]) + resistance_levels.extend([pivot_data.get("r1"), pivot_data.get("r2")]) + + support_levels.extend(rolling_support) + resistance_levels.extend(rolling_resistance) + + # Split volume nodes around latest close + if volume_nodes: + last_close = closes.iloc[-1] + for level in volume_nodes: + if level <= last_close: + support_levels.append(level) + else: + resistance_levels.append(level) - for i in range(window, len(data) - window): - if data.iloc[i] == highs.iloc[i]: - resistance_levels.append(data.iloc[i]) - if data.iloc[i] == lows.iloc[i]: - support_levels.append(data.iloc[i]) + support_clean = sorted({level for level in support_levels if level is not None})[:5] + resistance_clean = sorted({level for level in resistance_levels if level is not None}, reverse=True)[:5] + volume_clean = sorted({level for level in volume_nodes if level is not None})[:5] return { - "resistance": sorted(list(set(resistance_levels)), reverse=True)[:5], - "support": sorted(list(set(support_levels)))[:5], + "support": support_clean, + "resistance": resistance_clean, + "pivot": pivot_point, + "volume_nodes": volume_clean, } @classmethod - def calculate_all_indicators(cls, klines: List[List]) -> Dict[str, Any]: + def calculate_all_indicators( + cls, + klines: List[List], + sr_window: int = 20, + volume_bins: int = 24, + ) -> Dict[str, Any]: """ Calculate all technical indicators for given klines data @@ -269,8 +354,8 @@ def safe_get_last(series_or_value): indicators["williams_r"] = None # Support and Resistance - if len(df) >= 40: - sr_levels = cls.support_resistance(df["close"]) + if len(df) >= sr_window * 2: + sr_levels = cls.support_resistance(df, window=sr_window, bins=volume_bins) indicators["support_resistance"] = sr_levels else: indicators["support_resistance"] = None diff --git a/vertex_flow/plugins/crypto_trading/monitoring.py b/vertex_flow/plugins/crypto_trading/monitoring.py new file mode 100644 index 0000000..7a0262d --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/monitoring.py @@ -0,0 +1,58 @@ +"""Monitoring and alerting helpers for trading workflows.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Callable, Dict, Iterable, List, Optional + +from persistence import DataStore + + +class EventLogger: + """Central logger that fans out to console and optional sinks.""" + + def __init__(self, datastore: Optional[DataStore] = None) -> None: + self.datastore = datastore + self.history: List[Dict[str, Any]] = [] + + def log(self, event_type: str, message: str, payload: Optional[Dict[str, Any]] = None) -> None: + entry = { + "timestamp": datetime.utcnow().isoformat(timespec="seconds") + "Z", + "event": event_type, + "message": message, + "payload": payload or {}, + } + self.history.append(entry) + print(f"[{entry['timestamp']}] {event_type}: {message}") + if self.datastore: + self.datastore.record_metric(entry) + + def recent(self, limit: int = 10) -> Iterable[Dict[str, Any]]: + return self.history[-limit:] + + +class AlertManager: + """Dispatch alerts to registered handlers and persist them.""" + + def __init__(self, datastore: Optional[DataStore] = None) -> None: + self.handlers: List[Callable[[Dict[str, Any]], None]] = [] + self.datastore = datastore + + def register(self, handler: Callable[[Dict[str, Any]], None]) -> None: + self.handlers.append(handler) + + def notify(self, severity: str, message: str, context: Optional[Dict[str, Any]] = None) -> None: + payload = { + "timestamp": datetime.utcnow().isoformat(timespec="seconds") + "Z", + "severity": severity, + "message": message, + "context": context or {}, + } + for handler in self.handlers: + try: + handler(payload) + except Exception as exc: # pragma: no cover - defensive + print(f"Alert handler failed: {exc}") + if self.datastore: + self.datastore.record_alert(payload) + diff --git a/vertex_flow/plugins/crypto_trading/persistence.py b/vertex_flow/plugins/crypto_trading/persistence.py new file mode 100644 index 0000000..7c80be0 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/persistence.py @@ -0,0 +1,75 @@ +"""Lightweight persistence utilities for the crypto trading plugin.""" + +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Iterable, Optional + + +@dataclass +class ConfigSnapshot: + """Serializable snapshot of runtime configuration.""" + + label: str + created_at: str + payload: Dict[str, Any] + + @classmethod + def from_dict(cls, label: str, payload: Dict[str, Any]) -> "ConfigSnapshot": + timestamp = datetime.utcnow().isoformat(timespec="seconds") + "Z" + return cls(label=label, created_at=timestamp, payload=payload) + + +class DataStore: + """Simple JSON-lines persistence for trades, metrics, and alerts.""" + + def __init__(self, base_path: str = "data") -> None: + self.base_path = Path(base_path) + self.base_path.mkdir(parents=True, exist_ok=True) + + def _append(self, file_name: str, record: Dict[str, Any]) -> None: + file_path = self.base_path / file_name + line = json.dumps(record, ensure_ascii=False) + with file_path.open("a", encoding="utf-8") as handle: + handle.write(line + "\n") + + def record_trade(self, trade: Dict[str, Any]) -> None: + """Persist a trade event for later auditing.""" + self._append("trades.jsonl", trade) + + def record_metric(self, metric: Dict[str, Any]) -> None: + """Persist aggregated metrics (risk, equity, latency).""" + self._append("metrics.jsonl", metric) + + def record_alert(self, alert: Dict[str, Any]) -> None: + """Persist alerts so they can be reviewed even if notification fails.""" + self._append("alerts.jsonl", alert) + + def record_config(self, snapshot: ConfigSnapshot) -> None: + """Persist a configuration snapshot.""" + self._append("config_history.jsonl", asdict(snapshot)) + + def tail(self, file_name: str, limit: int = 10) -> Iterable[Dict[str, Any]]: + """Return the most recent records from a JSON-lines file.""" + file_path = self.base_path / file_name + if not file_path.exists(): + return [] + with file_path.open("r", encoding="utf-8") as handle: + lines = handle.readlines()[-limit:] + return [json.loads(line) for line in lines if line.strip()] + + +class ConfigHistory: + """Manage configuration snapshots for auditing and rollback.""" + + def __init__(self, datastore: Optional[DataStore] = None) -> None: + self.datastore = datastore or DataStore() + + def snapshot(self, label: str, payload: Dict[str, Any]) -> ConfigSnapshot: + snap = ConfigSnapshot.from_dict(label, payload) + self.datastore.record_config(snap) + return snap + diff --git a/vertex_flow/plugins/crypto_trading/risk_manager.py b/vertex_flow/plugins/crypto_trading/risk_manager.py new file mode 100644 index 0000000..537a3e8 --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/risk_manager.py @@ -0,0 +1,110 @@ +"""Risk monitoring utilities for strategy and execution oversight.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Optional + +from monitoring import AlertManager, EventLogger +from persistence import DataStore + + +@dataclass +class RiskState: + """Keeps track of rolling performance and exposure.""" + + equity_high: float = 0.0 + equity_low: float = 0.0 + cumulative_pnl: float = 0.0 + daily_loss: float = 0.0 + last_update: Optional[str] = None + kill_switch: bool = False + + +class RiskMonitor: + """Evaluate orders and equity swings against configuration thresholds.""" + + def __init__( + self, + max_drawdown: float, + max_daily_loss: float, + max_order_notional: float, + logger: Optional[EventLogger] = None, + alerts: Optional[AlertManager] = None, + datastore: Optional[DataStore] = None, + ) -> None: + self.max_drawdown = max_drawdown + self.max_daily_loss = max_daily_loss + self.max_order_notional = max_order_notional + self.logger = logger + self.alerts = alerts + self.datastore = datastore + self.state = RiskState() + + def _emit_alert(self, severity: str, message: str, extra: Dict[str, Any]) -> None: + if self.logger: + self.logger.log("risk", message, extra) + if self.alerts: + self.alerts.notify(severity, message, extra) + + def allow_order(self, notional: float) -> bool: + """Check if the proposed order stays within per-trade limits.""" + if self.state.kill_switch: + self._emit_alert("critical", "Order rejected: kill switch active", {"notional": notional}) + return False + if notional > self.max_order_notional: + self._emit_alert( + "warning", + "Order size exceeds configured limit", + {"notional": notional, "limit": self.max_order_notional}, + ) + return False + return True + + def update_equity(self, equity: float) -> None: + """Track equity curve and evaluate drawdown / daily loss.""" + now = datetime.utcnow().isoformat(timespec="seconds") + "Z" + if self.state.equity_high == 0.0: + self.state.equity_high = equity + self.state.equity_low = equity + else: + self.state.equity_high = max(self.state.equity_high, equity) + self.state.equity_low = min(self.state.equity_low, equity) + + drawdown = 0.0 + if self.state.equity_high > 0: + drawdown = (self.state.equity_high - equity) / self.state.equity_high + daily_loss = max(0.0, self.state.equity_high - equity) + + self.state.daily_loss = daily_loss + self.state.last_update = now + + record = { + "timestamp": now, + "equity": equity, + "drawdown": drawdown, + "daily_loss": daily_loss, + } + if self.datastore: + self.datastore.record_metric({"metric": "equity", **record}) + + if drawdown >= self.max_drawdown and not self.state.kill_switch: + self.state.kill_switch = True + self._emit_alert("critical", "Max drawdown breached; kill switch engaged", record) + elif daily_loss >= self.max_daily_loss: + self._emit_alert("warning", "Daily loss threshold breached", record) + + def record_trade(self, trade: Dict[str, Any]) -> None: + """Record realised PnL from completed trades.""" + pnl = float(trade.get("pnl", 0.0)) + self.state.cumulative_pnl += pnl + if self.datastore: + self.datastore.record_trade(trade) + if pnl < 0 and abs(pnl) > self.max_order_notional * 0.1: + self._emit_alert("info", "Large loss recorded", {"pnl": pnl, "trade": trade}) + + def reset_kill_switch(self) -> None: + self.state.kill_switch = False + self._emit_alert("info", "Kill switch reset", {}) + diff --git a/vertex_flow/plugins/crypto_trading/scheduler.py b/vertex_flow/plugins/crypto_trading/scheduler.py new file mode 100644 index 0000000..3a591cf --- /dev/null +++ b/vertex_flow/plugins/crypto_trading/scheduler.py @@ -0,0 +1,67 @@ +"""Basic scheduler for orchestrating strategy callbacks.""" + +from __future__ import annotations + +import time +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, Optional + +from monitoring import EventLogger + + +@dataclass +class ScheduledTask: + name: str + interval: float + func: Callable[[], Any] + last_run: float = field(default=0.0) + enabled: bool = field(default=True) + + def ready(self, now: float) -> bool: + if not self.enabled: + return False + return now - self.last_run >= self.interval + + +class StrategyScheduler: + """Tiny cooperative scheduler that runs registered callables.""" + + def __init__(self, logger: Optional[EventLogger] = None) -> None: + self.logger = logger + self.tasks: Dict[str, ScheduledTask] = {} + + def add_task(self, name: str, interval: float, func: Callable[[], Any]) -> None: + self.tasks[name] = ScheduledTask(name=name, interval=interval, func=func) + if self.logger: + self.logger.log("scheduler", f"Registered task '{name}'", {"interval": interval}) + + def remove_task(self, name: str) -> None: + if name in self.tasks: + self.tasks.pop(name) + if self.logger: + self.logger.log("scheduler", f"Removed task '{name}'", {}) + + def enable(self, name: str) -> None: + if name in self.tasks: + self.tasks[name].enabled = True + + def disable(self, name: str) -> None: + if name in self.tasks: + self.tasks[name].enabled = False + + def run_pending(self) -> None: + now = time.time() + for task in self.tasks.values(): + if task.ready(now): + if self.logger: + self.logger.log("scheduler", f"Running task '{task.name}'", {}) + task.func() + task.last_run = now + + def run_for(self, duration: float, poll_interval: float = 1.0) -> None: + """Run the scheduler loop for a bounded amount of time.""" + end = time.time() + duration + while time.time() < end: + self.run_pending() + time.sleep(poll_interval) + diff --git a/vertex_flow/plugins/crypto_trading/show_indicators.py b/vertex_flow/plugins/crypto_trading/show_indicators.py index 4d8f8be..f720f11 100644 --- a/vertex_flow/plugins/crypto_trading/show_indicators.py +++ b/vertex_flow/plugins/crypto_trading/show_indicators.py @@ -3,6 +3,7 @@ 展示持仓技术指标数据 """ +import argparse import traceback from client import CryptoTradingClient @@ -27,6 +28,17 @@ def format_number(value): return str(value) +def safe_float(value): + """尝试将任意值转换为浮点数""" + try: + if value in (None, ""): + return None + number = float(value) + return number + except (TypeError, ValueError): + return None + + def get_signal_emoji(rsi, macd_histogram): """根据RSI和MACD获取信号表情""" if rsi is None or macd_histogram is None: @@ -42,12 +54,40 @@ def get_signal_emoji(rsi, macd_histogram): return "⚪" # 中性 -def display_indicators(symbol, indicators, position_info=""): +def format_price(value): + """格式化价格,若无数据返回N/A""" + if value is None: + return "N/A" + return f"${format_number(value)}" + + +def display_indicators(symbol, indicators, position_info="", position_metrics=None): """显示单个币种的技术指标""" print(f"\n📊 {symbol}") if position_info: print(f" {position_info}") + if position_metrics: + entry_price = position_metrics.get("entry_price") + leverage = position_metrics.get("leverage") + support_level = position_metrics.get("support_level") + resistance_level = position_metrics.get("resistance_level") + pivot_level = position_metrics.get("pivot") + volume_node = position_metrics.get("volume_node") + + leverage_display = "N/A" + if leverage is not None: + leverage_display = f"{format_number(leverage)}x" + + print(f" 💰 买入成本: {format_price(entry_price)}") + print(f" 🎯 杠杆倍数: {leverage_display}") + print(f" 🛡 支撑位: {format_price(support_level)}") + print(f" 🧱 阻力位: {format_price(resistance_level)}") + if pivot_level is not None: + print(f" 📍 枢轴点: {format_price(pivot_level)}") + if volume_node is not None: + print(f" 📦 成交量峰值: {format_price(volume_node)}") + if "error" in indicators: print(f" ❌ 计算指标出错: {indicators['error']}") return @@ -99,80 +139,172 @@ def display_indicators(symbol, indicators, position_info=""): print(f" {signal_emoji} 综合信号") +def parse_args(): + parser = argparse.ArgumentParser(description="展示现货或合约的技术指标") + parser.add_argument( + "-m", + "--market", + choices=["spot", "futures", "both"], + default="both", + help="选择展示现货、合约或全部 (默认: both)", + ) + return parser.parse_args() + + def main(): + args = parse_args() + show_spot = args.market in {"spot", "both"} + show_futures = args.market in {"futures", "both"} + try: # 初始化客户端 config = CryptoTradingConfig() client = CryptoTradingClient(config) - print("🔸 现货持仓技术指标") - print("=" * 40) - - # 获取现货持仓 - spot_positions = client.get_spot_positions("okx") - - if spot_positions.get("success") and spot_positions.get("data"): - for position in spot_positions["data"]: - currency = position["currency"] - balance = position["balance"] - - # 跳过USDT和余额为0的币种 - if currency != "USDT" and float(balance) > 0: - symbol = f"{currency}-USDT" - position_info = f"持仓量: {format_number(float(balance))} {currency}" - - try: - # 获取K线数据 - klines = client.get_klines("okx", symbol, "1h", 100) - if klines and len(klines) > 26: # 确保有足够数据计算MACD - indicators = TechnicalIndicators.calculate_all_indicators(klines) - display_indicators(symbol, indicators, position_info) - else: + if show_spot: + print("🔸 现货持仓技术指标") + print("=" * 40) + + # 获取现货持仓 + spot_positions = client.get_spot_positions("okx") + + if spot_positions.get("success") and spot_positions.get("data"): + for position in spot_positions["data"]: + currency = position["currency"] + balance = position["balance"] + + # 跳过USDT和余额为0的币种 + if currency != "USDT" and float(balance) > 0: + symbol = f"{currency}-USDT" + position_info = f"持仓量: {format_number(float(balance))} {currency}" + + try: + # 获取K线数据 + klines = client.get_klines("okx", symbol, "1h", 100) + if klines and len(klines) > 26: # 确保有足够数据计算MACD + indicators = TechnicalIndicators.calculate_all_indicators(klines) + + support_level = None + resistance_level = None + pivot_level = None + volume_node = None + support_data = indicators.get("support_resistance") + if support_data: + support_levels = support_data.get("support") or [] + if support_levels: + support_level = support_levels[-1] + resistance_levels = support_data.get("resistance") or [] + if resistance_levels: + resistance_level = resistance_levels[0] + pivot_level = support_data.get("pivot") + volume_nodes = support_data.get("volume_nodes") or [] + if volume_nodes: + volume_node = volume_nodes[0] + + entry_price = None + for key in ("avg_price", "avgPrice", "avg_px", "average_price"): + candidate = safe_float(position.get(key)) + if candidate: + entry_price = candidate + break + + metrics = { + "entry_price": entry_price, + "leverage": 1, + "support_level": support_level, + "resistance_level": resistance_level, + "pivot": pivot_level, + "volume_node": volume_node, + } + + display_indicators(symbol, indicators, position_info, metrics) + else: + print(f"\n📊 {symbol}") + print(f" {position_info}") + print(f" ❌ K线数据不足 (需要至少26条)") + except Exception as e: print(f"\n📊 {symbol}") print(f" {position_info}") - print(f" ❌ K线数据不足 (需要至少26条)") - except Exception as e: - print(f"\n📊 {symbol}") - print(f" {position_info}") - print(f" ❌ 获取数据失败: {str(e)}") - else: - print(" ❌ 无法获取现货持仓数据") - - print("\n\n🔸 合约持仓技术指标") - print("=" * 40) - - # 获取合约持仓 - futures_positions = client.get_futures_positions("okx") - - if futures_positions.get("success") and futures_positions.get("data"): - for position in futures_positions["data"]: - symbol = position["symbol"] - size = position["size"] - side = position["side"] - unrealized_pnl = position.get("unrealized_pnl", 0) - - if float(size) != 0: - position_info = f"方向: {side}, 数量: {format_number(float(size))}" - if unrealized_pnl: - pnl_str = f"${format_number(float(unrealized_pnl))}" - position_info += f"\n 未实现盈亏: {pnl_str}" - - try: - # 获取K线数据 - klines = client.get_klines("okx", symbol, "1h", 100) - if klines and len(klines) > 26: - indicators = TechnicalIndicators.calculate_all_indicators(klines) - display_indicators(symbol, indicators, position_info) - else: + print(f" ❌ 获取数据失败: {str(e)}") + else: + print(" ❌ 无法获取现货持仓数据") + + if show_futures: + if show_spot: + print() + print("🔸 合约持仓技术指标") + print("=" * 40) + + # 获取合约持仓 + futures_positions = client.get_futures_positions("okx") + + if futures_positions.get("success") and futures_positions.get("data"): + for position in futures_positions["data"]: + symbol = position["symbol"] + size = position["size"] + side = position["side"] + unrealized_pnl = position.get("unrealized_pnl", 0) + + if float(size) != 0: + position_info = f"方向: {side}, 数量: {format_number(float(size))}" + if unrealized_pnl: + pnl_str = f"${format_number(float(unrealized_pnl))}" + position_info += f"\n 未实现盈亏: {pnl_str}" + + try: + # 获取K线数据 + klines = client.get_klines("okx", symbol, "1h", 100) + if klines and len(klines) > 26: + indicators = TechnicalIndicators.calculate_all_indicators(klines) + + support_level = None + resistance_level = None + pivot_level = None + volume_node = None + support_data = indicators.get("support_resistance") + if support_data: + support_levels = support_data.get("support") or [] + if support_levels: + support_level = support_levels[-1] + resistance_levels = support_data.get("resistance") or [] + if resistance_levels: + resistance_level = resistance_levels[0] + pivot_level = support_data.get("pivot") + volume_nodes = support_data.get("volume_nodes") or [] + if volume_nodes: + volume_node = volume_nodes[0] + + entry_price = safe_float(position.get("avg_price")) + if entry_price is None: + entry_price = safe_float(position.get("avg_price_str")) + + leverage = safe_float(position.get("leverage")) + if not leverage: + notional = abs(position.get("notional", 0)) + margin = position.get("margin", 0) + if margin: + leverage = notional / margin + + metrics = { + "entry_price": entry_price, + "leverage": leverage, + "support_level": support_level, + "resistance_level": resistance_level, + "pivot": pivot_level, + "volume_node": volume_node, + } + + display_indicators(symbol, indicators, position_info, metrics) + else: + print(f"\n📊 {symbol}") + print(f" {position_info}") + print(f" ❌ K线数据不足 (需要至少26条)") + except Exception as e: print(f"\n📊 {symbol}") print(f" {position_info}") - print(f" ❌ K线数据不足 (需要至少26条)") - except Exception as e: - print(f"\n📊 {symbol}") - print(f" {position_info}") - print(f" ❌ 获取数据失败: {str(e)}") - else: - print(" ❌ 无法获取合约持仓数据") + print(f" ❌ 获取数据失败: {str(e)}") + else: + print(" ❌ 无法获取合约持仓数据") print("\n" + "=" * 60) print("报告生成完成 ✅") diff --git a/vertex_flow/plugins/crypto_trading/trading.py b/vertex_flow/plugins/crypto_trading/trading.py index e31e962..edd8886 100644 --- a/vertex_flow/plugins/crypto_trading/trading.py +++ b/vertex_flow/plugins/crypto_trading/trading.py @@ -4,14 +4,29 @@ import time from decimal import ROUND_DOWN, Decimal -from typing import Any, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Union try: from .client import CryptoTradingClient from .indicators import TechnicalIndicators + from .execution_controls import ExecutionControls, ExecutionParameters + from .monitoring import AlertManager, EventLogger + from .persistence import ConfigHistory, DataStore + from .risk_manager import RiskMonitor + from .scheduler import StrategyScheduler except ImportError: from client import CryptoTradingClient from indicators import TechnicalIndicators + from execution_controls import ExecutionControls, ExecutionParameters + from monitoring import AlertManager, EventLogger + from persistence import ConfigHistory, DataStore + from risk_manager import RiskMonitor + from scheduler import StrategyScheduler + +try: + from .backtester import Backtester, MovingAverageCrossStrategy, Strategy +except ImportError: + from backtester import Backtester, MovingAverageCrossStrategy, Strategy class TradingEngine: @@ -20,6 +35,29 @@ class TradingEngine: def __init__(self, client: CryptoTradingClient): self.client = client self.config = client.config + trading_cfg = self.config.trading_config + + self.datastore = DataStore() + self.logger = EventLogger(self.datastore) + self.alerts = AlertManager(self.datastore) + self.scheduler = StrategyScheduler(self.logger) + self.config_history = ConfigHistory(self.datastore) + self.config_history.snapshot("runtime", self.config.get_sanitised_config()) + + execution_params = ExecutionParameters( + slippage_buffer=trading_cfg.slippage_buffer, + max_order_notional=trading_cfg.max_order_notional, + ) + self.execution_controls = ExecutionControls(execution_params) + + self.risk_monitor = RiskMonitor( + max_drawdown=trading_cfg.max_drawdown, + max_daily_loss=trading_cfg.max_daily_loss, + max_order_notional=trading_cfg.max_order_notional, + logger=self.logger, + alerts=self.alerts, + datastore=self.datastore, + ) def calculate_position_size(self, exchange: str, symbol: str, risk_percentage: float = None) -> float: """ @@ -106,9 +144,22 @@ def buy_market(self, exchange: str, symbol: str, amount_usdt: float) -> Dict[str if "error" in ticker: return {"error": f"Failed to get ticker: {ticker['error']}"} - current_price = ticker["price"] - quantity = amount_usdt / current_price - quantity = self.format_quantity(exchange, symbol, quantity) + current_price = float(ticker["price"]) + raw_quantity = amount_usdt / current_price + quantity = self.format_quantity(exchange, symbol, raw_quantity) + + prepared = self.execution_controls.prepare_order("buy", quantity, current_price) + quantity = self.format_quantity(exchange, symbol, prepared["quantity"]) + proposed_notional = quantity * current_price + + if not self.risk_monitor.allow_order(proposed_notional): + return { + "error": "Order blocked by risk controls", + "exchange": exchange, + "symbol": symbol, + "side": "buy", + "notional": proposed_notional, + } # Place market buy order if exchange not in self.client.exchanges: @@ -121,6 +172,17 @@ def buy_market(self, exchange: str, symbol: str, amount_usdt: float) -> Dict[str # Calculate stop loss and take profit sl_tp = self.calculate_stop_loss_take_profit(current_price, "buy") + self.logger.log( + "order", + "Executed market buy", + { + "exchange": exchange, + "symbol": symbol, + "notional": proposed_notional, + "slippage_price": prepared["price"], + }, + ) + result = { "status": "success", "exchange": exchange, @@ -128,12 +190,13 @@ def buy_market(self, exchange: str, symbol: str, amount_usdt: float) -> Dict[str "side": "buy", "type": "market", "quantity": quantity, - "amount_usdt": amount_usdt, + "amount_usdt": proposed_notional, "estimated_price": current_price, "stop_loss": sl_tp["stop_loss"], "take_profit": sl_tp["take_profit"], "order_result": order_result, "timestamp": time.time(), + "risk": {"notional": proposed_notional, "slippage_price": prepared["price"]}, } return result @@ -159,9 +222,22 @@ def sell_market(self, exchange: str, symbol: str, quantity: float) -> Dict[str, if "error" in ticker: return {"error": f"Failed to get ticker: {ticker['error']}"} - current_price = ticker["price"] + current_price = float(ticker["price"]) quantity = self.format_quantity(exchange, symbol, quantity) + prepared = self.execution_controls.prepare_order("sell", quantity, current_price) + quantity = self.format_quantity(exchange, symbol, prepared["quantity"]) + proposed_notional = quantity * current_price + + if not self.risk_monitor.allow_order(proposed_notional): + return { + "error": "Order blocked by risk controls", + "exchange": exchange, + "symbol": symbol, + "side": "sell", + "notional": proposed_notional, + } + # Place market sell order if exchange not in self.client.exchanges: return {"error": f"Exchange {exchange} not configured"} @@ -173,6 +249,17 @@ def sell_market(self, exchange: str, symbol: str, quantity: float) -> Dict[str, # Calculate stop loss and take profit sl_tp = self.calculate_stop_loss_take_profit(current_price, "sell") + self.logger.log( + "order", + "Executed market sell", + { + "exchange": exchange, + "symbol": symbol, + "notional": proposed_notional, + "slippage_price": prepared["price"], + }, + ) + result = { "status": "success", "exchange": exchange, @@ -181,11 +268,12 @@ def sell_market(self, exchange: str, symbol: str, quantity: float) -> Dict[str, "type": "market", "quantity": quantity, "estimated_price": current_price, - "estimated_amount_usdt": quantity * current_price, + "estimated_amount_usdt": proposed_notional, "stop_loss": sl_tp["stop_loss"], "take_profit": sl_tp["take_profit"], "order_result": order_result, "timestamp": time.time(), + "risk": {"notional": proposed_notional, "slippage_price": prepared["price"]}, } return result @@ -208,6 +296,18 @@ def buy_limit(self, exchange: str, symbol: str, quantity: float, price: float) - """ try: quantity = self.format_quantity(exchange, symbol, quantity) + capped_quantity, capped_notional = self.execution_controls.cap_notional(quantity, price) + quantity = self.format_quantity(exchange, symbol, capped_quantity) + proposed_notional = quantity * price + + if not self.risk_monitor.allow_order(proposed_notional): + return { + "error": "Order blocked by risk controls", + "exchange": exchange, + "symbol": symbol, + "side": "buy", + "notional": proposed_notional, + } if exchange not in self.client.exchanges: return {"error": f"Exchange {exchange} not configured"} @@ -219,6 +319,17 @@ def buy_limit(self, exchange: str, symbol: str, quantity: float, price: float) - # Calculate stop loss and take profit sl_tp = self.calculate_stop_loss_take_profit(price, "buy") + self.logger.log( + "order", + "Placed limit buy", + { + "exchange": exchange, + "symbol": symbol, + "notional": proposed_notional, + "price": price, + }, + ) + result = { "status": "success", "exchange": exchange, @@ -227,11 +338,12 @@ def buy_limit(self, exchange: str, symbol: str, quantity: float, price: float) - "type": "limit", "quantity": quantity, "price": price, - "amount_usdt": quantity * price, + "amount_usdt": proposed_notional, "stop_loss": sl_tp["stop_loss"], "take_profit": sl_tp["take_profit"], "order_result": order_result, "timestamp": time.time(), + "risk": {"notional": proposed_notional}, } return result @@ -254,6 +366,18 @@ def sell_limit(self, exchange: str, symbol: str, quantity: float, price: float) """ try: quantity = self.format_quantity(exchange, symbol, quantity) + capped_quantity, capped_notional = self.execution_controls.cap_notional(quantity, price) + quantity = self.format_quantity(exchange, symbol, capped_quantity) + proposed_notional = quantity * price + + if not self.risk_monitor.allow_order(proposed_notional): + return { + "error": "Order blocked by risk controls", + "exchange": exchange, + "symbol": symbol, + "side": "sell", + "notional": proposed_notional, + } if exchange not in self.client.exchanges: return {"error": f"Exchange {exchange} not configured"} @@ -265,6 +389,17 @@ def sell_limit(self, exchange: str, symbol: str, quantity: float, price: float) # Calculate stop loss and take profit sl_tp = self.calculate_stop_loss_take_profit(price, "sell") + self.logger.log( + "order", + "Placed limit sell", + { + "exchange": exchange, + "symbol": symbol, + "notional": proposed_notional, + "price": price, + }, + ) + result = { "status": "success", "exchange": exchange, @@ -273,11 +408,12 @@ def sell_limit(self, exchange: str, symbol: str, quantity: float, price: float) "type": "limit", "quantity": quantity, "price": price, - "amount_usdt": quantity * price, + "amount_usdt": proposed_notional, "stop_loss": sl_tp["stop_loss"], "take_profit": sl_tp["take_profit"], "order_result": order_result, "timestamp": time.time(), + "risk": {"notional": proposed_notional}, } return result @@ -285,6 +421,77 @@ def sell_limit(self, exchange: str, symbol: str, quantity: float, price: float) except Exception as e: return {"error": f"Failed to execute limit sell order: {str(e)}", "exchange": exchange, "symbol": symbol} + def schedule_strategy(self, name: str, interval: float, callback: Callable[[], Any]) -> None: + """Register a named strategy callback with the cooperative scheduler.""" + self.scheduler.add_task(name, interval, callback) + + def run_scheduler(self, duration: float, poll_interval: float = 1.0) -> None: + """Run the scheduler loop for a bounded amount of time.""" + self.scheduler.run_for(duration, poll_interval) + + def run_backtest( + self, + exchange: str, + symbol: str, + strategy: Optional[Strategy] = None, + interval: str = "1h", + limit: int = 200, + initial_equity: Optional[float] = None, + ) -> Dict[str, Any]: + """Fetch historical data and execute a simple backtest.""" + candles = self.client.get_klines(exchange, symbol, interval, limit) + if not candles: + return {"error": "No historical data available"} + + runner = strategy or MovingAverageCrossStrategy() + backtester = Backtester( + strategy=runner, + initial_equity=initial_equity or self.calculate_position_size(exchange, symbol), + ) + result = backtester.run(candles) + payload = { + "strategy": result.strategy, + "starting_equity": result.starting_equity, + "ending_equity": result.ending_equity, + "total_return": result.total_return, + "max_drawdown": result.max_drawdown, + "trade_count": len(result.trades), + "trades": [trade.__dict__ for trade in result.trades], + } + self.logger.log( + "backtest", + f"Backtest finished for {symbol}", + { + "strategy": result.strategy, + "return": payload["total_return"], + "max_drawdown": result.max_drawdown, + }, + ) + return payload + + def capture_equity_snapshot(self, exchange: str) -> Dict[str, Any]: + """Update the risk monitor with the latest balance-derived equity.""" + account = self.client.get_account_info(exchange) + if "error" in account: + return {"error": account["error"], "exchange": exchange} + + balances = account.get("balances", {}) + usdt = balances.get("USDT") + equity = 0.0 + if isinstance(usdt, dict): + equity = float(usdt.get("total", usdt.get("available", 0.0))) + elif isinstance(usdt, (int, float)): + equity = float(usdt) + + self.risk_monitor.update_equity(equity) + snapshot = { + "exchange": exchange, + "equity": equity, + "timestamp": time.time(), + } + self.logger.log("equity", f"Equity snapshot for {exchange}", {"equity": equity}) + return snapshot + def auto_trade_by_signals(self, exchange: str, symbol: str, amount_usdt: float = None) -> Dict[str, Any]: """ Execute trade based on technical analysis signals @@ -333,7 +540,7 @@ def auto_trade_by_signals(self, exchange: str, symbol: str, amount_usdt: float = # This is simplified - in practice, you'd track your positions ticker = self.client.get_ticker(exchange, symbol) if "error" not in ticker: - quantity = amount_usdt / ticker["price"] + quantity = amount_usdt / float(ticker["price"]) result = self.sell_market(exchange, symbol, quantity) else: result = {"error": "Failed to get current price for sell order"}