├── claude_config_example.json ├── claude_config_with_api_key_example.json ├── run-crypto-mcp.sh ├── Package.resolved ├── .gitignore ├── Package.swift ├── LICENSE ├── QUICK_REFERENCE.md ├── check_config.sh ├── CHANGELOG.md ├── Tests └── CryptoAnalysisMCPTests │ └── CryptoAnalysisMCPTests.swift ├── EXAMPLES.md ├── Sources └── CryptoAnalysisMCP │ ├── Models.swift │ ├── DexPaprikaDataProvider.swift │ ├── AnalysisFormatters.swift │ ├── CryptoDataProvider.swift │ ├── TechnicalAnalyzer.swift │ ├── DexPaprikaAdvanced.swift │ ├── SupportResistanceAnalyzer.swift │ ├── SimpleMCP.swift │ └── Main.swift ├── PROMPTS.md └── README.md /claude_config_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "crypto-analysis": { 4 | "command": "/path/to/CryptoAnalysisMCP/crypto-analysis-mcp" 5 | }, 6 | "other-mcp": { 7 | "command": "/path/to/other-mcp" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /claude_config_with_api_key_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "crypto-analysis": { 4 | "command": "/path/to/CryptoAnalysisMCP/crypto-analysis-mcp", 5 | "env": { 6 | "COINPAPRIKA_API_KEY": "your-api-key-here" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /run-crypto-mcp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Wrapper script for CryptoAnalysisMCP 3 | 4 | # Get the directory of this script 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | 7 | # Execute the binary with proper environment 8 | exec "$DIR/.build/release/CryptoAnalysisMCP" "$@" 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-argument-parser", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-argument-parser", 7 | "state" : { 8 | "revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b", 9 | "version" : "1.5.1" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-log", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-log", 16 | "state" : { 17 | "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", 18 | "version" : "1.6.3" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Swift 2 | .DS_Store 3 | /.build 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | DerivedData/ 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | 11 | # Build artifacts 12 | crypto-analysis-mcp 13 | *.dSYM 14 | 15 | # IDE 16 | .vscode/ 17 | .idea/ 18 | *.swp 19 | *.swo 20 | *~ 21 | 22 | # Environment 23 | .env 24 | .env.local 25 | 26 | # Logs 27 | *.log 28 | 29 | # OS generated files 30 | .DS_Store 31 | .DS_Store? 32 | ._* 33 | .Spotlight-V100 34 | .Trashes 35 | ehthumbs.db 36 | Thumbs.db 37 | 38 | # Reddit draft (temporary) 39 | reddit-post-draft.md 40 | 41 | # Test outputs 42 | test_output/ 43 | coverage/ 44 | 45 | # Marketing and drafts - NEVER commit these 46 | Marketing/ 47 | *draft*.md 48 | *tweet*.md 49 | *reddit*.md 50 | *post*.md 51 | 52 | # Archives and backups 53 | .archive/ 54 | *.backup 55 | *.disabled 56 | *.temp 57 | *.tmp 58 | .*.backup 59 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "CryptoAnalysisMCP", 6 | platforms: [ 7 | .macOS(.v13) 8 | ], 9 | products: [ 10 | .executable(name: "CryptoAnalysisMCP", targets: ["CryptoAnalysisMCP"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), 14 | .package(url: "https://github.com/apple/swift-log", from: "1.0.0") 15 | ], 16 | targets: [ 17 | .executableTarget( 18 | name: "CryptoAnalysisMCP", 19 | dependencies: [ 20 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 21 | .product(name: "Logging", package: "swift-log") 22 | ] 23 | ), 24 | .testTarget( 25 | name: "CryptoAnalysisMCPTests", 26 | dependencies: ["CryptoAnalysisMCP"] 27 | ) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 CryptoAnalysisMCP Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QUICK_REFERENCE.md: -------------------------------------------------------------------------------- 1 | # CryptoAnalysisMCP Quick Reference 2 | 3 | ## 🚀 Essential Prompts 4 | 5 | ### Price & Trend 6 | ``` 7 | "What's BTC doing?" 8 | "ETH price and trend" 9 | "Is SOL bullish?" 10 | ``` 11 | 12 | ### Technical Indicators 13 | ``` 14 | "RSI and MACD for BTC" 15 | "Show me ETH indicators" 16 | "SOL technical analysis" 17 | ``` 18 | 19 | ### Trading Signals 20 | ``` 21 | "Buy or sell BTC?" 22 | "ETH entry points" 23 | "SOL trading signals" 24 | ``` 25 | 26 | ### Risk Levels 27 | - `conservative` - Safe entries only 28 | - `moderate` - Balanced approach 29 | - `aggressive` - High risk/reward 30 | 31 | ### Timeframes 32 | - `daily` - Best for free tier 33 | - `4h` - Pro tier only 34 | - `weekly` - Longer trends 35 | 36 | ## 📊 Common Commands 37 | 38 | **Full Analysis:** 39 | ``` 40 | "Complete analysis of BTC" 41 | "Wall Street report on ETH" 42 | "Professional SOL assessment" 43 | ``` 44 | 45 | **Pattern Detection:** 46 | ``` 47 | "Find chart patterns in BTC" 48 | "Triangle patterns in crypto" 49 | "Reversal setups" 50 | ``` 51 | 52 | **Support/Resistance:** 53 | ``` 54 | "Key levels for BTC" 55 | "ETH support and resistance" 56 | "Where's SOL headed?" 57 | ``` 58 | 59 | **Multi-Timeframe:** 60 | ``` 61 | "BTC on all timeframes" 62 | "Multi-timeframe ETH" 63 | "Big picture for SOL" 64 | ``` 65 | 66 | ## 💰 Position Sizing 67 | 68 | ``` 69 | "2% risk on $10k account" 70 | "Position size for BTC" 71 | "How much ETH can I buy?" 72 | ``` 73 | 74 | ## 🎯 Quick Decisions 75 | 76 | ``` 77 | "BTC: Buy, sell, or hold?" 78 | "Quick take on ETH" 79 | "SOL in 10 seconds" 80 | ``` 81 | 82 | --- 83 | 84 | 📖 Full guide: [PROMPTS.md](./PROMPTS.md) 85 | -------------------------------------------------------------------------------- /check_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "CryptoAnalysisMCP Configuration Checker" 4 | echo "======================================" 5 | echo "" 6 | 7 | # Get the directory of this script 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 9 | 10 | # Check if binary exists in local build 11 | if [ -f "$DIR/.build/release/CryptoAnalysisMCP" ]; then 12 | echo "✅ Binary found at: $DIR/.build/release/CryptoAnalysisMCP" 13 | echo " Size: $(ls -lh $DIR/.build/release/CryptoAnalysisMCP | awk '{print $5}')" 14 | else 15 | echo "❌ Binary NOT found in .build/release/" 16 | echo " Run ./build-release.sh first" 17 | fi 18 | 19 | # Check if binary exists in /usr/local/bin 20 | if [ -f "/usr/local/bin/crypto-analysis-mcp" ]; then 21 | echo "✅ Global binary found at: /usr/local/bin/crypto-analysis-mcp" 22 | fi 23 | 24 | echo "" 25 | echo "Claude Desktop Configuration" 26 | echo "============================" 27 | echo "" 28 | echo "Add one of these to your ~/Library/Application Support/Claude/claude_desktop_config.json:" 29 | echo "" 30 | echo "Option 1 - Local installation:" 31 | echo '{' 32 | echo ' "mcpServers": {' 33 | echo ' "crypto-analysis": {' 34 | echo ' "command": "'$DIR'/crypto-analysis-mcp"' 35 | echo ' }' 36 | echo ' }' 37 | echo '}' 38 | echo "" 39 | echo "Option 2 - Global installation:" 40 | echo '{' 41 | echo ' "mcpServers": {' 42 | echo ' "crypto-analysis": {' 43 | echo ' "command": "/usr/local/bin/crypto-analysis-mcp"' 44 | echo ' }' 45 | echo ' }' 46 | echo '}' 47 | echo "" 48 | echo "After updating config:" 49 | echo "1. Save the file" 50 | echo "2. Restart Claude Desktop completely" 51 | echo "3. The crypto-analysis MCP should appear in available tools" 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to CryptoAnalysisMCP will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.1.0] - 2025-06-27 9 | 10 | ### Added 11 | - 🔥 **DexPaprika Integration**: Real-time DEX analytics across 23+ blockchain networks 12 | - **Token Liquidity Analysis**: Check liquidity pools across all DEXes 13 | - **DEX Price Comparison**: Compare token prices across different exchanges 14 | - **Pool Analytics**: Detailed metrics for specific liquidity pools 15 | - **Network Overview**: Get top pools and DEX information by network 16 | - **Advanced Token Search**: Filter by liquidity, volume, and network 17 | - **OHLCV Data**: Historical candlestick data for liquidity pools 18 | 19 | ### Fixed 20 | - Access control issues for clean builds 21 | - Made logger internal for extension access 22 | - Fixed all dataProvider references to use proper accessors 23 | - Removed disabled files from source directory 24 | 25 | ### Changed 26 | - Updated README with comprehensive v1.1 feature documentation 27 | - Enhanced error handling for DEX API responses 28 | - Improved build process for fresh installations 29 | 30 | ## [1.0.0] - 2025-06-01 31 | 32 | ### Added 33 | - Initial release with core cryptocurrency analysis features 34 | - Real-time price data for 2,500+ cryptocurrencies 35 | - Technical indicators (RSI, MACD, Moving Averages, Bollinger Bands) 36 | - Chart pattern detection (Head & Shoulders, Triangles, Double Tops/Bottoms) 37 | - Support & Resistance level identification 38 | - Trading signals with risk management 39 | - Multi-timeframe analysis 40 | - CoinPaprika API integration 41 | -------------------------------------------------------------------------------- /Tests/CryptoAnalysisMCPTests/CryptoAnalysisMCPTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CryptoAnalysisMCP 3 | 4 | final class CryptoAnalysisMCPTests: XCTestCase { 5 | 6 | func testTechnicalIndicatorCalculations() async throws { 7 | // Test data - simple ascending price pattern 8 | let testData = createTestCandleData() 9 | let analyzer = TechnicalAnalyzer() 10 | 11 | // Test SMA calculation 12 | let smaResults = analyzer.calculateSMA(data: testData, period: 5) 13 | XCTAssertFalse(smaResults.isEmpty, "SMA results should not be empty") 14 | 15 | // Test RSI calculation 16 | let rsiResults = analyzer.calculateRSI(data: testData, period: 14) 17 | XCTAssertFalse(rsiResults.isEmpty, "RSI results should not be empty") 18 | 19 | if let lastRSI = rsiResults.last { 20 | XCTAssertTrue(lastRSI.value >= 0 && lastRSI.value <= 100, "RSI should be between 0 and 100") 21 | } 22 | } 23 | 24 | func testChartPatternDetection() async throws { 25 | let testData = createHeadAndShouldersPattern() 26 | let recognizer = ChartPatternRecognizer() 27 | 28 | let patterns = await recognizer.detectPatterns(data: testData) 29 | 30 | // Should detect at least some patterns in structured test data 31 | XCTAssertFalse(patterns.isEmpty, "Should detect patterns in test data") 32 | 33 | // Check pattern confidence 34 | for pattern in patterns { 35 | XCTAssertTrue(pattern.confidence >= 0.0 && pattern.confidence <= 1.0, 36 | "Pattern confidence should be between 0 and 1") 37 | } 38 | } 39 | 40 | func testSupportResistanceAnalysis() async throws { 41 | let testData = createTestCandleData() 42 | let analyzer = SupportResistanceAnalyzer() 43 | 44 | let levels = await analyzer.findKeyLevels(data: testData) 45 | 46 | // Should find some levels 47 | XCTAssertFalse(levels.isEmpty, "Should find support/resistance levels") 48 | 49 | // Check level properties 50 | for level in levels { 51 | XCTAssertTrue(level.strength >= 0.0 && level.strength <= 1.0, 52 | "Level strength should be between 0 and 1") 53 | XCTAssertTrue(level.touches >= 2, "Levels should have at least 2 touches") 54 | } 55 | } 56 | 57 | func testDataProvider() async throws { 58 | let provider = CryptoDataProvider() 59 | 60 | // Test with a known cryptocurrency 61 | do { 62 | let priceData = try await provider.getCurrentPrice(symbol: "BTC") 63 | 64 | XCTAssertEqual(priceData.symbol, "BTC") 65 | XCTAssertTrue(priceData.price > 0, "Price should be positive") 66 | XCTAssertTrue(priceData.volume24h >= 0, "Volume should be non-negative") 67 | } catch { 68 | // This test might fail due to network issues or API limits 69 | // In a real test environment, you'd use mock data 70 | print("Network test failed (expected in some environments): \(error)") 71 | } 72 | } 73 | 74 | func testModelCreation() { 75 | let timestamp = Date() 76 | let candle = CandleData( 77 | timestamp: timestamp, 78 | open: 100.0, 79 | high: 110.0, 80 | low: 95.0, 81 | close: 105.0, 82 | volume: 1000.0 83 | ) 84 | 85 | XCTAssertEqual(candle.bodySize, 5.0, "Body size should be |close - open|") 86 | XCTAssertTrue(candle.isBullish, "Candle should be bullish when close > open") 87 | XCTAssertFalse(candle.isBearish, "Candle should not be bearish when close > open") 88 | XCTAssertEqual(candle.upperShadow, 5.0, "Upper shadow should be high - max(open, close)") 89 | XCTAssertEqual(candle.lowerShadow, 5.0, "Lower shadow should be min(open, close) - low") 90 | } 91 | 92 | func testTradingSignalGeneration() { 93 | let signal = TradingSignal.buy 94 | XCTAssertEqual(signal.numericValue, 1.0, "Buy signal should have numeric value of 1.0") 95 | 96 | let strongSell = TradingSignal.strongSell 97 | XCTAssertEqual(strongSell.numericValue, -2.0, "Strong sell should have numeric value of -2.0") 98 | } 99 | 100 | func testTimeframeConversion() { 101 | let daily = Timeframe.daily 102 | XCTAssertEqual(daily.minutes, 1440, "Daily timeframe should be 1440 minutes") 103 | 104 | let fourHour = Timeframe.fourHour 105 | XCTAssertEqual(fourHour.minutes, 240, "4-hour timeframe should be 240 minutes") 106 | } 107 | 108 | // MARK: - Helper Methods for Test Data 109 | 110 | private func createTestCandleData() -> [CandleData] { 111 | var candles: [CandleData] = [] 112 | let baseTime = Date().addingTimeInterval(-30 * 24 * 60 * 60) // 30 days ago 113 | 114 | for i in 0..<30 { 115 | let timestamp = baseTime.addingTimeInterval(Double(i) * 24 * 60 * 60) 116 | let basePrice = 100.0 + Double(i) // Ascending trend 117 | let volatility = Double.random(in: 0.95...1.05) 118 | 119 | let open = basePrice * volatility 120 | let close = basePrice * Double.random(in: 0.98...1.02) 121 | let high = max(open, close) * Double.random(in: 1.0...1.03) 122 | let low = min(open, close) * Double.random(in: 0.97...1.0) 123 | let volume = Double.random(in: 1000...10000) 124 | 125 | candles.append(CandleData( 126 | timestamp: timestamp, 127 | open: open, 128 | high: high, 129 | low: low, 130 | close: close, 131 | volume: volume 132 | )) 133 | } 134 | 135 | return candles 136 | } 137 | 138 | private func createHeadAndShouldersPattern() -> [CandleData] { 139 | var candles: [CandleData] = [] 140 | let baseTime = Date().addingTimeInterval(-20 * 24 * 60 * 60) 141 | 142 | // Create a head and shoulders pattern 143 | let prices: [Double] = [ 144 | 100, 105, 110, 108, 105, // Left shoulder 145 | 110, 115, 120, 118, 115, // Head formation 146 | 110, 108, 112, 110, 108, // Right shoulder 147 | 105, 102, 100, 98, 95 // Breakdown 148 | ] 149 | 150 | for (i, price) in prices.enumerated() { 151 | let timestamp = baseTime.addingTimeInterval(Double(i) * 24 * 60 * 60) 152 | let volatility = Double.random(in: 0.98...1.02) 153 | 154 | let close = price * volatility 155 | let open = close * Double.random(in: 0.99...1.01) 156 | let high = max(open, close) * Double.random(in: 1.0...1.02) 157 | let low = min(open, close) * Double.random(in: 0.98...1.0) 158 | let volume = Double.random(in: 1000...5000) 159 | 160 | candles.append(CandleData( 161 | timestamp: timestamp, 162 | open: open, 163 | high: high, 164 | low: low, 165 | close: close, 166 | volume: volume 167 | )) 168 | } 169 | 170 | return candles 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # CryptoAnalysisMCP Examples 📊 2 | 3 | This file contains practical examples of how to use the CryptoAnalysisMCP server. 4 | 5 | ## Quick Start 6 | 7 | ### 1. Start the MCP Server 8 | ```bash 9 | swift run CryptoAnalysisMCP 10 | ``` 11 | 12 | ### 2. Connect from Claude Desktop 13 | 14 | Add this to your Claude Desktop MCP configuration: 15 | 16 | ```json 17 | { 18 | "mcpServers": { 19 | "crypto-analysis": { 20 | "command": "/path/to/CryptoAnalysisMCP/.build/release/CryptoAnalysisMCP", 21 | "args": [], 22 | "env": {} 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | ## Example Queries for Claude 29 | 30 | ### Basic Price Check 31 | > "What's the current price of Bitcoin?" 32 | 33 | This will call `get_crypto_price` with symbol "BTC" and return current market data. 34 | 35 | ### Technical Analysis 36 | > "Analyze Bitcoin's technical indicators on the daily timeframe" 37 | 38 | This will call `get_technical_indicators` and return RSI, MACD, moving averages, etc. 39 | 40 | ### Pattern Recognition 41 | > "Are there any chart patterns forming on Ethereum's daily chart?" 42 | 43 | This will call `detect_chart_patterns` and identify formations like triangles, head & shoulders, etc. 44 | 45 | ### Trading Signals 46 | > "Give me a trading signal for Bitcoin with moderate risk tolerance" 47 | 48 | This will call `get_trading_signals` and provide entry/exit recommendations. 49 | 50 | ### Multi-Timeframe Analysis 51 | > "Analyze Ethereum across all timeframes and give me the overall picture" 52 | 53 | This will call `multi_timeframe_analysis` for comprehensive cross-timeframe view. 54 | 55 | ### Support & Resistance 56 | > "Where are the key support and resistance levels for Solana?" 57 | 58 | This will call `get_support_resistance` and identify critical price levels. 59 | 60 | ### Full Analysis 61 | > "Give me a complete technical analysis of Cardano including everything" 62 | 63 | This will call `get_full_analysis` for comprehensive report. 64 | 65 | ## Advanced Examples 66 | 67 | ### Specific Indicator Request 68 | > "What's Bitcoin's RSI and MACD on the 4-hour timeframe?" 69 | 70 | ### Pattern-Specific Search 71 | > "Look for any head and shoulders patterns on Ethereum's weekly chart" 72 | 73 | ### Risk-Adjusted Signals 74 | > "Give me conservative trading signals for Bitcoin - I want high confidence entries only" 75 | 76 | ### Multi-Crypto Comparison 77 | > "Compare the technical analysis of Bitcoin, Ethereum, and Cardano" 78 | 79 | ### Entry/Exit Strategy 80 | > "Where should I enter Bitcoin if I want to buy, and what should my stop loss be?" 81 | 82 | ## Real-World Scenarios 83 | 84 | ### Scenario 1: Day Trading Setup 85 | ``` 86 | User: "I'm looking for a day trading setup on Bitcoin. Show me 4-hour patterns and current momentum." 87 | 88 | Expected Response: 89 | - 4-hour chart patterns 90 | - RSI and MACD signals 91 | - Support/resistance levels 92 | - Entry recommendations with stops 93 | ``` 94 | 95 | ### Scenario 2: Swing Trading Analysis 96 | ``` 97 | User: "I'm thinking of holding Ethereum for a few weeks. What does the daily analysis look like?" 98 | 99 | Expected Response: 100 | - Daily timeframe indicators 101 | - Chart patterns with targets 102 | - Multi-week support levels 103 | - Risk-adjusted signals 104 | ``` 105 | 106 | ### Scenario 3: Long-term Investment 107 | ``` 108 | User: "Should I add to my Solana position? Show me the weekly technical picture." 109 | 110 | Expected Response: 111 | - Weekly trend analysis 112 | - Long-term support/resistance 113 | - Pattern formations 114 | - Investment-grade signals 115 | ``` 116 | 117 | ### Scenario 4: Risk Management 118 | ``` 119 | User: "Bitcoin is approaching a key level. Where should I place my stop loss?" 120 | 121 | Expected Response: 122 | - Current support levels 123 | - Stop loss recommendations 124 | - Risk/reward ratios 125 | - Level strength analysis 126 | ``` 127 | 128 | ### Scenario 5: Market Overview 129 | ``` 130 | User: "Give me a quick technical overview of the top 5 cryptocurrencies." 131 | 132 | Expected Response: 133 | - Multi-crypto analysis 134 | - Overall market sentiment 135 | - Key levels to watch 136 | - Relative strength comparison 137 | ``` 138 | 139 | ## Sample Output Formats 140 | 141 | ### Price Data Response 142 | ```json 143 | { 144 | "symbol": "BTC", 145 | "price": 109293.67, 146 | "change_24h": 3304.62, 147 | "change_percent_24h": 3.12, 148 | "volume_24h": 34888334155.28, 149 | "market_cap": 2172580030356, 150 | "rank": 1, 151 | "percent_changes": { 152 | "1h": 0.1, 153 | "6h": -0.38, 154 | "7d": 3.82, 155 | "30d": 4.58 156 | } 157 | } 158 | ``` 159 | 160 | ### Technical Indicators Response 161 | ```json 162 | { 163 | "symbol": "BTC", 164 | "timeframe": "daily", 165 | "indicators": { 166 | "RSI_14": { 167 | "value": 67.5, 168 | "signal": "HOLD", 169 | "parameters": {"period": 14} 170 | }, 171 | "MACD_12_26_9": { 172 | "value": 1250.45, 173 | "signal": "BUY", 174 | "parameters": {"histogram": 245.67} 175 | }, 176 | "SMA_20": { 177 | "value": 107890.23, 178 | "signal": "BUY" 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | ### Pattern Detection Response 185 | ```json 186 | { 187 | "symbol": "ETH", 188 | "patterns": [ 189 | { 190 | "type": "ASCENDING_TRIANGLE", 191 | "confidence": 0.85, 192 | "description": "Strong ascending triangle with 3 touches on resistance", 193 | "target_price": 2850.00, 194 | "stop_loss": 2450.00, 195 | "is_bullish": true 196 | } 197 | ] 198 | } 199 | ``` 200 | 201 | ### Trading Signals Response 202 | ```json 203 | { 204 | "symbol": "BTC", 205 | "primary_signal": "BUY", 206 | "confidence": 0.75, 207 | "entry_price": 109293.67, 208 | "stop_loss": 107500.00, 209 | "take_profit": 112000.00, 210 | "reasoning": "RSI shows oversold conditions. Chart patterns detected: ASCENDING_TRIANGLE. Price near key support level at 107500.00" 211 | } 212 | ``` 213 | 214 | ## Tips for Best Results 215 | 216 | ### 1. Be Specific 217 | - Specify the cryptocurrency symbol 218 | - Mention the timeframe you're interested in 219 | - State your risk tolerance if asking for signals 220 | 221 | ### 2. Use Context 222 | - Mention if you're day trading, swing trading, or investing 223 | - Provide your current position if relevant 224 | - Ask about specific price levels you're watching 225 | 226 | ### 3. Ask Follow-up Questions 227 | - "What if price breaks below that support level?" 228 | - "How does this compare to last week's analysis?" 229 | - "What are the key levels to watch for a breakout?" 230 | 231 | ### 4. Combine Analyses 232 | - Ask for multiple timeframes 233 | - Request both patterns and indicators 234 | - Get both technical and risk analysis 235 | 236 | ## Troubleshooting 237 | 238 | ### Common Issues 239 | 1. **"No data available"** - The cryptocurrency symbol might not be supported 240 | 2. **"Insufficient data"** - Not enough historical data for the requested timeframe 241 | 3. **"Rate limit exceeded"** - Too many requests; wait a moment and try again 242 | 243 | ### Supported Symbols 244 | Make sure to use standard symbols like: 245 | - BTC (Bitcoin) 246 | - ETH (Ethereum) 247 | - ADA (Cardano) 248 | - SOL (Solana) 249 | - DOT (Polkadot) 250 | - LINK (Chainlink) 251 | - MATIC (Polygon) 252 | - AVAX (Avalanche) 253 | 254 | ### Best Practices 255 | - Wait a few seconds between requests 256 | - Use clear, specific cryptocurrency names 257 | - Specify your timeframe preference 258 | - Ask for clarification if results are unclear 259 | 260 | --- 261 | 262 | **Happy Trading! 📈** 263 | 264 | *Remember: This is for educational purposes only. Always do your own research and manage risk appropriately.* 265 | -------------------------------------------------------------------------------- /Sources/CryptoAnalysisMCP/Models.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Core Data Models 4 | 5 | /// Represents OHLCV (Open, High, Low, Close, Volume) candle data 6 | struct CandleData { 7 | let timestamp: Date 8 | let open: Double 9 | let high: Double 10 | let low: Double 11 | let close: Double 12 | let volume: Double 13 | 14 | /// Returns the candle body size 15 | var bodySize: Double { 16 | abs(close - open) 17 | } 18 | 19 | /// Returns the upper shadow size 20 | var upperShadow: Double { 21 | high - max(open, close) 22 | } 23 | 24 | /// Returns the lower shadow size 25 | var lowerShadow: Double { 26 | min(open, close) - low 27 | } 28 | 29 | /// True if bullish candle (close > open) 30 | var isBullish: Bool { 31 | close > open 32 | } 33 | 34 | /// True if bearish candle (close < open) 35 | var isBearish: Bool { 36 | close < open 37 | } 38 | 39 | /// True if doji (very small body) 40 | var isDoji: Bool { 41 | bodySize <= (high - low) * 0.1 42 | } 43 | } 44 | 45 | /// Real-time price data with market information 46 | struct PriceData { 47 | let symbol: String 48 | let price: Double 49 | let change24h: Double 50 | let changePercent24h: Double 51 | let volume24h: Double 52 | let marketCap: Double? 53 | let timestamp: Date 54 | let rank: Int? 55 | 56 | // Additional market data from CoinPaprika 57 | let percentChange15m: Double? 58 | let percentChange30m: Double? 59 | let percentChange1h: Double? 60 | let percentChange6h: Double? 61 | let percentChange12h: Double? 62 | let percentChange7d: Double? 63 | let percentChange30d: Double? 64 | let percentChange1y: Double? 65 | let athPrice: Double? 66 | let athDate: Date? 67 | } 68 | 69 | /// Technical indicator results 70 | struct IndicatorResult { 71 | let name: String 72 | let value: Double 73 | let signal: TradingSignal 74 | let timestamp: Date 75 | let parameters: [String: Any] 76 | } 77 | 78 | /// Trading signal enumeration 79 | enum TradingSignal: String, CaseIterable { 80 | case strongBuy = "STRONG_BUY" 81 | case buy = "BUY" 82 | case hold = "HOLD" 83 | case sell = "SELL" 84 | case strongSell = "STRONG_SELL" 85 | 86 | var numericValue: Double { 87 | switch self { 88 | case .strongBuy: return 2.0 89 | case .buy: return 1.0 90 | case .hold: return 0.0 91 | case .sell: return -1.0 92 | case .strongSell: return -2.0 93 | } 94 | } 95 | } 96 | 97 | /// Chart pattern detection result 98 | struct ChartPattern { 99 | let type: PatternType 100 | let confidence: Double 101 | let startDate: Date 102 | let endDate: Date 103 | let keyPoints: [PatternPoint] 104 | let description: String 105 | let target: Double? 106 | let stopLoss: Double? 107 | } 108 | 109 | /// Pattern types that can be detected 110 | enum PatternType: String, CaseIterable { 111 | // Reversal Patterns 112 | case headAndShoulders = "HEAD_AND_SHOULDERS" 113 | case inverseHeadAndShoulders = "INVERSE_HEAD_AND_SHOULDERS" 114 | case doubleTop = "DOUBLE_TOP" 115 | case doubleBottom = "DOUBLE_BOTTOM" 116 | case tripleTop = "TRIPLE_TOP" 117 | case tripleBottom = "TRIPLE_BOTTOM" 118 | 119 | // Continuation Patterns 120 | case ascendingTriangle = "ASCENDING_TRIANGLE" 121 | case descendingTriangle = "DESCENDING_TRIANGLE" 122 | case symmetricalTriangle = "SYMMETRICAL_TRIANGLE" 123 | case flag = "FLAG" 124 | case pennant = "PENNANT" 125 | case rectangle = "RECTANGLE" 126 | case risingWedge = "RISING_WEDGE" 127 | case fallingWedge = "FALLING_WEDGE" 128 | 129 | // Candlestick Patterns 130 | case hammer = "HAMMER" 131 | case shootingStar = "SHOOTING_STAR" 132 | case doji = "DOJI" 133 | case engulfing = "ENGULFING" 134 | case harami = "HARAMI" 135 | case morningStar = "MORNING_STAR" 136 | case eveningStar = "EVENING_STAR" 137 | 138 | var isReversal: Bool { 139 | switch self { 140 | case .headAndShoulders, .inverseHeadAndShoulders, .doubleTop, .doubleBottom, 141 | .tripleTop, .tripleBottom, .hammer, .shootingStar, .doji, 142 | .engulfing, .harami, .morningStar, .eveningStar: 143 | return true 144 | default: 145 | return false 146 | } 147 | } 148 | 149 | var isBullish: Bool { 150 | switch self { 151 | case .inverseHeadAndShoulders, .doubleBottom, .tripleBottom, .hammer, 152 | .engulfing, .harami, .morningStar: 153 | return true 154 | case .headAndShoulders, .doubleTop, .tripleTop, .shootingStar, .eveningStar: 155 | return false 156 | default: 157 | return false // Neutral or depends on context 158 | } 159 | } 160 | } 161 | 162 | /// Key point in a chart pattern 163 | struct PatternPoint { 164 | let timestamp: Date 165 | let price: Double 166 | let type: PointType 167 | } 168 | 169 | enum PointType: String { 170 | case peak = "PEAK" 171 | case trough = "TROUGH" 172 | case breakout = "BREAKOUT" 173 | case support = "SUPPORT" 174 | case resistance = "RESISTANCE" 175 | } 176 | 177 | /// Support and resistance level 178 | struct SupportResistanceLevel { 179 | let price: Double 180 | let strength: Double // 0.0 to 1.0 181 | let type: LevelType 182 | let touches: Int 183 | let lastTouch: Date 184 | let isActive: Bool 185 | } 186 | 187 | enum LevelType: String { 188 | case support = "SUPPORT" 189 | case resistance = "RESISTANCE" 190 | case pivotPoint = "PIVOT_POINT" 191 | case fibonacciLevel = "FIBONACCI_LEVEL" 192 | } 193 | 194 | /// Timeframe enumeration 195 | enum Timeframe: String, CaseIterable { 196 | case fourHour = "4h" 197 | case daily = "1d" 198 | case weekly = "1w" 199 | case monthly = "1M" 200 | 201 | var minutes: Int { 202 | switch self { 203 | case .fourHour: return 240 204 | case .daily: return 1440 205 | case .weekly: return 10080 206 | case .monthly: return 43200 207 | } 208 | } 209 | 210 | var seconds: TimeInterval { 211 | return TimeInterval(minutes * 60) 212 | } 213 | } 214 | 215 | /// Multi-timeframe analysis result 216 | struct MultiTimeframeAnalysis { 217 | let symbol: String 218 | let timestamp: Date 219 | let timeframes: [Timeframe: TimeframeAnalysis] 220 | let overallSignal: TradingSignal 221 | let confidence: Double 222 | let recommendation: String 223 | } 224 | 225 | struct TimeframeAnalysis { 226 | let timeframe: Timeframe 227 | let trend: TrendDirection 228 | let indicators: [IndicatorResult] 229 | let patterns: [ChartPattern] 230 | let keyLevels: [SupportResistanceLevel] 231 | let signal: TradingSignal 232 | } 233 | 234 | enum TrendDirection: String { 235 | case strongUptrend = "STRONG_UPTREND" 236 | case uptrend = "UPTREND" 237 | case sideways = "SIDEWAYS" 238 | case downtrend = "DOWNTREND" 239 | case strongDowntrend = "STRONG_DOWNTREND" 240 | } 241 | 242 | /// Comprehensive analysis result 243 | struct AnalysisResult { 244 | let symbol: String 245 | let timestamp: Date 246 | let currentPrice: Double 247 | let indicators: [IndicatorResult] 248 | let patterns: [ChartPattern] 249 | let supportResistance: [SupportResistanceLevel] 250 | let signals: [TradingSignal] 251 | let overallSignal: TradingSignal 252 | let confidence: Double 253 | let summary: String 254 | let recommendations: [String] 255 | } 256 | 257 | /// Risk level for trading signals 258 | enum RiskLevel: String, CaseIterable { 259 | case conservative = "CONSERVATIVE" 260 | case moderate = "MODERATE" 261 | case aggressive = "AGGRESSIVE" 262 | 263 | var signalThreshold: Double { 264 | switch self { 265 | case .conservative: return 0.8 266 | case .moderate: return 0.6 267 | case .aggressive: return 0.4 268 | } 269 | } 270 | } 271 | 272 | /// API Error types 273 | enum CryptoAnalysisError: Error, LocalizedError { 274 | case invalidSymbol(String) 275 | case networkError(String) 276 | case dataParsingError(String) 277 | case insufficientData(String) 278 | case rateLimitExceeded 279 | case apiKeyMissing 280 | case unknown(String) 281 | 282 | var errorDescription: String? { 283 | switch self { 284 | case .invalidSymbol(let symbol): 285 | return "Invalid cryptocurrency symbol: \(symbol)" 286 | case .networkError(let message): 287 | return "Network error: \(message)" 288 | case .dataParsingError(let message): 289 | return "Data parsing error: \(message)" 290 | case .insufficientData(let message): 291 | return "Insufficient data: \(message)" 292 | case .rateLimitExceeded: 293 | return "API rate limit exceeded. Please try again later." 294 | case .apiKeyMissing: 295 | return "API key is missing or invalid" 296 | case .unknown(let message): 297 | return "Unknown error: \(message)" 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /PROMPTS.md: -------------------------------------------------------------------------------- 1 | # Crypto Analysis Prompts 2 | 3 | A comprehensive guide to getting the most out of CryptoAnalysisMCP with example prompts for different trading styles and analysis needs. 4 | 5 | ## 📚 Table of Contents 6 | - [Quick Start Prompts](#quick-start-prompts) 7 | - [Day Trading Prompts](#day-trading-prompts) 8 | - [Swing Trading Prompts](#swing-trading-prompts) 9 | - [Position/Long-term Trading](#positionlong-term-trading) 10 | - [Technical Indicator Analysis](#technical-indicator-analysis) 11 | - [Chart Pattern Recognition](#chart-pattern-recognition) 12 | - [Risk Management Prompts](#risk-management-prompts) 13 | - [Multi-Asset Analysis](#multi-asset-analysis) 14 | - [Market Conditions & Sentiment](#market-conditions--sentiment) 15 | - [Advanced Analysis Prompts](#advanced-analysis-prompts) 16 | 17 | ## Quick Start Prompts 18 | 19 | ### Basic Analysis 20 | ``` 21 | "Give me a quick technical analysis of BTC" 22 | "Is ETH bullish or bearish right now?" 23 | "What's the current trend for SOL?" 24 | "Should I buy MATIC today?" 25 | "Analyze ADA price action" 26 | ``` 27 | 28 | ### Price & Momentum 29 | ``` 30 | "Show me BTC price with key levels" 31 | "What's the momentum on ETH?" 32 | "Is DOGE oversold or overbought?" 33 | "Check if AVAX is gaining strength" 34 | "How's the volume on LINK today?" 35 | ``` 36 | 37 | ## Day Trading Prompts 38 | 39 | ### Intraday Setups 40 | ``` 41 | "Analyze BTC for day trading opportunities on the 4h timeframe" 42 | "Show me scalping levels for ETH today" 43 | "What are the intraday support and resistance levels for SOL?" 44 | "Find me the best crypto for day trading right now" 45 | "Give me 15-minute chart analysis for BTC" 46 | ``` 47 | 48 | ### Scalping Focus 49 | ``` 50 | "Identify quick scalp opportunities in BTC" 51 | "Show me the tightest range to trade on ETH" 52 | "What's the next micro support for SOL?" 53 | "Best entry for a quick MATIC scalp" 54 | "Monitor DOT for breakout on short timeframe" 55 | ``` 56 | 57 | ### High Probability Setups 58 | ``` 59 | "Find me oversold bounces in major cryptos" 60 | "Show me coins near strong support for quick trades" 61 | "Which crypto has the clearest intraday trend?" 62 | "Identify momentum breakouts happening now" 63 | "Best risk/reward setups for next 4 hours" 64 | ``` 65 | 66 | ## Swing Trading Prompts 67 | 68 | ### 3-7 Day Outlook 69 | ``` 70 | "Provide swing trading setup for ETH with 3-7 day outlook" 71 | "Analyze BTC patterns on daily timeframe for swing trades" 72 | "Give me entry, stop loss, and targets for swing trading SOL" 73 | "Best swing trade opportunities this week" 74 | "Show me cryptos setting up for multi-day moves" 75 | ``` 76 | 77 | ### Pattern-Based Swings 78 | ``` 79 | "Find me bull flag patterns in major cryptos" 80 | "Show ascending triangles ready to break" 81 | "Identify cup and handle formations" 82 | "Which cryptos are forming double bottoms?" 83 | "Best reversal patterns for swing trades" 84 | ``` 85 | 86 | ### Trend Following 87 | ``` 88 | "Show me cryptos in strong uptrends for swing trading" 89 | "Find pullbacks in trending coins" 90 | "Best retracement entries in bull markets" 91 | "Identify trend continuation setups" 92 | "Which coins are respecting their moving averages?" 93 | ``` 94 | 95 | ## Position/Long-term Trading 96 | 97 | ### Investment Analysis 98 | ``` 99 | "Do a complete investment analysis on BTC" 100 | "Long-term technical outlook for ETH" 101 | "Is this a good accumulation zone for SOL?" 102 | "Best cryptos for 3-month hold based on technicals" 103 | "Major support levels for long-term BTC positions" 104 | ``` 105 | 106 | ### Accumulation Strategies 107 | ``` 108 | "Where should I start accumulating BTC for long-term?" 109 | "Best levels to dollar-cost average into ETH" 110 | "Long-term support zones for building positions" 111 | "Technical levels for scaling into positions" 112 | "Monthly chart analysis for major cryptos" 113 | ``` 114 | 115 | ## Technical Indicator Analysis 116 | 117 | ### Momentum Indicators 118 | ``` 119 | "What's the RSI saying about BTC?" 120 | "Show me MACD signals on ETH" 121 | "Check Stochastic RSI on SOL" 122 | "Is the RSI diverging on ADA?" 123 | "Momentum oscillators analysis for DOT" 124 | ``` 125 | 126 | ### Moving Averages 127 | ``` 128 | "How's BTC performing vs its moving averages?" 129 | "Show me the 50/200 MA cross on ETH" 130 | "Which MAs are acting as support for SOL?" 131 | "Best MA strategy for current market" 132 | "EMA ribbon analysis on major cryptos" 133 | ``` 134 | 135 | ### Volatility Indicators 136 | ``` 137 | "Check Bollinger Bands squeeze on BTC" 138 | "Is ETH volatility expanding or contracting?" 139 | "ATR analysis for position sizing on SOL" 140 | "Show me coins with tightening ranges" 141 | "Volatility-based entry points" 142 | ``` 143 | 144 | ### Combined Indicators 145 | ``` 146 | "Give me RSI, MACD, and Bollinger Bands for BTC" 147 | "Full indicator dashboard for ETH" 148 | "Show all momentum indicators for SOL" 149 | "Technical indicator confluence on ADA" 150 | "Which indicators are most bullish on MATIC?" 151 | ``` 152 | 153 | ## Chart Pattern Recognition 154 | 155 | ### Classic Patterns 156 | ``` 157 | "Are there any chart patterns forming on BTC?" 158 | "Find head and shoulders patterns in crypto" 159 | "Show me triangle patterns ready to break" 160 | "Identify double tops or bottoms" 161 | "Best wedge patterns in major cryptos" 162 | ``` 163 | 164 | ### Breakout Patterns 165 | ``` 166 | "Show me cryptos about to breakout" 167 | "Find ascending triangles near resistance" 168 | "Which patterns have the highest breakout probability?" 169 | "Identify bull flags in uptrends" 170 | "Best continuation patterns right now" 171 | ``` 172 | 173 | ### Reversal Patterns 174 | ``` 175 | "Find potential reversal patterns in oversold cryptos" 176 | "Show me bottoming patterns" 177 | "Identify trend reversal setups" 178 | "Best inverse head and shoulders patterns" 179 | "Major reversal signals on daily charts" 180 | ``` 181 | 182 | ## Risk Management Prompts 183 | 184 | ### Position Sizing 185 | ``` 186 | "Calculate position size for BTC trade with 2% risk" 187 | "What's the safe position size given ATR?" 188 | "Risk-adjusted position sizing for volatile cryptos" 189 | "How much SOL can I buy with $10k and 3% risk?" 190 | "Optimal position sizes based on volatility" 191 | ``` 192 | 193 | ### Stop Loss Placement 194 | ``` 195 | "Where should I place my stop loss for BTC long?" 196 | "Calculate stop loss based on ATR" 197 | "Best stop loss for swing trading ETH" 198 | "Trailing stop strategy for trending markets" 199 | "Dynamic stop loss levels based on support" 200 | ``` 201 | 202 | ### Risk/Reward Analysis 203 | ``` 204 | "Show me trades with 1:3 risk/reward or better" 205 | "Calculate risk/reward for BTC at current levels" 206 | "Best risk/reward setups available now" 207 | "Find low-risk high-reward opportunities" 208 | "Optimal entry points for best risk/reward" 209 | ``` 210 | 211 | ## Multi-Asset Analysis 212 | 213 | ### Correlation Analysis 214 | ``` 215 | "Compare BTC and ETH technical setups" 216 | "Which altcoins are diverging from BTC?" 217 | "Show me non-correlated crypto opportunities" 218 | "Best diversification based on technicals" 219 | "Analyze sector rotation in crypto" 220 | ``` 221 | 222 | ### Relative Strength 223 | ``` 224 | "Which crypto is showing the most strength?" 225 | "Rank top 10 cryptos by technical score" 226 | "Show me outperformers on the daily" 227 | "Best relative strength plays" 228 | "Weakest cryptos to avoid or short" 229 | ``` 230 | 231 | ### Portfolio Analysis 232 | ``` 233 | "Analyze my portfolio: BTC, ETH, SOL, ADA" 234 | "Technical health check on multiple positions" 235 | "Rebalancing suggestions based on technicals" 236 | "Which of my holdings should I trim or add?" 237 | "Portfolio risk analysis using technicals" 238 | ``` 239 | 240 | ## Market Conditions & Sentiment 241 | 242 | ### Broad Market Analysis 243 | ``` 244 | "What's the overall crypto market sentiment?" 245 | "Are we in risk-on or risk-off mode?" 246 | "Technical breadth analysis of crypto market" 247 | "Major market support and resistance levels" 248 | "Is this a good time to be in crypto?" 249 | ``` 250 | 251 | ### Trend Analysis 252 | ``` 253 | "What's the major trend in crypto right now?" 254 | "Are we in accumulation or distribution?" 255 | "Show me the market structure on higher timeframes" 256 | "Key levels that would change the trend" 257 | "Bull market vs bear market indicators" 258 | ``` 259 | 260 | ## Advanced Analysis Prompts 261 | 262 | ### Institutional-Grade Analysis 263 | ``` 264 | "Do a complete Wall Street analyst report on BTC" 265 | "Analyze ETH like a hedge fund would" 266 | "Give me all technical indicators, patterns, and signals for SOL" 267 | "Professional trading desk analysis on ADA" 268 | "Institutional entry and exit points for DOT" 269 | ``` 270 | 271 | ### Multi-Timeframe Confluence 272 | ``` 273 | "Show me multi-timeframe analysis on BTC" 274 | "Where do all timeframes align for ETH?" 275 | "Confluence zones across all timeframes" 276 | "Best entries confirmed by multiple timeframes" 277 | "Top-down analysis from monthly to 4h" 278 | ``` 279 | 280 | ### Complex Strategies 281 | ``` 282 | "Design a grid trading strategy for ranging BTC" 283 | "Mean reversion opportunities in crypto" 284 | "Pairs trading setup for BTC/ETH" 285 | "Advanced options strategies based on technicals" 286 | "Algorithmic entry signals for systematic trading" 287 | ``` 288 | 289 | ### Custom Analysis 290 | ``` 291 | "Analyze BTC but focus only on volume patterns" 292 | "Show me Fibonacci levels for major retracements" 293 | "Elliott Wave count on ETH" 294 | "Harmonic patterns in crypto markets" 295 | "Custom indicator combination for my strategy" 296 | ``` 297 | 298 | ## Strategy-Specific Prompts 299 | 300 | ### Conservative Approach 301 | ``` 302 | "Show me conservative trading strategy for BTC" 303 | "Safest entry points with minimal risk" 304 | "Low-risk accumulation zones" 305 | "Conservative portfolio suggestions" 306 | "Capital preservation focused analysis" 307 | ``` 308 | 309 | ### Moderate Risk 310 | ``` 311 | "Balanced risk/reward opportunities in crypto" 312 | "Standard trading setups with clear stops" 313 | "Moderate risk swing trades" 314 | "Portfolio with balanced risk profile" 315 | "Best trends with reasonable volatility" 316 | ``` 317 | 318 | ### Aggressive Trading 319 | ``` 320 | "What's the aggressive play on SOL?" 321 | "High-risk high-reward setups" 322 | "Leveraged trading opportunities" 323 | "Aggressive accumulation points" 324 | "Maximum gain potential trades" 325 | ``` 326 | 327 | ## Educational Prompts 328 | 329 | ### Learning Technical Analysis 330 | ``` 331 | "Explain what the RSI is telling us about BTC" 332 | "Teach me how to read MACD signals" 333 | "What does this chart pattern mean?" 334 | "How do I identify support and resistance?" 335 | "Explain the current market structure" 336 | ``` 337 | 338 | ### Strategy Development 339 | ``` 340 | "Help me build a trading strategy for crypto" 341 | "What indicators work best together?" 342 | "How do I combine multiple timeframes?" 343 | "Best practices for crypto technical analysis" 344 | "Common mistakes to avoid in crypto trading" 345 | ``` 346 | 347 | ## Quick Decision Prompts 348 | 349 | ### Yes/No Decisions 350 | ``` 351 | "Should I buy BTC right now? Quick answer" 352 | "Is it time to sell my ETH?" 353 | "Hold or sell SOL?" 354 | "Good time to enter crypto market?" 355 | "Wait or buy the dip?" 356 | ``` 357 | 358 | ### Rapid Fire Analysis 359 | ``` 360 | "BTC in 10 seconds" 361 | "Quick take on ETH" 362 | "One-line analysis of SOL" 363 | "Bullish or bearish? That's all" 364 | "Price target for ADA, nothing else" 365 | ``` 366 | 367 | ## Custom Combinations 368 | 369 | Feel free to combine different elements: 370 | ``` 371 | "Show me conservative swing trade setups for BTC with RSI below 40 and near moving average support" 372 | 373 | "Find aggressive day trading opportunities in top 10 cryptos with increasing volume and bullish MACD crossovers" 374 | 375 | "Analyze ETH for institutional accumulation using volume profile, smart money indicators, and major support levels" 376 | 377 | "Multi-timeframe confluence scan for cryptos showing bull flags on daily, oversold on 4h, and above 200 MA" 378 | ``` 379 | 380 | --- 381 | 382 | ## 💡 Pro Tips 383 | 384 | 1. **Be Specific**: The more specific your request, the more targeted the analysis 385 | 2. **Mention Timeframe**: Always specify if you want 4h, daily, or weekly analysis 386 | 3. **State Your Style**: Mention if you're day trading, swing trading, or investing 387 | 4. **Risk Preference**: Include your risk tolerance for personalized strategies 388 | 5. **Multiple Assets**: Feel free to ask for comparative analysis 389 | 390 | ## 🎯 Remember 391 | 392 | - All analysis is based on technical indicators only 393 | - Always do your own research 394 | - Use proper risk management 395 | - Past performance doesn't guarantee future results 396 | - Crypto markets are highly volatile 397 | 398 | Happy Trading! 🚀 399 | -------------------------------------------------------------------------------- /Sources/CryptoAnalysisMCP/DexPaprikaDataProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | 4 | /// Provides cryptocurrency data from DexPaprika API - 7+ million tokens! 5 | /// NO API KEY REQUIRED - Completely free access 6 | actor DexPaprikaDataProvider { 7 | internal let logger = Logger(label: "DexPaprikaDataProvider") 8 | 9 | // DexPaprika API configuration 10 | internal let dexPaprikaBaseURL = "https://api.dexpaprika.com" 11 | 12 | // Cache for performance 13 | private var tokenCache: [String: (data: DexPaprikaToken, timestamp: Date)] = [:] 14 | private var networkCache: [String: [DexPaprikaNetwork]] = [:] 15 | private let cacheTimeout: TimeInterval = 60 // 1 minute for price data 16 | 17 | // Network name to ID mapping for common networks 18 | internal let networkMapping: [String: String] = [ 19 | "ethereum": "ethereum", 20 | "eth": "ethereum", 21 | "binance": "bsc", 22 | "binance-smart-chain": "bsc", 23 | "bsc": "bsc", 24 | "polygon": "polygon", 25 | "matic": "polygon", 26 | "arbitrum": "arbitrum", 27 | "arb": "arbitrum", 28 | "optimism": "optimism", 29 | "op": "optimism", 30 | "avalanche": "avalanche", 31 | "avalanche-c-chain": "avalanche", 32 | "avax": "avalanche", 33 | "fantom": "fantom", 34 | "ftm": "fantom", 35 | "solana": "solana", 36 | "sol": "solana", 37 | "base": "base", 38 | "zksync": "zksync", 39 | "zksync-era": "zksync", 40 | "linea": "linea", 41 | "mantle": "mantle", 42 | "blast": "blast" 43 | ] 44 | 45 | /// Get available networks 46 | func getNetworks() async throws -> [DexPaprikaNetwork] { 47 | // Check cache first 48 | if let cached = networkCache["all"], !cached.isEmpty { 49 | return cached 50 | } 51 | 52 | let urlString = "\(dexPaprikaBaseURL)/networks" 53 | guard let url = URL(string: urlString) else { 54 | throw CryptoAnalysisError.networkError("Invalid URL") 55 | } 56 | 57 | logger.info("Fetching available networks from DexPaprika") 58 | 59 | do { 60 | let (data, response) = try await URLSession.shared.data(from: url) 61 | 62 | guard let httpResponse = response as? HTTPURLResponse, 63 | (200...299).contains(httpResponse.statusCode) else { 64 | throw CryptoAnalysisError.networkError("Invalid response") 65 | } 66 | 67 | // Debug: Log raw response 68 | if let jsonString = String(data: data, encoding: .utf8) { 69 | logger.info("Raw networks response: \(jsonString.prefix(200))...") 70 | } 71 | 72 | let networks = try JSONDecoder().decode([DexPaprikaNetwork].self, from: data) 73 | 74 | // Cache the result 75 | networkCache["all"] = networks 76 | 77 | logger.info("Found \(networks.count) networks on DexPaprika") 78 | return networks 79 | 80 | } catch { 81 | logger.error("Failed to fetch networks: \(error)") 82 | throw error 83 | } 84 | } 85 | 86 | /// Search for tokens across all networks 87 | func searchToken(query: String, limit: Int = 10) async throws -> [DexPaprikaSearchResult] { 88 | let urlString = "\(dexPaprikaBaseURL)/search?query=\(query)&limit=\(limit)" 89 | guard let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") else { 90 | throw CryptoAnalysisError.networkError("Invalid URL") 91 | } 92 | 93 | logger.info("Searching for '\(query)' on DexPaprika") 94 | 95 | do { 96 | let (data, response) = try await URLSession.shared.data(from: url) 97 | 98 | guard let httpResponse = response as? HTTPURLResponse, 99 | (200...299).contains(httpResponse.statusCode) else { 100 | throw CryptoAnalysisError.networkError("Invalid response") 101 | } 102 | 103 | let searchResponse = try JSONDecoder().decode(DexPaprikaSearchResponse.self, from: data) 104 | 105 | logger.info("Found \(searchResponse.tokens.count) tokens matching '\(query)'") 106 | return searchResponse.tokens 107 | 108 | } catch { 109 | logger.error("Search failed: \(error)") 110 | throw error 111 | } 112 | } 113 | 114 | /// Get token data by network and address 115 | func getToken(network: String, address: String) async throws -> DexPaprikaToken { 116 | let cacheKey = "\(network)_\(address)" 117 | 118 | // Check cache first 119 | if let cached = tokenCache[cacheKey], 120 | Date().timeIntervalSince(cached.timestamp) < cacheTimeout { 121 | logger.info("Returning cached data for \(cacheKey)") 122 | return cached.data 123 | } 124 | 125 | // Resolve network name to ID if needed 126 | let networkId = networkMapping[network.lowercased()] ?? network.lowercased() 127 | 128 | let urlString = "\(dexPaprikaBaseURL)/networks/\(networkId)/tokens/\(address)" 129 | guard let url = URL(string: urlString) else { 130 | throw CryptoAnalysisError.networkError("Invalid URL") 131 | } 132 | 133 | logger.info("Fetching token data from DexPaprika: \(networkId)/\(address)") 134 | 135 | do { 136 | let (data, response) = try await URLSession.shared.data(from: url) 137 | 138 | guard let httpResponse = response as? HTTPURLResponse else { 139 | throw CryptoAnalysisError.networkError("Invalid response") 140 | } 141 | 142 | if httpResponse.statusCode == 404 { 143 | throw CryptoAnalysisError.invalidSymbol("Token not found on DexPaprika: \(address) on \(networkId)") 144 | } 145 | 146 | guard (200...299).contains(httpResponse.statusCode) else { 147 | throw CryptoAnalysisError.networkError("HTTP Error \(httpResponse.statusCode)") 148 | } 149 | 150 | let token = try JSONDecoder().decode(DexPaprikaToken.self, from: data) 151 | 152 | // Cache the result 153 | tokenCache[cacheKey] = (token, Date()) 154 | 155 | logger.info("Retrieved token: \(token.name) (\(token.symbol))") 156 | return token 157 | 158 | } catch { 159 | logger.error("Failed to fetch token data: \(error)") 160 | throw error 161 | } 162 | } 163 | 164 | /// Convert DexPaprika token data to standard PriceData format 165 | func convertToPriceData(token: DexPaprikaToken) -> PriceData { 166 | let summary = token.summary 167 | return PriceData( 168 | symbol: token.symbol.uppercased(), 169 | price: summary.priceUsd, 170 | change24h: summary.priceUsd * (summary.priceChange24h / 100), 171 | changePercent24h: summary.priceChange24h, 172 | volume24h: summary.volumeUsd24h, 173 | marketCap: summary.liquidityUsd, 174 | timestamp: Date(), 175 | rank: 0, // DexPaprika doesn't provide rank 176 | // Additional time-based changes not available from DexPaprika 177 | percentChange15m: nil, 178 | percentChange30m: nil, 179 | percentChange1h: nil, 180 | percentChange6h: nil, 181 | percentChange12h: nil, 182 | percentChange7d: nil, 183 | percentChange30d: nil, 184 | percentChange1y: nil, 185 | athPrice: nil, 186 | athDate: nil 187 | ) 188 | } 189 | 190 | /// Get token by symbol (searches across all networks) 191 | func getTokenBySymbol(_ symbol: String) async throws -> DexPaprikaToken { 192 | // First, search for the token 193 | let searchResults = try await searchToken(query: symbol, limit: 20) 194 | 195 | // Find exact match or best match 196 | let upperSymbol = symbol.uppercased() 197 | 198 | if let exactMatch = searchResults.first(where: { $0.symbol.uppercased() == upperSymbol }) { 199 | // Get the full token data 200 | return try await getToken(network: exactMatch.networkId, address: exactMatch.address) 201 | } else if let firstResult = searchResults.first { 202 | logger.info("No exact match for \(upperSymbol), using first result: \(firstResult.name)") 203 | return try await getToken(network: firstResult.networkId, address: firstResult.address) 204 | } else { 205 | throw CryptoAnalysisError.invalidSymbol("\(upperSymbol) - Not found on any DEX") 206 | } 207 | } 208 | 209 | /// Clear all caches 210 | func clearCache() async { 211 | tokenCache.removeAll() 212 | networkCache.removeAll() 213 | logger.info("Cleared all DexPaprika caches") 214 | } 215 | } 216 | 217 | // MARK: - DexPaprika Response Models 218 | 219 | struct DexPaprikaNetwork: Codable { 220 | let id: String 221 | let displayName: String 222 | 223 | private enum CodingKeys: String, CodingKey { 224 | case id 225 | case displayName = "display_name" 226 | } 227 | 228 | // Computed properties for compatibility 229 | var name: String { displayName } 230 | var shortName: String { id } 231 | } 232 | 233 | struct DexPaprikaSearchResponse: Codable { 234 | let tokens: [DexPaprikaSearchResult] 235 | } 236 | 237 | struct DexPaprikaSearchResult: Codable { 238 | let id: String // This is the address 239 | let name: String 240 | let symbol: String 241 | let chain: String // This is the network ID 242 | let priceUsd: Double? 243 | let liquidityUsd: Double? 244 | let volumeUsd: Double? 245 | 246 | private enum CodingKeys: String, CodingKey { 247 | case id, name, symbol, chain 248 | case priceUsd = "price_usd" 249 | case liquidityUsd = "liquidity_usd" 250 | case volumeUsd = "volume_usd" 251 | } 252 | 253 | // Computed properties for compatibility with existing code 254 | var address: String { id } 255 | var networkId: String { chain } 256 | var networkName: String { chain } 257 | var logoUrl: String? { nil } 258 | } 259 | 260 | struct DexPaprikaToken: Codable { 261 | let id: String 262 | let name: String 263 | let symbol: String 264 | let chain: String 265 | let decimals: Int 266 | let totalSupply: Double? 267 | let summary: DexPaprikaSummary 268 | 269 | private enum CodingKeys: String, CodingKey { 270 | case id, name, symbol, chain, decimals, summary 271 | case totalSupply = "total_supply" 272 | } 273 | 274 | // Computed properties for compatibility 275 | var address: String { id } 276 | var logoUrl: String? { nil } 277 | var network: DexPaprikaNetworkInfo { 278 | DexPaprikaNetworkInfo(id: chain, name: chain, shortName: nil) 279 | } 280 | } 281 | 282 | struct DexPaprikaSummary: Codable { 283 | let priceUsd: Double 284 | let liquidityUsd: Double 285 | let pools: Int? 286 | let twentyFourHour: DexPaprikaTimeData? 287 | 288 | private enum CodingKeys: String, CodingKey { 289 | case pools 290 | case priceUsd = "price_usd" 291 | case liquidityUsd = "liquidity_usd" 292 | case twentyFourHour = "24h" 293 | } 294 | 295 | // Computed properties for compatibility 296 | var priceChange24h: Double { 297 | twentyFourHour?.lastPriceUsdChange ?? 0 298 | } 299 | var volumeUsd24h: Double { 300 | twentyFourHour?.volumeUsd ?? 0 301 | } 302 | var totalSupply: Double? { nil } 303 | var circulatingSupply: Double? { nil } 304 | var holders: Int? { nil } 305 | } 306 | 307 | struct DexPaprikaTimeData: Codable { 308 | let volumeUsd: Double 309 | let lastPriceUsdChange: Double 310 | 311 | private enum CodingKeys: String, CodingKey { 312 | case volumeUsd = "volume_usd" 313 | case lastPriceUsdChange = "last_price_usd_change" 314 | } 315 | } 316 | 317 | struct DexPaprikaNetworkInfo: Codable { 318 | let id: String 319 | let name: String 320 | let shortName: String? 321 | 322 | private enum CodingKeys: String, CodingKey { 323 | case id 324 | case name 325 | case shortName = "short_name" 326 | } 327 | 328 | // Computed property for compatibility 329 | var displayName: String { name } 330 | } 331 | -------------------------------------------------------------------------------- /Sources/CryptoAnalysisMCP/AnalysisFormatters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Analysis Formatters Extension 4 | 5 | extension Date { 6 | /// Format date for display 7 | var displayString: String { 8 | let formatter = DateFormatter() 9 | formatter.dateStyle = .medium 10 | formatter.timeStyle = .short 11 | return formatter.string(from: self) 12 | } 13 | } 14 | 15 | // MARK: - Formatting Functions 16 | 17 | /// Parse timeframe string to enum 18 | func parseTimeframe(_ timeframeString: String?) -> Timeframe { 19 | guard let timeframe = timeframeString else { return .daily } 20 | 21 | switch timeframe.lowercased() { 22 | case "4h", "4hour", "4-hour": 23 | return .fourHour 24 | case "daily", "1d", "day": 25 | return .daily 26 | case "weekly", "1w", "week": 27 | return .weekly 28 | case "monthly", "1m", "month": 29 | return .monthly 30 | default: 31 | return .daily 32 | } 33 | } 34 | 35 | /// Parse risk level string to enum 36 | func parseRiskLevel(_ riskString: String?) -> RiskLevel { 37 | guard let risk = riskString else { return .moderate } 38 | 39 | switch risk.lowercased() { 40 | case "conservative", "low": 41 | return .conservative 42 | case "moderate", "medium", "mid": 43 | return .moderate 44 | case "aggressive", "high": 45 | return .aggressive 46 | default: 47 | return .moderate 48 | } 49 | } 50 | 51 | /// Format price data for JSON response 52 | func formatPriceData(_ priceData: PriceData) -> [String: Any] { 53 | var result: [String: Any] = [ 54 | "symbol": priceData.symbol, 55 | "price": priceData.price, 56 | "change_24h": priceData.change24h, 57 | "change_percent_24h": priceData.changePercent24h, 58 | "volume_24h": priceData.volume24h, 59 | "timestamp": ISO8601DateFormatter().string(from: priceData.timestamp) 60 | ] 61 | 62 | if let marketCap = priceData.marketCap { 63 | result["market_cap"] = marketCap 64 | } 65 | 66 | if let rank = priceData.rank { 67 | result["rank"] = rank 68 | } 69 | 70 | // Add additional timeframe changes 71 | var changes: [String: Any] = [:] 72 | if let change15m = priceData.percentChange15m { changes["15m"] = change15m } 73 | if let change30m = priceData.percentChange30m { changes["30m"] = change30m } 74 | if let change1h = priceData.percentChange1h { changes["1h"] = change1h } 75 | if let change6h = priceData.percentChange6h { changes["6h"] = change6h } 76 | if let change12h = priceData.percentChange12h { changes["12h"] = change12h } 77 | if let change7d = priceData.percentChange7d { changes["7d"] = change7d } 78 | if let change30d = priceData.percentChange30d { changes["30d"] = change30d } 79 | if let change1y = priceData.percentChange1y { changes["1y"] = change1y } 80 | 81 | if !changes.isEmpty { 82 | result["percent_changes"] = changes 83 | } 84 | 85 | // Add ATH information 86 | if let athPrice = priceData.athPrice { 87 | result["ath_price"] = athPrice 88 | if let athDate = priceData.athDate { 89 | result["ath_date"] = ISO8601DateFormatter().string(from: athDate) 90 | } 91 | } 92 | 93 | return result 94 | } 95 | 96 | /// Format chart pattern for JSON response 97 | func formatPattern(_ pattern: ChartPattern) -> [String: Any] { 98 | var result: [String: Any] = [ 99 | "type": pattern.type.rawValue, 100 | "confidence": pattern.confidence, 101 | "start_date": ISO8601DateFormatter().string(from: pattern.startDate), 102 | "end_date": ISO8601DateFormatter().string(from: pattern.endDate), 103 | "description": pattern.description, 104 | "is_reversal": pattern.type.isReversal, 105 | "is_bullish": pattern.type.isBullish 106 | ] 107 | 108 | if let target = pattern.target { 109 | result["target_price"] = target 110 | } 111 | 112 | if let stopLoss = pattern.stopLoss { 113 | result["stop_loss"] = stopLoss 114 | } 115 | 116 | result["key_points"] = pattern.keyPoints.map { point in 117 | [ 118 | "timestamp": ISO8601DateFormatter().string(from: point.timestamp), 119 | "price": point.price, 120 | "type": point.type.rawValue 121 | ] 122 | } 123 | 124 | return result 125 | } 126 | 127 | /// Format support/resistance level for JSON response 128 | func formatSupportResistance(_ level: SupportResistanceLevel) -> [String: Any] { 129 | return [ 130 | "price": level.price, 131 | "strength": level.strength, 132 | "type": level.type.rawValue, 133 | "touches": level.touches, 134 | "last_touch": ISO8601DateFormatter().string(from: level.lastTouch), 135 | "is_active": level.isActive 136 | ] 137 | } 138 | 139 | /// Format analysis result for JSON response 140 | func formatAnalysisResult(_ result: AnalysisResult) -> [String: Any] { 141 | return [ 142 | "symbol": result.symbol, 143 | "timestamp": ISO8601DateFormatter().string(from: result.timestamp), 144 | "current_price": result.currentPrice, 145 | "overall_signal": result.overallSignal.rawValue, 146 | "confidence": result.confidence, 147 | "summary": result.summary, 148 | "recommendations": result.recommendations, 149 | "indicators": [ 150 | "latest_values": getLatestIndicatorValues(result.indicators), 151 | "signals": result.indicators.map { [ 152 | "name": $0.name, 153 | "value": $0.value, 154 | "signal": $0.signal.rawValue, 155 | "timestamp": ISO8601DateFormatter().string(from: $0.timestamp) 156 | ]} 157 | ], 158 | "patterns": [ 159 | "count": result.patterns.count, 160 | "detected": result.patterns.map(formatPattern) 161 | ], 162 | "support_resistance": [ 163 | "support_levels": result.supportResistance.filter { $0.type == .support }.map(formatSupportResistance), 164 | "resistance_levels": result.supportResistance.filter { $0.type == .resistance }.map(formatSupportResistance), 165 | "key_levels_count": result.supportResistance.count 166 | ] 167 | ] 168 | } 169 | 170 | /// Get latest values for each indicator type 171 | func getLatestIndicatorValues(_ indicators: [IndicatorResult]) -> [String: Any] { 172 | var latest: [String: Any] = [:] 173 | 174 | // Group indicators by name and get the latest value for each 175 | let groupedIndicators = Dictionary(grouping: indicators) { $0.name } 176 | 177 | for (name, indicatorGroup) in groupedIndicators { 178 | if let latestIndicator = indicatorGroup.max(by: { $0.timestamp < $1.timestamp }) { 179 | latest[name] = [ 180 | "value": latestIndicator.value, 181 | "signal": latestIndicator.signal.rawValue, 182 | "parameters": latestIndicator.parameters 183 | ] 184 | } 185 | } 186 | 187 | return latest 188 | } 189 | 190 | /// Find nearest price level 191 | func findNearestLevel(_ levels: [SupportResistanceLevel], to price: Double) -> Double? { 192 | return levels.min { abs($0.price - price) < abs($1.price - price) }?.price 193 | } 194 | 195 | /// Calculate stop loss based on signal and levels 196 | func calculateStopLoss(signal: TradingSignal, price: Double, levels: [SupportResistanceLevel]) -> Double? { 197 | switch signal { 198 | case .buy, .strongBuy: 199 | // Stop loss below nearest support 200 | if let support = findNearestLevel(levels.filter { $0.type == .support && $0.price < price }, to: price) { 201 | return support * 0.98 // 2% below support 202 | } 203 | return price * 0.95 // Default 5% stop loss 204 | case .sell, .strongSell: 205 | // Stop loss above nearest resistance 206 | if let resistance = findNearestLevel(levels.filter { $0.type == .resistance && $0.price > price }, to: price) { 207 | return resistance * 1.02 // 2% above resistance 208 | } 209 | return price * 1.05 // Default 5% stop loss 210 | default: 211 | return nil 212 | } 213 | } 214 | 215 | /// Calculate take profit based on signal and levels 216 | func calculateTakeProfit(signal: TradingSignal, price: Double, levels: [SupportResistanceLevel]) -> Double? { 217 | switch signal { 218 | case .buy, .strongBuy: 219 | // Take profit at nearest resistance 220 | if let resistance = findNearestLevel(levels.filter { $0.type == .resistance && $0.price > price }, to: price) { 221 | return resistance * 0.98 // Just below resistance 222 | } 223 | return price * 1.10 // Default 10% profit target 224 | case .sell, .strongSell: 225 | // Take profit at nearest support 226 | if let support = findNearestLevel(levels.filter { $0.type == .support && $0.price < price }, to: price) { 227 | return support * 1.02 // Just above support 228 | } 229 | return price * 0.90 // Default 10% profit target 230 | default: 231 | return nil 232 | } 233 | } 234 | 235 | /// Generate trading signal reasoning 236 | func generateSignalReasoning( 237 | signal: TradingSignal, 238 | indicators: [IndicatorResult], 239 | patterns: [ChartPattern], 240 | levels: [SupportResistanceLevel], 241 | currentPrice: Double 242 | ) -> String { 243 | var reasons: [String] = [] 244 | 245 | // Indicator reasoning 246 | let latestIndicators = getLatestIndicatorValues(indicators) 247 | if let rsi = latestIndicators["RSI_14"] as? [String: Any], 248 | let rsiValue = rsi["value"] as? Double { 249 | if rsiValue > 70 { 250 | reasons.append("RSI shows overbought conditions (\(String(format: "%.1f", rsiValue)))") 251 | } else if rsiValue < 30 { 252 | reasons.append("RSI shows oversold conditions (\(String(format: "%.1f", rsiValue)))") 253 | } 254 | } 255 | 256 | // Pattern reasoning 257 | if !patterns.isEmpty { 258 | let patternTypes = patterns.map { $0.type.rawValue }.joined(separator: ", ") 259 | reasons.append("Chart patterns detected: \(patternTypes)") 260 | } 261 | 262 | // Level reasoning 263 | let nearestSupport = findNearestLevel(levels.filter { $0.type == .support }, to: currentPrice) 264 | let nearestResistance = findNearestLevel(levels.filter { $0.type == .resistance }, to: currentPrice) 265 | 266 | if let support = nearestSupport { 267 | let distance = abs(currentPrice - support) / currentPrice 268 | if distance < 0.03 { 269 | reasons.append("Price near key support level at \(String(format: "%.2f", support))") 270 | } 271 | } 272 | 273 | if let resistance = nearestResistance { 274 | let distance = abs(currentPrice - resistance) / currentPrice 275 | if distance < 0.03 { 276 | reasons.append("Price near key resistance level at \(String(format: "%.2f", resistance))") 277 | } 278 | } 279 | 280 | return reasons.isEmpty ? "Analysis based on overall technical indicators" : reasons.joined(separator: ". ") 281 | } 282 | 283 | /// Determine overall trend from indicators 284 | func determineTrend(from indicators: [IndicatorResult]) -> String { 285 | let latest = getLatestIndicatorValues(indicators) 286 | 287 | var bullishCount = 0 288 | var bearishCount = 0 289 | 290 | // Check EMA alignment 291 | if let ema20 = latest["EMA_20"] as? [String: Any], 292 | let ema50 = latest["EMA_50"] as? [String: Any], 293 | let ema20Value = ema20["value"] as? Double, 294 | let ema50Value = ema50["value"] as? Double { 295 | if ema20Value > ema50Value { 296 | bullishCount += 1 297 | } else { 298 | bearishCount += 1 299 | } 300 | } 301 | 302 | // Check MACD 303 | if let macd = latest["MACD_12_26_9"] as? [String: Any], 304 | let signal = macd["signal"] as? String { 305 | if signal == "BUY" { 306 | bullishCount += 1 307 | } else if signal == "SELL" { 308 | bearishCount += 1 309 | } 310 | } 311 | 312 | if bullishCount > bearishCount { 313 | return "UPTREND" 314 | } else if bearishCount > bullishCount { 315 | return "DOWNTREND" 316 | } else { 317 | return "SIDEWAYS" 318 | } 319 | } 320 | 321 | /// Generate comprehensive summary 322 | func generateComprehensiveSummary( 323 | symbol: String, 324 | price: PriceData, 325 | indicators: [IndicatorResult], 326 | patterns: [ChartPattern], 327 | levels: [SupportResistanceLevel], 328 | signal: TradingSignal, 329 | confidence: Double 330 | ) -> String { 331 | 332 | let trend = determineTrend(from: indicators) 333 | let patternCount = patterns.count 334 | let levelCount = levels.count 335 | 336 | return """ 337 | \(symbol) is currently trading at $\(String(format: "%.2f", price.price)) with a \(trend.lowercased()) bias. 338 | Technical analysis shows \(signal.rawValue.lowercased()) signal with \(String(format: "%.1f", confidence * 100))% confidence. 339 | \(patternCount) chart patterns detected and \(levelCount) key support/resistance levels identified. 340 | 24h change: \(String(format: "%.2f", price.changePercent24h))%. 341 | """ 342 | } 343 | 344 | /// Generate recommendations based on analysis 345 | func generateRecommendations( 346 | indicators: [IndicatorResult], 347 | patterns: [ChartPattern], 348 | levels: [SupportResistanceLevel], 349 | riskLevel: RiskLevel 350 | ) -> [String] { 351 | 352 | var recommendations: [String] = [] 353 | 354 | // Risk-based recommendations 355 | switch riskLevel { 356 | case .conservative: 357 | recommendations.append("Wait for strong confirmation signals before entering positions") 358 | recommendations.append("Use tight stop losses and smaller position sizes") 359 | case .moderate: 360 | recommendations.append("Consider entering positions on pullbacks to support levels") 361 | recommendations.append("Use standard risk management with 2% position risk") 362 | case .aggressive: 363 | recommendations.append("Can take positions on early signals with larger size") 364 | recommendations.append("Use wider stops to avoid getting stopped out by volatility") 365 | } 366 | 367 | // Pattern-based recommendations 368 | if !patterns.isEmpty { 369 | let highConfidencePatterns = patterns.filter { $0.confidence > 0.7 } 370 | if !highConfidencePatterns.isEmpty { 371 | recommendations.append("Strong chart patterns suggest potential price movement") 372 | } 373 | } 374 | 375 | // Level-based recommendations 376 | let strongLevels = levels.filter { $0.strength > 0.7 } 377 | if !strongLevels.isEmpty { 378 | recommendations.append("Monitor key support/resistance levels for breakout opportunities") 379 | } 380 | 381 | recommendations.append("Always use proper risk management and position sizing") 382 | 383 | return recommendations 384 | } 385 | -------------------------------------------------------------------------------- /Sources/CryptoAnalysisMCP/CryptoDataProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | 4 | /// Provides cryptocurrency data from various sources 5 | actor CryptoDataProvider { 6 | private let logger = Logger(label: "CryptoDataProvider") 7 | 8 | // DexPaprika provider for 7+ million tokens! 9 | let dexPaprikaProvider = DexPaprikaDataProvider() 10 | 11 | // CoinPaprika API configuration 12 | private let coinPaprikaBaseURL = "https://api.coinpaprika.com/v1" 13 | 14 | // API Key - Set this if you have a paid CoinPaprika subscription 15 | // You can also set the COINPAPRIKA_API_KEY environment variable 16 | private let apiKey: String? = ProcessInfo.processInfo.environment["COINPAPRIKA_API_KEY"] 17 | 18 | // Cache for performance 19 | private var priceCache: [String: (data: PriceData, timestamp: Date)] = [:] 20 | private var historicalCache: [String: (data: [CandleData], timestamp: Date)] = [:] 21 | private let cacheTimeout: TimeInterval = 60 // 1 minute for price data 22 | private let historicalCacheTimeout: TimeInterval = 300 // 5 minutes for historical data 23 | 24 | // Symbol mapping for CoinPaprika - commonly used coins for performance 25 | private let symbolMapping: [String: String] = [ 26 | "BTC": "btc-bitcoin", 27 | "ETH": "eth-ethereum", 28 | "ADA": "ada-cardano", 29 | "DOT": "dot-polkadot", 30 | "LINK": "link-chainlink", 31 | "SOL": "sol-solana", 32 | "MATIC": "matic-polygon", 33 | "AVAX": "avax-avalanche", 34 | "ATOM": "atom-cosmos", 35 | "XRP": "xrp-xrp", 36 | "BNB": "bnb-binance-coin", 37 | "DOGE": "doge-dogecoin", 38 | "SHIB": "shib-shiba-inu", 39 | "UNI": "uni-uniswap", 40 | "LTC": "ltc-litecoin", 41 | "ALGO": "algo-algorand", 42 | "VET": "vet-vechain", 43 | "FTM": "ftm-fantom", 44 | "NEAR": "near-near-protocol", 45 | "AAVE": "aave-aave", 46 | "RNDR": "rndr-render-token", 47 | "RENDER": "rndr-render-token", 48 | "SUI": "sui-sui", 49 | "APT": "apt-aptos", 50 | "ARB": "arb-arbitrum", 51 | "OP": "op-optimism" 52 | ] 53 | 54 | // Cache for dynamically resolved coin IDs 55 | private var coinIdCache: [String: String] = [:] 56 | 57 | /// Resolve a symbol to a CoinPaprika coin ID 58 | private func resolveCoinId(for symbol: String) async throws -> String { 59 | let upperSymbol = symbol.uppercased() 60 | 61 | // Check static mapping first 62 | if let mappedId = symbolMapping[upperSymbol] { 63 | return mappedId 64 | } 65 | 66 | // Check dynamic cache 67 | if let cachedId = coinIdCache[upperSymbol] { 68 | return cachedId 69 | } 70 | 71 | // Search for the coin dynamically 72 | logger.info("Symbol \(upperSymbol) not in mapping, searching dynamically...") 73 | let searchResults = try await searchCrypto(query: upperSymbol) 74 | 75 | // Find exact match or first result with matching symbol 76 | let coinId: String 77 | if let exactMatch = searchResults.first(where: { $0.symbol.uppercased() == upperSymbol }) { 78 | coinId = exactMatch.id 79 | logger.info("Found exact match: \(exactMatch.name) (\(coinId))") 80 | } else if let firstResult = searchResults.first { 81 | // Use first result if no exact match 82 | coinId = firstResult.id 83 | logger.info("Using first search result: \(firstResult.name) (\(coinId))") 84 | } else { 85 | // If CoinPaprika search fails, try DexPaprika 86 | logger.info("No results on CoinPaprika for \(upperSymbol), trying DexPaprika...") 87 | 88 | do { 89 | let dexResults = try await dexPaprikaProvider.searchToken(query: upperSymbol, limit: 5) 90 | if let firstDexResult = dexResults.first { 91 | logger.info("Found on DexPaprika: \(firstDexResult.name) (\(firstDexResult.symbol))") 92 | // Return a special marker that indicates we should use DexPaprika 93 | throw CryptoAnalysisError.networkError("USE_DEXPAPRIKA") 94 | } 95 | } catch { 96 | // Ignore DexPaprika errors here 97 | } 98 | 99 | throw CryptoAnalysisError.invalidSymbol("\(upperSymbol) - Not found on CoinPaprika or any DEX") 100 | } 101 | 102 | // Cache the result 103 | coinIdCache[upperSymbol] = coinId 104 | 105 | return coinId 106 | } 107 | 108 | /// Get current price data for a cryptocurrency 109 | func getCurrentPrice(symbol: String) async throws -> PriceData { 110 | let upperSymbol = symbol.uppercased() 111 | 112 | // Check cache first 113 | if let cached = priceCache[upperSymbol], 114 | Date().timeIntervalSince(cached.timestamp) < cacheTimeout { 115 | logger.info("Returning cached price for \(upperSymbol)") 116 | return cached.data 117 | } 118 | 119 | // Try CoinPaprika first 120 | do { 121 | // Get CoinPaprika ID dynamically 122 | let coinId = try await resolveCoinId(for: upperSymbol) 123 | 124 | // Fetch from CoinPaprika ticker endpoint 125 | let urlString = "\(coinPaprikaBaseURL)/tickers/\(coinId)" 126 | guard let url = URL(string: urlString) else { 127 | throw CryptoAnalysisError.networkError("Invalid URL") 128 | } 129 | 130 | logger.info("Fetching price data for \(upperSymbol) from CoinPaprika") 131 | 132 | do { 133 | let (data, response) = try await URLSession.shared.data(from: url) 134 | 135 | guard let httpResponse = response as? HTTPURLResponse else { 136 | throw CryptoAnalysisError.networkError("Invalid response") 137 | } 138 | 139 | // Check for payment required error 140 | if httpResponse.statusCode == 402 { 141 | throw CryptoAnalysisError.networkError("This endpoint requires a paid CoinPaprika API subscription. Free tier includes 1 year of daily historical data - try using 'daily' timeframe.") 142 | } 143 | 144 | guard (200...299).contains(httpResponse.statusCode) else { 145 | throw CryptoAnalysisError.networkError("HTTP Error \(httpResponse.statusCode)") 146 | } 147 | 148 | let decoder = JSONDecoder() 149 | decoder.dateDecodingStrategy = .iso8601 150 | 151 | let tickerResponse = try decoder.decode(CoinPaprikaTickerResponse.self, from: data) 152 | 153 | let priceData = PriceData( 154 | symbol: upperSymbol, 155 | price: tickerResponse.quotes.USD.price, 156 | change24h: tickerResponse.quotes.USD.price - (tickerResponse.quotes.USD.price / (1 + tickerResponse.quotes.USD.percentChange24h / 100)), 157 | changePercent24h: tickerResponse.quotes.USD.percentChange24h, 158 | volume24h: tickerResponse.quotes.USD.volume24h, 159 | marketCap: tickerResponse.quotes.USD.marketCap, 160 | timestamp: Date(), 161 | rank: tickerResponse.rank, 162 | percentChange15m: tickerResponse.quotes.USD.percentChange15m, 163 | percentChange30m: tickerResponse.quotes.USD.percentChange30m, 164 | percentChange1h: tickerResponse.quotes.USD.percentChange1h, 165 | percentChange6h: tickerResponse.quotes.USD.percentChange6h, 166 | percentChange12h: tickerResponse.quotes.USD.percentChange12h, 167 | percentChange7d: tickerResponse.quotes.USD.percentChange7d, 168 | percentChange30d: tickerResponse.quotes.USD.percentChange30d, 169 | percentChange1y: tickerResponse.quotes.USD.percentChange1y, 170 | athPrice: tickerResponse.quotes.USD.athPrice, 171 | athDate: tickerResponse.quotes.USD.athDate != nil ? ISO8601DateFormatter().date(from: tickerResponse.quotes.USD.athDate!) : nil 172 | ) 173 | 174 | // Cache the result 175 | priceCache[upperSymbol] = (priceData, Date()) 176 | 177 | return priceData 178 | 179 | } catch { 180 | logger.error("Failed to fetch price data: \(error)") 181 | throw CryptoAnalysisError.networkError(error.localizedDescription) 182 | } 183 | } catch { 184 | // If CoinPaprika fails, try DexPaprika 185 | logger.info("CoinPaprika failed for \(upperSymbol), trying DexPaprika...") 186 | 187 | do { 188 | let dexToken = try await dexPaprikaProvider.getTokenBySymbol(upperSymbol) 189 | let priceData = await dexPaprikaProvider.convertToPriceData(token: dexToken) 190 | 191 | // Cache the result 192 | priceCache[upperSymbol] = (priceData, Date()) 193 | 194 | logger.info("Successfully retrieved \(upperSymbol) from DexPaprika!") 195 | return priceData 196 | } catch { 197 | logger.error("Both CoinPaprika and DexPaprika failed for \(upperSymbol)") 198 | throw CryptoAnalysisError.invalidSymbol("\(upperSymbol) - Not found on CoinPaprika or any DEX") 199 | } 200 | } 201 | } 202 | 203 | /// Get historical OHLCV data 204 | func getHistoricalData(symbol: String, timeframe: Timeframe, periods: Int = 100) async throws -> [CandleData] { 205 | let upperSymbol = symbol.uppercased() 206 | let cacheKey = "\(upperSymbol)_\(timeframe.rawValue)_\(periods)" 207 | 208 | // Check cache first 209 | if let cached = historicalCache[cacheKey], 210 | Date().timeIntervalSince(cached.timestamp) < historicalCacheTimeout { 211 | logger.info("Returning cached historical data for \(upperSymbol)") 212 | return cached.data 213 | } 214 | 215 | // Get CoinPaprika ID dynamically 216 | let coinId = try await resolveCoinId(for: upperSymbol) 217 | 218 | // Calculate date range 219 | let endDate = Date() 220 | let startDate: Date 221 | 222 | switch timeframe { 223 | case .fourHour: 224 | startDate = endDate.addingTimeInterval(-Double(periods) * 4 * 60 * 60) 225 | case .daily: 226 | startDate = endDate.addingTimeInterval(-Double(periods) * 24 * 60 * 60) 227 | case .weekly: 228 | startDate = endDate.addingTimeInterval(-Double(periods) * 7 * 24 * 60 * 60) 229 | case .monthly: 230 | startDate = endDate.addingTimeInterval(-Double(periods) * 30 * 24 * 60 * 60) 231 | } 232 | 233 | // Format dates 234 | let dateFormatter = DateFormatter() 235 | dateFormatter.dateFormat = "yyyy-MM-dd" 236 | 237 | let startDateString = dateFormatter.string(from: startDate) 238 | let endDateString = dateFormatter.string(from: endDate) 239 | 240 | // Construct URL for OHLCV data 241 | let interval: String 242 | switch timeframe { 243 | case .fourHour: 244 | interval = "4h" 245 | case .daily: 246 | interval = "1d" 247 | case .weekly: 248 | interval = "7d" 249 | case .monthly: 250 | interval = "30d" 251 | } 252 | 253 | var urlString = "\(coinPaprikaBaseURL)/coins/\(coinId)/ohlcv/historical?start=\(startDateString)&end=\(endDateString)&interval=\(interval)" 254 | 255 | // Add API key if available 256 | if let apiKey = apiKey { 257 | urlString += "&apikey=\(apiKey)" 258 | } 259 | 260 | guard let url = URL(string: urlString) else { 261 | throw CryptoAnalysisError.networkError("Invalid URL") 262 | } 263 | 264 | logger.info("Fetching historical data for \(upperSymbol) from \(startDateString) to \(endDateString)") 265 | 266 | do { 267 | let (data, response) = try await URLSession.shared.data(from: url) 268 | 269 | guard let httpResponse = response as? HTTPURLResponse else { 270 | throw CryptoAnalysisError.networkError("Invalid response") 271 | } 272 | 273 | // Check for payment required error 274 | if httpResponse.statusCode == 402 { 275 | throw CryptoAnalysisError.networkError("This endpoint requires a paid CoinPaprika API subscription. Free tier includes 1 year of daily historical data - try using 'daily' timeframe.") 276 | } 277 | 278 | guard (200...299).contains(httpResponse.statusCode) else { 279 | throw CryptoAnalysisError.networkError("HTTP Error \(httpResponse.statusCode)") 280 | } 281 | 282 | let decoder = JSONDecoder() 283 | decoder.dateDecodingStrategy = .iso8601 284 | 285 | let ohlcvResponse = try decoder.decode([CoinPaprikaOHLCVResponse].self, from: data) 286 | 287 | // Convert to CandleData 288 | let candles = ohlcvResponse.map { ohlcv in 289 | CandleData( 290 | timestamp: ISO8601DateFormatter().date(from: ohlcv.timeOpen) ?? Date(), 291 | open: ohlcv.open, 292 | high: ohlcv.high, 293 | low: ohlcv.low, 294 | close: ohlcv.close, 295 | volume: ohlcv.volume 296 | ) 297 | }.sorted { $0.timestamp < $1.timestamp } 298 | 299 | // Cache the result 300 | historicalCache[cacheKey] = (candles, Date()) 301 | 302 | logger.info("Retrieved \(candles.count) candles for \(upperSymbol)") 303 | 304 | return candles 305 | 306 | } catch { 307 | logger.error("Failed to fetch historical data: \(error)") 308 | throw error 309 | } 310 | } 311 | 312 | 313 | } 314 | 315 | // MARK: - CoinPaprika Response Models 316 | 317 | private struct CoinPaprikaTickerResponse: Codable { 318 | let id: String 319 | let name: String 320 | let symbol: String 321 | let rank: Int 322 | let quotes: CoinPaprikaQuotes 323 | } 324 | 325 | private struct CoinPaprikaQuotes: Codable { 326 | let USD: CoinPaprikaQuoteData 327 | } 328 | 329 | private struct CoinPaprikaQuoteData: Codable { 330 | let price: Double 331 | let volume24h: Double 332 | let percentChange24h: Double 333 | let percentChange7d: Double 334 | let percentChange30d: Double 335 | let percentChange1y: Double 336 | let marketCap: Double 337 | let athPrice: Double? 338 | let athDate: String? 339 | let percentChange15m: Double? 340 | let percentChange30m: Double? 341 | let percentChange1h: Double? 342 | let percentChange6h: Double? 343 | let percentChange12h: Double? 344 | 345 | private enum CodingKeys: String, CodingKey { 346 | case price 347 | case volume24h = "volume_24h" 348 | case percentChange24h = "percent_change_24h" 349 | case percentChange7d = "percent_change_7d" 350 | case percentChange30d = "percent_change_30d" 351 | case percentChange1y = "percent_change_1y" 352 | case marketCap = "market_cap" 353 | case athPrice = "ath_price" 354 | case athDate = "ath_date" 355 | case percentChange15m = "percent_change_15m" 356 | case percentChange30m = "percent_change_30m" 357 | case percentChange1h = "percent_change_1h" 358 | case percentChange6h = "percent_change_6h" 359 | case percentChange12h = "percent_change_12h" 360 | } 361 | } 362 | 363 | private struct CoinPaprikaOHLCVResponse: Codable { 364 | let timeOpen: String 365 | let timeClose: String 366 | let open: Double 367 | let high: Double 368 | let low: Double 369 | let close: Double 370 | let volume: Double 371 | let marketCap: Double 372 | 373 | private enum CodingKeys: String, CodingKey { 374 | case timeOpen = "time_open" 375 | case timeClose = "time_close" 376 | case open 377 | case high 378 | case low 379 | case close 380 | case volume 381 | case marketCap = "market_cap" 382 | } 383 | } 384 | 385 | // MARK: - Additional Data Methods 386 | extension CryptoDataProvider { 387 | 388 | /// Get list of available cryptocurrencies 389 | func getAvailableSymbols() async -> [String] { 390 | return Array(symbolMapping.keys).sorted() 391 | } 392 | 393 | /// Search for cryptocurrency by name or symbol 394 | func searchCrypto(query: String) async throws -> [(symbol: String, name: String, id: String)] { 395 | let urlString = "\(coinPaprikaBaseURL)/search?q=\(query)&limit=10" 396 | guard let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") else { 397 | throw CryptoAnalysisError.networkError("Invalid URL") 398 | } 399 | 400 | do { 401 | let (data, response) = try await URLSession.shared.data(from: url) 402 | 403 | guard let httpResponse = response as? HTTPURLResponse, 404 | (200...299).contains(httpResponse.statusCode) else { 405 | throw CryptoAnalysisError.networkError("Invalid response") 406 | } 407 | 408 | let searchResponse = try JSONDecoder().decode(CoinPaprikaSearchResponse.self, from: data) 409 | 410 | return searchResponse.currencies.map { currency in 411 | (symbol: currency.symbol, name: currency.name, id: currency.id) 412 | } 413 | } catch { 414 | logger.error("Search failed: \(error)") 415 | throw CryptoAnalysisError.networkError(error.localizedDescription) 416 | } 417 | } 418 | 419 | /// Clear all caches 420 | func clearCache() async { 421 | priceCache.removeAll() 422 | historicalCache.removeAll() 423 | logger.info("Cleared all caches") 424 | } 425 | } 426 | 427 | private struct CoinPaprikaSearchResponse: Codable { 428 | let currencies: [CoinPaprikaSearchResult] 429 | } 430 | 431 | private struct CoinPaprikaSearchResult: Codable { 432 | let id: String 433 | let name: String 434 | let symbol: String 435 | let rank: Int 436 | } 437 | -------------------------------------------------------------------------------- /Sources/CryptoAnalysisMCP/TechnicalAnalyzer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | 4 | /// Performs technical analysis calculations on cryptocurrency data 5 | actor TechnicalAnalyzer { 6 | private let logger = Logger(label: "TechnicalAnalyzer") 7 | 8 | // MARK: - Moving Averages 9 | 10 | /// Calculate Simple Moving Average 11 | func calculateSMA(data: [CandleData], period: Int) -> [IndicatorResult] { 12 | guard data.count >= period else { return [] } 13 | 14 | var results: [IndicatorResult] = [] 15 | 16 | for i in (period - 1).. period ? results.last?.value : nil) 22 | 23 | results.append(IndicatorResult( 24 | name: "SMA_\(period)", 25 | value: sma, 26 | signal: signal, 27 | timestamp: data[i].timestamp, 28 | parameters: ["period": period] 29 | )) 30 | } 31 | 32 | return results 33 | } 34 | 35 | /// Calculate Exponential Moving Average 36 | func calculateEMA(data: [CandleData], period: Int) -> [IndicatorResult] { 37 | guard data.count >= period else { return [] } 38 | 39 | let multiplier = 2.0 / (Double(period) + 1.0) 40 | var results: [IndicatorResult] = [] 41 | 42 | // Start with SMA for the first value 43 | let firstSMA = Array(data[0.. [IndicatorResult] { 71 | guard data.count > period else { return [] } 72 | 73 | var results: [IndicatorResult] = [] 74 | var gains: [Double] = [] 75 | var losses: [Double] = [] 76 | 77 | // Calculate price changes 78 | for i in 1.. [IndicatorResult] { 111 | guard data.count >= kPeriod else { return [] } 112 | 113 | var kValues: [Double] = [] 114 | var results: [IndicatorResult] = [] 115 | 116 | // Calculate %K values 117 | for i in (kPeriod - 1).. [IndicatorResult] { 148 | guard data.count >= slowPeriod else { return [] } 149 | 150 | let fastEMA = calculateEMA(data: data, period: fastPeriod) 151 | let slowEMA = calculateEMA(data: data, period: slowPeriod) 152 | 153 | guard fastEMA.count >= signalPeriod && slowEMA.count >= signalPeriod else { return [] } 154 | 155 | var macdLine: [Double] = [] 156 | var results: [IndicatorResult] = [] 157 | 158 | // Calculate MACD line (fast EMA - slow EMA) 159 | let startIndex = slowPeriod - fastPeriod 160 | for i in 0..= signalPeriod else { return [] } 167 | 168 | let multiplier = 2.0 / (Double(signalPeriod) + 1.0) 169 | var signalLine = macdLine[0.. signalPeriod - 1 { 173 | signalLine = (macdLine[i] * multiplier) + (signalLine * (1 - multiplier)) 174 | } 175 | 176 | let histogram = macdLine[i] - signalLine 177 | let signal = determineMACDSignal(macd: macdLine[i], signal: signalLine, histogram: histogram) 178 | 179 | results.append(IndicatorResult( 180 | name: "MACD_\(fastPeriod)_\(slowPeriod)_\(signalPeriod)", 181 | value: macdLine[i], 182 | signal: signal, 183 | timestamp: data[i + slowPeriod].timestamp, 184 | parameters: [ 185 | "fastPeriod": fastPeriod, 186 | "slowPeriod": slowPeriod, 187 | "signalPeriod": signalPeriod, 188 | "signalLine": signalLine, 189 | "histogram": histogram 190 | ] 191 | )) 192 | } 193 | 194 | return results 195 | } 196 | 197 | // MARK: - Bollinger Bands 198 | 199 | /// Calculate Bollinger Bands 200 | func calculateBollingerBands(data: [CandleData], period: Int = 20, standardDeviations: Double = 2.0) -> [IndicatorResult] { 201 | guard data.count >= period else { return [] } 202 | 203 | var results: [IndicatorResult] = [] 204 | 205 | for i in (period - 1).. [IndicatorResult] { 246 | guard data.count >= period else { return [] } 247 | 248 | var results: [IndicatorResult] = [] 249 | 250 | for i in (period - 1).. [IndicatorResult] { 279 | guard data.count > 1 else { return [] } 280 | 281 | var results: [IndicatorResult] = [] 282 | var obv: Double = 0 283 | 284 | for i in 1.. data[i-1].close { 286 | obv += data[i].volume 287 | } else if data[i].close < data[i-1].close { 288 | obv -= data[i].volume 289 | } 290 | // If equal, OBV remains the same 291 | 292 | let signal = determineVolumeSignal(obv: obv, previousOBV: results.last?.value) 293 | 294 | results.append(IndicatorResult( 295 | name: "OBV", 296 | value: obv, 297 | signal: signal, 298 | timestamp: data[i].timestamp, 299 | parameters: ["volume": data[i].volume, "priceChange": data[i].close - data[i-1].close] 300 | )) 301 | } 302 | 303 | return results 304 | } 305 | } 306 | 307 | // MARK: - Signal Determination Methods 308 | extension TechnicalAnalyzer { 309 | 310 | private func determineTrendSignal(currentPrice: Double, ma: Double, previousMA: Double?) -> TradingSignal { 311 | let priceAboveMA = currentPrice > ma 312 | 313 | if let prevMA = previousMA { 314 | let maRising = ma > prevMA 315 | 316 | if priceAboveMA && maRising { 317 | return .buy 318 | } else if !priceAboveMA && !maRising { 319 | return .sell 320 | } 321 | } 322 | 323 | return priceAboveMA ? .hold : .hold 324 | } 325 | 326 | private func determineRSISignal(rsi: Double) -> TradingSignal { 327 | if rsi >= 70 { 328 | return .sell // Overbought 329 | } else if rsi <= 30 { 330 | return .buy // Oversold 331 | } else if rsi >= 60 { 332 | return .hold // Approaching overbought 333 | } else if rsi <= 40 { 334 | return .hold // Approaching oversold 335 | } 336 | return .hold 337 | } 338 | 339 | private func determineStochasticSignal(k: Double, d: Double) -> TradingSignal { 340 | if k >= 80 && d >= 80 { 341 | return .sell // Overbought 342 | } else if k <= 20 && d <= 20 { 343 | return .buy // Oversold 344 | } else if k > d && k < 80 { 345 | return .buy // Bullish crossover 346 | } else if k < d && k > 20 { 347 | return .sell // Bearish crossover 348 | } 349 | return .hold 350 | } 351 | 352 | private func determineMACDSignal(macd: Double, signal: Double, histogram: Double) -> TradingSignal { 353 | if macd > signal && histogram > 0 { 354 | return .buy // Bullish 355 | } else if macd < signal && histogram < 0 { 356 | return .sell // Bearish 357 | } 358 | return .hold 359 | } 360 | 361 | private func determineBollingerSignal(price: Double, upper: Double, middle: Double, lower: Double) -> TradingSignal { 362 | let percentB = (price - lower) / (upper - lower) 363 | 364 | if percentB >= 1.0 { 365 | return .sell // Price above upper band (overbought) 366 | } else if percentB <= 0.0 { 367 | return .buy // Price below lower band (oversold) 368 | } else if percentB > 0.8 { 369 | return .hold // Approaching upper band 370 | } else if percentB < 0.2 { 371 | return .hold // Approaching lower band 372 | } 373 | return .hold 374 | } 375 | 376 | private func determineWilliamsRSignal(williamsR: Double) -> TradingSignal { 377 | if williamsR >= -20 { 378 | return .sell // Overbought 379 | } else if williamsR <= -80 { 380 | return .buy // Oversold 381 | } 382 | return .hold 383 | } 384 | 385 | private func determineVolumeSignal(obv: Double, previousOBV: Double?) -> TradingSignal { 386 | guard let prevOBV = previousOBV else { return .hold } 387 | 388 | if obv > prevOBV { 389 | return .buy // Volume supporting upward movement 390 | } else if obv < prevOBV { 391 | return .sell // Volume supporting downward movement 392 | } 393 | return .hold 394 | } 395 | } 396 | 397 | // MARK: - Comprehensive Analysis 398 | extension TechnicalAnalyzer { 399 | 400 | /// Calculate all major technical indicators for a dataset 401 | func calculateAllIndicators(data: [CandleData]) async -> [IndicatorResult] { 402 | guard !data.isEmpty else { return [] } 403 | 404 | var allIndicators: [IndicatorResult] = [] 405 | 406 | // Moving Averages 407 | allIndicators.append(contentsOf: calculateSMA(data: data, period: 5)) 408 | allIndicators.append(contentsOf: calculateSMA(data: data, period: 10)) 409 | allIndicators.append(contentsOf: calculateSMA(data: data, period: 20)) 410 | allIndicators.append(contentsOf: calculateSMA(data: data, period: 50)) 411 | allIndicators.append(contentsOf: calculateSMA(data: data, period: 200)) 412 | 413 | allIndicators.append(contentsOf: calculateEMA(data: data, period: 5)) 414 | allIndicators.append(contentsOf: calculateEMA(data: data, period: 10)) 415 | allIndicators.append(contentsOf: calculateEMA(data: data, period: 20)) 416 | allIndicators.append(contentsOf: calculateEMA(data: data, period: 50)) 417 | 418 | // Oscillators 419 | allIndicators.append(contentsOf: calculateRSI(data: data, period: 14)) 420 | allIndicators.append(contentsOf: calculateRSI(data: data, period: 21)) 421 | allIndicators.append(contentsOf: calculateStochastic(data: data, kPeriod: 14, dPeriod: 3)) 422 | allIndicators.append(contentsOf: calculateWilliamsR(data: data, period: 14)) 423 | 424 | // MACD 425 | allIndicators.append(contentsOf: calculateMACD(data: data, fastPeriod: 12, slowPeriod: 26, signalPeriod: 9)) 426 | 427 | // Bollinger Bands 428 | allIndicators.append(contentsOf: calculateBollingerBands(data: data, period: 20, standardDeviations: 2.0)) 429 | 430 | // Volume 431 | allIndicators.append(contentsOf: calculateOBV(data: data)) 432 | 433 | logger.info("Calculated \(allIndicators.count) technical indicators") 434 | 435 | return allIndicators 436 | } 437 | 438 | /// Generate an overall trading signal based on multiple indicators 439 | func generateOverallSignal(indicators: [IndicatorResult]) -> (signal: TradingSignal, confidence: Double) { 440 | guard !indicators.isEmpty else { return (.hold, 0.0) } 441 | 442 | let signalCounts = indicators.reduce(into: [TradingSignal: Double]()) { counts, indicator in 443 | counts[indicator.signal, default: 0] += 1 444 | } 445 | 446 | let totalCount = Double(indicators.count) 447 | var weightedScore: Double = 0 448 | 449 | for (signal, count) in signalCounts { 450 | let weight = count / totalCount 451 | weightedScore += signal.numericValue * weight 452 | } 453 | 454 | let confidence = abs(weightedScore) / 2.0 // Normalize to 0-1 scale 455 | 456 | let finalSignal: TradingSignal 457 | if weightedScore >= 0.5 { 458 | finalSignal = .buy 459 | } else if weightedScore <= -0.5 { 460 | finalSignal = .sell 461 | } else { 462 | finalSignal = .hold 463 | } 464 | 465 | return (finalSignal, min(confidence, 1.0)) 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /Sources/CryptoAnalysisMCP/DexPaprikaAdvanced.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | /// Advanced DexPaprika features for v1.2 8 | extension DexPaprikaDataProvider { 9 | 10 | // MARK: - Pool Operations 11 | 12 | /// Get top pools across all networks 13 | /// NOTE: This endpoint has been deprecated by DexPaprika. Use getNetworkPools instead. 14 | func getTopPools(limit: Int = 20, orderBy: String = "volume_usd", sort: String = "desc") async throws -> [DexPaprikaPool] { 15 | // This endpoint is deprecated 16 | throw CryptoAnalysisError.networkError("The global /pools endpoint has been deprecated. Please use getNetworkPools with a specific network instead.") 17 | } 18 | 19 | /// Get pools on a specific network 20 | func getNetworkPools(network: String, limit: Int = 20, orderBy: String = "volume_usd") async throws -> [DexPaprikaPool] { 21 | let networkId = networkMapping[network.lowercased()] ?? network.lowercased() 22 | let urlString = "\(dexPaprikaBaseURL)/networks/\(networkId)/pools?limit=\(limit)&orderBy=\(orderBy)&sort=desc" 23 | 24 | guard let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? "") else { 25 | throw CryptoAnalysisError.networkError("Invalid URL") 26 | } 27 | 28 | logger.info("Fetching pools on \(networkId)") 29 | 30 | do { 31 | let (data, response) = try await URLSession.shared.data(from: url) 32 | 33 | guard let httpResponse = response as? HTTPURLResponse, 34 | (200...299).contains(httpResponse.statusCode) else { 35 | throw CryptoAnalysisError.networkError("Invalid response") 36 | } 37 | 38 | // Parse the wrapped response 39 | let poolsResponse = try JSONDecoder().decode(DexPaprikaPoolsResponse.self, from: data) 40 | logger.info("Found \(poolsResponse.pools.count) pools on \(networkId)") 41 | return poolsResponse.pools 42 | 43 | } catch { 44 | logger.error("Failed to fetch network pools: \(error)") 45 | throw error 46 | } 47 | } 48 | 49 | /// Get pools for a specific DEX 50 | func getDexPools(network: String, dex: String, limit: Int = 20) async throws -> [DexPaprikaPool] { 51 | let networkId = networkMapping[network.lowercased()] ?? network.lowercased() 52 | let urlString = "\(dexPaprikaBaseURL)/networks/\(networkId)/dexes/\(dex)/pools?limit=\(limit)&orderBy=volume_usd&sort=desc" 53 | 54 | guard let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? "") else { 55 | throw CryptoAnalysisError.networkError("Invalid URL") 56 | } 57 | 58 | logger.info("Fetching pools for \(dex) on \(networkId)") 59 | 60 | do { 61 | let (data, response) = try await URLSession.shared.data(from: url) 62 | 63 | guard let httpResponse = response as? HTTPURLResponse, 64 | (200...299).contains(httpResponse.statusCode) else { 65 | throw CryptoAnalysisError.networkError("Invalid response") 66 | } 67 | 68 | // Try parsing as wrapped response first 69 | if let poolsResponse = try? JSONDecoder().decode(DexPaprikaPoolsResponse.self, from: data) { 70 | logger.info("Found \(poolsResponse.pools.count) pools for \(dex)") 71 | return poolsResponse.pools 72 | } 73 | 74 | // Fallback to array response 75 | let pools = try JSONDecoder().decode([DexPaprikaPool].self, from: data) 76 | logger.info("Found \(pools.count) pools for \(dex)") 77 | return pools 78 | 79 | } catch { 80 | logger.error("Failed to fetch DEX pools: \(error)") 81 | throw error 82 | } 83 | } 84 | 85 | /// Get detailed pool information 86 | func getPoolDetails(network: String, poolAddress: String) async throws -> DexPaprikaPoolDetail { 87 | let networkId = networkMapping[network.lowercased()] ?? network.lowercased() 88 | let urlString = "\(dexPaprikaBaseURL)/networks/\(networkId)/pools/\(poolAddress)" 89 | 90 | guard let url = URL(string: urlString) else { 91 | throw CryptoAnalysisError.networkError("Invalid URL") 92 | } 93 | 94 | logger.info("Fetching pool details for \(poolAddress) on \(networkId)") 95 | 96 | do { 97 | let (data, response) = try await URLSession.shared.data(from: url) 98 | 99 | guard let httpResponse = response as? HTTPURLResponse, 100 | (200...299).contains(httpResponse.statusCode) else { 101 | throw CryptoAnalysisError.networkError("Invalid response") 102 | } 103 | 104 | let poolDetail = try JSONDecoder().decode(DexPaprikaPoolDetail.self, from: data) 105 | logger.info("Retrieved pool details: \(poolDetail.name)") 106 | return poolDetail 107 | 108 | } catch { 109 | logger.error("Failed to fetch pool details: \(error)") 110 | throw error 111 | } 112 | } 113 | 114 | /// Get pools containing a specific token 115 | func getTokenPools(network: String, tokenAddress: String, limit: Int = 20) async throws -> [DexPaprikaPool] { 116 | let networkId = networkMapping[network.lowercased()] ?? network.lowercased() 117 | let urlString = "\(dexPaprikaBaseURL)/networks/\(networkId)/tokens/\(tokenAddress)/pools?limit=\(limit)&orderBy=volume_usd&sort=desc" 118 | 119 | guard let url = URL(string: urlString) else { 120 | throw CryptoAnalysisError.networkError("Invalid URL") 121 | } 122 | 123 | logger.info("Fetching pools for token \(tokenAddress)") 124 | 125 | do { 126 | let (data, response) = try await URLSession.shared.data(from: url) 127 | 128 | guard let httpResponse = response as? HTTPURLResponse, 129 | (200...299).contains(httpResponse.statusCode) else { 130 | throw CryptoAnalysisError.networkError("Invalid response") 131 | } 132 | 133 | // Try parsing as wrapped response first 134 | if let poolsResponse = try? JSONDecoder().decode(DexPaprikaPoolsResponse.self, from: data) { 135 | logger.info("Found \(poolsResponse.pools.count) pools containing token") 136 | return poolsResponse.pools 137 | } 138 | 139 | // Fallback to array response 140 | let pools = try JSONDecoder().decode([DexPaprikaPool].self, from: data) 141 | logger.info("Found \(pools.count) pools containing token") 142 | return pools 143 | 144 | } catch { 145 | logger.error("Failed to fetch token pools: \(error)") 146 | throw error 147 | } 148 | } 149 | 150 | // MARK: - DEX Operations 151 | 152 | /// Get list of DEXes on a network 153 | func getNetworkDexes(network: String) async throws -> [DexPaprikaDex] { 154 | let networkId = networkMapping[network.lowercased()] ?? network.lowercased() 155 | let urlString = "\(dexPaprikaBaseURL)/networks/\(networkId)/dexes" 156 | 157 | guard let url = URL(string: urlString) else { 158 | throw CryptoAnalysisError.networkError("Invalid URL") 159 | } 160 | 161 | logger.info("Fetching DEXes on \(networkId)") 162 | 163 | do { 164 | let (data, response) = try await URLSession.shared.data(from: url) 165 | 166 | guard let httpResponse = response as? HTTPURLResponse, 167 | (200...299).contains(httpResponse.statusCode) else { 168 | throw CryptoAnalysisError.networkError("Invalid response") 169 | } 170 | 171 | let dexResponse = try JSONDecoder().decode(DexPaprikaDexResponse.self, from: data) 172 | logger.info("Found \(dexResponse.dexes.count) DEXes on \(networkId)") 173 | return dexResponse.dexes 174 | 175 | } catch { 176 | logger.error("Failed to fetch DEXes: \(error)") 177 | throw error 178 | } 179 | } 180 | 181 | // MARK: - Historical Data 182 | 183 | /// Get OHLCV data for a pool 184 | func getPoolOHLCV(network: String, poolAddress: String, start: String, end: String? = nil, interval: String = "1d", limit: Int = 100) async throws -> [DexPaprikaOHLCV] { 185 | let networkId = networkMapping[network.lowercased()] ?? network.lowercased() 186 | var urlString = "\(dexPaprikaBaseURL)/networks/\(networkId)/pools/\(poolAddress)/ohlcv?start=\(start)&interval=\(interval)&limit=\(limit)" 187 | 188 | if let end = end { 189 | urlString += "&end=\(end)" 190 | } 191 | 192 | guard let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? "") else { 193 | throw CryptoAnalysisError.networkError("Invalid URL") 194 | } 195 | 196 | logger.info("Fetching OHLCV data for pool \(poolAddress)") 197 | 198 | do { 199 | let (data, response) = try await URLSession.shared.data(from: url) 200 | 201 | guard let httpResponse = response as? HTTPURLResponse, 202 | (200...299).contains(httpResponse.statusCode) else { 203 | throw CryptoAnalysisError.networkError("Invalid response") 204 | } 205 | 206 | let ohlcvData = try JSONDecoder().decode([DexPaprikaOHLCV].self, from: data) 207 | logger.info("Retrieved \(ohlcvData.count) OHLCV data points") 208 | return ohlcvData 209 | 210 | } catch { 211 | logger.error("Failed to fetch OHLCV data: \(error)") 212 | throw error 213 | } 214 | } 215 | 216 | // MARK: - Comparison Functions 217 | 218 | /// Compare token prices across different DEXes 219 | func compareTokenAcrossDexes(network: String, tokenAddress: String) async throws -> TokenDexComparison { 220 | // First get all pools for the token 221 | let pools = try await getTokenPools(network: network, tokenAddress: tokenAddress, limit: 50) 222 | 223 | // Group by DEX 224 | var dexPrices: [String: [DexPrice]] = [:] 225 | 226 | for pool in pools { 227 | let dexId = pool.dexId 228 | if dexPrices[dexId] == nil { 229 | dexPrices[dexId] = [] 230 | } 231 | 232 | let price = DexPrice( 233 | poolAddress: pool.address, 234 | poolName: pool.name, 235 | priceUsd: pool.priceUsd, 236 | liquidityUsd: pool.liquidityUsd, 237 | volume24h: pool.volumeUsd24h, 238 | feeRate: pool.feeRate 239 | ) 240 | 241 | dexPrices[dexId]?.append(price) 242 | } 243 | 244 | // Find best prices 245 | var allPrices = dexPrices.values.flatMap { $0 } 246 | allPrices.sort { $0.priceUsd < $1.priceUsd } 247 | 248 | let bestPrice = allPrices.first 249 | let worstPrice = allPrices.last 250 | 251 | // Calculate average 252 | let avgPrice = allPrices.reduce(0.0) { $0 + $1.priceUsd } / Double(allPrices.count) 253 | 254 | return TokenDexComparison( 255 | tokenAddress: tokenAddress, 256 | network: network, 257 | dexPrices: dexPrices, 258 | bestPrice: bestPrice, 259 | worstPrice: worstPrice, 260 | averagePrice: avgPrice, 261 | priceSpread: (worstPrice?.priceUsd ?? 0) - (bestPrice?.priceUsd ?? 0), 262 | timestamp: Date() 263 | ) 264 | } 265 | } 266 | 267 | // MARK: - Advanced DexPaprika Models 268 | 269 | // Wrapper for pools response 270 | struct DexPaprikaPoolsResponse: Codable { 271 | let pools: [DexPaprikaPool] 272 | let pageInfo: DexPaprikaPageInfo? 273 | 274 | private enum CodingKeys: String, CodingKey { 275 | case pools 276 | case pageInfo = "page_info" 277 | } 278 | } 279 | 280 | struct DexPaprikaPageInfo: Codable { 281 | let limit: Int 282 | let page: Int 283 | let totalItems: Int 284 | let totalPages: Int 285 | 286 | private enum CodingKeys: String, CodingKey { 287 | case limit, page 288 | case totalItems = "total_items" 289 | case totalPages = "total_pages" 290 | } 291 | } 292 | 293 | struct DexPaprikaPool: Codable { 294 | let id: String 295 | let dexId: String 296 | let dexName: String 297 | let chain: String 298 | let volumeUsd: Double 299 | let createdAt: String 300 | let createdAtBlockNumber: Int? 301 | let transactions: Int 302 | let priceUsd: Double 303 | let lastPriceChangeUsd5m: Double? 304 | let lastPriceChangeUsd1h: Double? 305 | let lastPriceChangeUsd24h: Double? 306 | let fee: Double? 307 | let tokens: [PoolToken] 308 | 309 | private enum CodingKeys: String, CodingKey { 310 | case id, chain, transactions, fee, tokens 311 | case dexId = "dex_id" 312 | case dexName = "dex_name" 313 | case volumeUsd = "volume_usd" 314 | case createdAt = "created_at" 315 | case createdAtBlockNumber = "created_at_block_number" 316 | case priceUsd = "price_usd" 317 | case lastPriceChangeUsd5m = "last_price_change_usd_5m" 318 | case lastPriceChangeUsd1h = "last_price_change_usd_1h" 319 | case lastPriceChangeUsd24h = "last_price_change_usd_24h" 320 | } 321 | 322 | // Computed properties for compatibility 323 | var address: String { id } 324 | var name: String { "\(tokens.first?.symbol ?? "?") / \(tokens.last?.symbol ?? "?")" } 325 | var networkId: String { chain } 326 | var volumeUsd24h: Double { volumeUsd } 327 | var liquidityUsd: Double { 0 } // Not provided in this endpoint 328 | var feeRate: Double? { fee } 329 | var token0: PoolToken { tokens.first ?? PoolToken(id: "", name: "", symbol: "", chain: "") } 330 | var token1: PoolToken { tokens.last ?? PoolToken(id: "", name: "", symbol: "", chain: "") } 331 | } 332 | 333 | struct PoolToken: Codable { 334 | let id: String 335 | let name: String 336 | let symbol: String 337 | let chain: String 338 | let type: String? 339 | let status: String? 340 | let decimals: Int? 341 | let totalSupply: Double? 342 | let description: String? 343 | let website: String? 344 | let addedAt: String? 345 | let fdv: Double? 346 | 347 | private enum CodingKeys: String, CodingKey { 348 | case id, name, symbol, chain, type, status, decimals, description, website, fdv 349 | case totalSupply = "total_supply" 350 | case addedAt = "added_at" 351 | } 352 | 353 | // Computed properties for compatibility 354 | var address: String { id } 355 | var logoUrl: String? { nil } 356 | 357 | // Initializer for empty token 358 | init(id: String, name: String, symbol: String, chain: String) { 359 | self.id = id 360 | self.name = name 361 | self.symbol = symbol 362 | self.chain = chain 363 | self.type = nil 364 | self.status = nil 365 | self.decimals = nil 366 | self.totalSupply = nil 367 | self.description = nil 368 | self.website = nil 369 | self.addedAt = nil 370 | self.fdv = nil 371 | } 372 | } 373 | 374 | struct DexPaprikaPoolDetail: Codable { 375 | let address: String 376 | let name: String 377 | let dexId: String 378 | let dexName: String 379 | let networkId: String 380 | let networkName: String 381 | let priceUsd: Double 382 | let priceToken0: Double 383 | let priceToken1: Double 384 | let volumeUsd24h: Double 385 | let volumeToken0_24h: Double 386 | let volumeToken1_24h: Double 387 | let liquidityUsd: Double 388 | let liquidityToken0: Double 389 | let liquidityToken1: Double 390 | let feeRate: Double? 391 | let priceChange24h: Double 392 | let volumeChange24h: Double 393 | let liquidityChange24h: Double 394 | let token0: PoolToken 395 | let token1: PoolToken 396 | let createdAt: String 397 | 398 | private enum CodingKeys: String, CodingKey { 399 | case address 400 | case name 401 | case dexId = "dex_id" 402 | case dexName = "dex_name" 403 | case networkId = "network_id" 404 | case networkName = "network_name" 405 | case priceUsd = "price_usd" 406 | case priceToken0 = "price_token_0" 407 | case priceToken1 = "price_token_1" 408 | case volumeUsd24h = "volume_usd_24h" 409 | case volumeToken0_24h = "volume_token_0_24h" 410 | case volumeToken1_24h = "volume_token_1_24h" 411 | case liquidityUsd = "liquidity_usd" 412 | case liquidityToken0 = "liquidity_token_0" 413 | case liquidityToken1 = "liquidity_token_1" 414 | case feeRate = "fee_rate" 415 | case priceChange24h = "price_change_24h" 416 | case volumeChange24h = "volume_change_24h" 417 | case liquidityChange24h = "liquidity_change_24h" 418 | case token0 = "token_0" 419 | case token1 = "token_1" 420 | case createdAt = "created_at" 421 | } 422 | } 423 | 424 | // Wrapper for DEX response 425 | struct DexPaprikaDexResponse: Codable { 426 | let dexes: [DexPaprikaDex] 427 | } 428 | 429 | struct DexPaprikaDex: Codable { 430 | let dexId: String 431 | let dexName: String 432 | let chain: String 433 | let `protocol`: String 434 | 435 | private enum CodingKeys: String, CodingKey { 436 | case dexId = "dex_id" 437 | case dexName = "dex_name" 438 | case chain 439 | case `protocol` = "protocol" 440 | } 441 | 442 | // Computed properties for compatibility 443 | var id: String { dexId } 444 | var name: String { dexName } 445 | var volumeUsd24h: Double { 0 } // Not provided in this endpoint 446 | var liquidityUsd: Double { 0 } // Not provided in this endpoint 447 | var poolCount: Int { 0 } // Not provided in this endpoint 448 | var logoUrl: String? { nil } // Not provided in this endpoint 449 | } 450 | 451 | struct DexPaprikaOHLCV: Codable { 452 | let timestamp: String 453 | let open: Double 454 | let high: Double 455 | let low: Double 456 | let close: Double 457 | let volume: Double 458 | } 459 | 460 | // MARK: - Comparison Models 461 | 462 | struct TokenDexComparison { 463 | let tokenAddress: String 464 | let network: String 465 | let dexPrices: [String: [DexPrice]] 466 | let bestPrice: DexPrice? 467 | let worstPrice: DexPrice? 468 | let averagePrice: Double 469 | let priceSpread: Double 470 | let timestamp: Date 471 | } 472 | 473 | struct DexPrice { 474 | let poolAddress: String 475 | let poolName: String 476 | let priceUsd: Double 477 | let liquidityUsd: Double 478 | let volume24h: Double 479 | let feeRate: Double? 480 | } 481 | -------------------------------------------------------------------------------- /Sources/CryptoAnalysisMCP/SupportResistanceAnalyzer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | 4 | /// Analyzes price data to identify support and resistance levels 5 | actor SupportResistanceAnalyzer { 6 | private let logger = Logger(label: "SupportResistanceAnalyzer") 7 | 8 | /// Minimum touches required for a level to be considered significant 9 | private let minTouches = 2 10 | 11 | /// Price tolerance for grouping similar levels 12 | private let priceTolerance = 0.02 // 2% 13 | 14 | /// Main method to find key support and resistance levels 15 | func findKeyLevels(data: [CandleData], timeframe: Timeframe = .daily) async -> [SupportResistanceLevel] { 16 | guard data.count >= 20 else { 17 | logger.info("Insufficient data for support/resistance analysis") 18 | return [] 19 | } 20 | 21 | var allLevels: [SupportResistanceLevel] = [] 22 | 23 | // Method 1: Pivot-based levels 24 | let pivotLevels = findPivotBasedLevels(data: data) 25 | allLevels.append(contentsOf: pivotLevels) 26 | 27 | // Method 2: Volume profile levels 28 | let volumeLevels = findVolumeProfileLevels(data: data) 29 | allLevels.append(contentsOf: volumeLevels) 30 | 31 | // Method 3: Fibonacci levels 32 | let fiboLevels = findFibonacciLevels(data: data) 33 | allLevels.append(contentsOf: fiboLevels) 34 | 35 | // Method 4: Psychological levels (round numbers) 36 | let psychoLevels = findPsychologicalLevels(data: data) 37 | allLevels.append(contentsOf: psychoLevels) 38 | 39 | // Consolidate and rank levels 40 | let consolidatedLevels = consolidateLevels(allLevels) 41 | 42 | // Sort by strength and filter active levels 43 | let sortedLevels = consolidatedLevels 44 | .filter { $0.isActive } 45 | .sorted { $0.strength > $1.strength } 46 | 47 | logger.info("Found \(sortedLevels.count) key support/resistance levels") 48 | 49 | return sortedLevels 50 | } 51 | 52 | // MARK: - Pivot-based Support/Resistance 53 | 54 | private func findPivotBasedLevels(data: [CandleData]) -> [SupportResistanceLevel] { 55 | var levels: [SupportResistanceLevel] = [] 56 | 57 | // Find local highs and lows 58 | var localHighs: [(price: Double, timestamp: Date, touches: Int)] = [] 59 | var localLows: [(price: Double, timestamp: Date, touches: Int)] = [] 60 | 61 | for i in 1..<(data.count - 1) { 62 | let prev = data[i - 1] 63 | let current = data[i] 64 | let next = data[i + 1] 65 | 66 | // Local high 67 | if current.high > prev.high && current.high > next.high { 68 | localHighs.append((price: current.high, timestamp: current.timestamp, touches: 1)) 69 | } 70 | 71 | // Local low 72 | if current.low < prev.low && current.low < next.low { 73 | localLows.append((price: current.low, timestamp: current.timestamp, touches: 1)) 74 | } 75 | } 76 | 77 | // Group similar price levels and count touches 78 | let groupedHighs = groupSimilarPriceLevels(localHighs) 79 | let groupedLows = groupSimilarPriceLevels(localLows) 80 | 81 | // Create resistance levels from highs 82 | for high in groupedHighs where high.touches >= minTouches { 83 | let level = SupportResistanceLevel( 84 | price: high.price, 85 | strength: calculateStrength(touches: high.touches, recency: high.timestamp), 86 | type: .resistance, 87 | touches: high.touches, 88 | lastTouch: high.timestamp, 89 | isActive: isLevelActive(price: high.price, currentPrice: data.last!.close) 90 | ) 91 | levels.append(level) 92 | } 93 | 94 | // Create support levels from lows 95 | for low in groupedLows where low.touches >= minTouches { 96 | let level = SupportResistanceLevel( 97 | price: low.price, 98 | strength: calculateStrength(touches: low.touches, recency: low.timestamp), 99 | type: .support, 100 | touches: low.touches, 101 | lastTouch: low.timestamp, 102 | isActive: isLevelActive(price: low.price, currentPrice: data.last!.close) 103 | ) 104 | levels.append(level) 105 | } 106 | 107 | return levels 108 | } 109 | 110 | // MARK: - Volume Profile Levels 111 | 112 | private func findVolumeProfileLevels(data: [CandleData]) -> [SupportResistanceLevel] { 113 | var levels: [SupportResistanceLevel] = [] 114 | 115 | // Create price buckets 116 | let priceRange = data.map { $0.high }.max()! - data.map { $0.low }.min()! 117 | let bucketSize = priceRange / 50 // 50 price buckets 118 | let minPrice = data.map { $0.low }.min()! 119 | 120 | var volumeProfile: [Int: Double] = [:] 121 | 122 | // Accumulate volume in price buckets 123 | for candle in data { 124 | let avgPrice = (candle.high + candle.low + candle.close) / 3 125 | let bucketIndex = Int((avgPrice - minPrice) / bucketSize) 126 | volumeProfile[bucketIndex, default: 0] += candle.volume 127 | } 128 | 129 | // Find high volume nodes (potential support/resistance) 130 | let sortedBuckets = volumeProfile.sorted { $0.value > $1.value } 131 | let topBuckets = Array(sortedBuckets.prefix(10)) // Top 10 volume nodes 132 | 133 | for (bucketIndex, volume) in topBuckets { 134 | let price = minPrice + (Double(bucketIndex) + 0.5) * bucketSize 135 | let currentPrice = data.last!.close 136 | 137 | let levelType: LevelType = price < currentPrice ? .support : .resistance 138 | 139 | // Count how many times price touched this level 140 | let touches = countPriceTouches(price: price, data: data) 141 | 142 | if touches >= minTouches { 143 | let level = SupportResistanceLevel( 144 | price: price, 145 | strength: calculateVolumeStrength(volume: volume, totalVolume: volumeProfile.values.reduce(0, +)), 146 | type: levelType, 147 | touches: touches, 148 | lastTouch: findLastTouch(price: price, data: data) ?? Date(), 149 | isActive: true 150 | ) 151 | levels.append(level) 152 | } 153 | } 154 | 155 | return levels 156 | } 157 | 158 | // MARK: - Fibonacci Levels 159 | 160 | private func findFibonacciLevels(data: [CandleData]) -> [SupportResistanceLevel] { 161 | var levels: [SupportResistanceLevel] = [] 162 | 163 | // Find swing high and low 164 | let high = data.map { $0.high }.max()! 165 | let low = data.map { $0.low }.min()! 166 | let range = high - low 167 | 168 | // Fibonacci ratios 169 | let fibRatios = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0] 170 | 171 | for ratio in fibRatios { 172 | let price = low + (range * ratio) 173 | let currentPrice = data.last!.close 174 | 175 | // Count actual touches at this level 176 | let touches = countPriceTouches(price: price, data: data) 177 | 178 | if touches >= 1 { // Lower threshold for Fibonacci levels 179 | let levelType: LevelType = price < currentPrice ? .support : .resistance 180 | 181 | let level = SupportResistanceLevel( 182 | price: price, 183 | strength: 0.5 + (Double(touches) * 0.1), // Base strength + touch bonus 184 | type: levelType, 185 | touches: touches, 186 | lastTouch: findLastTouch(price: price, data: data) ?? Date(), 187 | isActive: true 188 | ) 189 | levels.append(level) 190 | } 191 | } 192 | 193 | return levels 194 | } 195 | 196 | // MARK: - Psychological Levels 197 | 198 | private func findPsychologicalLevels(data: [CandleData]) -> [SupportResistanceLevel] { 199 | var levels: [SupportResistanceLevel] = [] 200 | 201 | let currentPrice = data.last!.close 202 | 203 | // Determine appropriate round number interval based on price 204 | let interval: Double 205 | if currentPrice < 1 { 206 | interval = 0.1 207 | } else if currentPrice < 10 { 208 | interval = 1 209 | } else if currentPrice < 100 { 210 | interval = 10 211 | } else if currentPrice < 1000 { 212 | interval = 100 213 | } else if currentPrice < 10000 { 214 | interval = 1000 215 | } else { 216 | interval = 10000 217 | } 218 | 219 | // Find round numbers within the data range 220 | let minPrice = data.map { $0.low }.min()! 221 | let maxPrice = data.map { $0.high }.max()! 222 | 223 | var price = floor(minPrice / interval) * interval 224 | 225 | while price <= maxPrice { 226 | if price >= minPrice { 227 | // Count how many times price touched this level 228 | let touches = countPriceTouches(price: price, data: data) 229 | 230 | if touches >= 1 { 231 | let levelType: LevelType = price < currentPrice ? .support : .resistance 232 | 233 | let level = SupportResistanceLevel( 234 | price: price, 235 | strength: 0.4 + (Double(touches) * 0.15), // Psychological levels have base strength 236 | type: levelType, 237 | touches: touches, 238 | lastTouch: findLastTouch(price: price, data: data) ?? Date(), 239 | isActive: true 240 | ) 241 | levels.append(level) 242 | } 243 | } 244 | price += interval 245 | } 246 | 247 | return levels 248 | } 249 | 250 | // MARK: - Helper Methods 251 | 252 | private func groupSimilarPriceLevels(_ levels: [(price: Double, timestamp: Date, touches: Int)]) -> [(price: Double, timestamp: Date, touches: Int)] { 253 | guard !levels.isEmpty else { return [] } 254 | 255 | var grouped: [(price: Double, timestamp: Date, touches: Int)] = [] 256 | var used = Set() 257 | 258 | for i in 0.. latestTimestamp { 273 | latestTimestamp = levels[j].timestamp 274 | } 275 | used.insert(j) 276 | } 277 | } 278 | 279 | let avgPrice = groupPrices.reduce(0, +) / Double(groupPrices.count) 280 | grouped.append((price: avgPrice, timestamp: latestTimestamp, touches: totalTouches)) 281 | } 282 | 283 | return grouped 284 | } 285 | 286 | private func countPriceTouches(price: Double, data: [CandleData]) -> Int { 287 | var touches = 0 288 | 289 | for candle in data { 290 | let tolerance = price * priceTolerance 291 | 292 | // Check if high touched the level 293 | if abs(candle.high - price) <= tolerance { 294 | touches += 1 295 | } 296 | // Check if low touched the level 297 | else if abs(candle.low - price) <= tolerance { 298 | touches += 1 299 | } 300 | // Check if price passed through the level 301 | else if candle.low < price && candle.high > price { 302 | touches += 1 303 | } 304 | } 305 | 306 | return touches 307 | } 308 | 309 | private func findLastTouch(price: Double, data: [CandleData]) -> Date? { 310 | for candle in data.reversed() { 311 | let tolerance = price * priceTolerance 312 | 313 | if abs(candle.high - price) <= tolerance || 314 | abs(candle.low - price) <= tolerance || 315 | (candle.low < price && candle.high > price) { 316 | return candle.timestamp 317 | } 318 | } 319 | 320 | return nil 321 | } 322 | 323 | private func calculateStrength(touches: Int, recency: Date) -> Double { 324 | // Base strength from number of touches 325 | var strength = min(Double(touches) / 10.0, 0.5) 326 | 327 | // Recency bonus (more recent = stronger) 328 | let daysSinceTouch = Date().timeIntervalSince(recency) / (24 * 60 * 60) 329 | if daysSinceTouch < 7 { 330 | strength += 0.3 331 | } else if daysSinceTouch < 30 { 332 | strength += 0.2 333 | } else if daysSinceTouch < 90 { 334 | strength += 0.1 335 | } 336 | 337 | // Additional touches bonus 338 | if touches >= 5 { 339 | strength += 0.2 340 | } else if touches >= 3 { 341 | strength += 0.1 342 | } 343 | 344 | return min(strength, 1.0) 345 | } 346 | 347 | private func calculateVolumeStrength(volume: Double, totalVolume: Double) -> Double { 348 | let volumeRatio = volume / totalVolume 349 | return min(volumeRatio * 10, 1.0) // Scale up as volume nodes are typically small percentages 350 | } 351 | 352 | private func isLevelActive(price: Double, currentPrice: Double) -> Bool { 353 | // A level is active if it's within 10% of current price 354 | let distance = abs(price - currentPrice) / currentPrice 355 | return distance <= 0.1 356 | } 357 | 358 | private func consolidateLevels(_ levels: [SupportResistanceLevel]) -> [SupportResistanceLevel] { 359 | guard !levels.isEmpty else { return [] } 360 | 361 | var consolidated: [SupportResistanceLevel] = [] 362 | var used = Set() 363 | 364 | let sortedLevels = levels.sorted { $0.price < $1.price } 365 | 366 | for i in 0.. [(slope: Double, intercept: Double, type: LevelType)] { 409 | var trendLines: [(slope: Double, intercept: Double, type: LevelType)] = [] 410 | 411 | // Find peaks and troughs 412 | var peaks: [(index: Int, price: Double)] = [] 413 | var troughs: [(index: Int, price: Double)] = [] 414 | 415 | for i in 1..<(data.count - 1) { 416 | if data[i].high > data[i-1].high && data[i].high > data[i+1].high { 417 | peaks.append((index: i, price: data[i].high)) 418 | } 419 | if data[i].low < data[i-1].low && data[i].low < data[i+1].low { 420 | troughs.append((index: i, price: data[i].low)) 421 | } 422 | } 423 | 424 | // Find resistance trend lines (connecting peaks) 425 | if peaks.count >= 2 { 426 | for i in 0..<(peaks.count - 1) { 427 | for j in (i + 1)..= 2 { 441 | for i in 0..<(troughs.count - 1) { 442 | for j in (i + 1).. Bool { 458 | var touchCount = 0 459 | let tolerance = priceTolerance 460 | 461 | for point in points { 462 | let expectedPrice = slope * Double(point.index) + intercept 463 | let actualPrice = point.price 464 | 465 | if abs(actualPrice - expectedPrice) / actualPrice <= tolerance { 466 | touchCount += 1 467 | } 468 | } 469 | 470 | return touchCount >= 3 // Require at least 3 touches for a valid trend line 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /Sources/CryptoAnalysisMCP/SimpleMCP.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | 4 | // MARK: - No-op Logger for production 5 | public struct SwiftLogNoOpLogHandler: LogHandler { 6 | public var logLevel: Logger.Level = .trace 7 | public var metadata: Logger.Metadata = [:] 8 | 9 | public init() {} 10 | 11 | public subscript(metadataKey key: String) -> Logger.Metadata.Value? { 12 | get { return nil } 13 | set(newValue) { } 14 | } 15 | 16 | public func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) { 17 | // Do nothing - no logging in production 18 | } 19 | } 20 | 21 | // MARK: - Simple MCP Protocol Implementation 22 | 23 | /// Basic MCP message structure that can be either request or notification 24 | struct MCPMessage: Codable { 25 | let jsonrpc: String 26 | let id: Int? 27 | let method: String? 28 | let params: [String: Any]? 29 | let result: [String: Any]? 30 | let error: MCPError? 31 | 32 | private enum CodingKeys: String, CodingKey { 33 | case jsonrpc, id, method, params, result, error 34 | } 35 | 36 | init(from decoder: Decoder) throws { 37 | let container = try decoder.container(keyedBy: CodingKeys.self) 38 | self.jsonrpc = try container.decodeIfPresent(String.self, forKey: .jsonrpc) ?? "2.0" 39 | self.id = try container.decodeIfPresent(Int.self, forKey: .id) 40 | self.method = try container.decodeIfPresent(String.self, forKey: .method) 41 | 42 | if container.contains(.params) { 43 | if let paramsDict = try? container.decode([String: Any].self, forKey: .params) { 44 | self.params = paramsDict 45 | } else { 46 | self.params = nil 47 | } 48 | } else { 49 | self.params = nil 50 | } 51 | 52 | if container.contains(.result) { 53 | if let resultDict = try? container.decode([String: Any].self, forKey: .result) { 54 | self.result = resultDict 55 | } else { 56 | self.result = nil 57 | } 58 | } else { 59 | self.result = nil 60 | } 61 | 62 | self.error = try container.decodeIfPresent(MCPError.self, forKey: .error) 63 | } 64 | 65 | func encode(to encoder: Encoder) throws { 66 | var container = encoder.container(keyedBy: CodingKeys.self) 67 | try container.encode(jsonrpc, forKey: .jsonrpc) 68 | try container.encodeIfPresent(id, forKey: .id) 69 | try container.encodeIfPresent(method, forKey: .method) 70 | 71 | if let params = params { 72 | try container.encode(params, forKey: .params) 73 | } 74 | 75 | if let result = result { 76 | try container.encode(result, forKey: .result) 77 | } 78 | 79 | try container.encodeIfPresent(error, forKey: .error) 80 | } 81 | } 82 | 83 | /// MCP Response message 84 | struct MCPResponse: Codable { 85 | let jsonrpc: String = "2.0" 86 | let id: Int? 87 | let result: [String: Any]? 88 | let error: MCPError? 89 | 90 | private enum CodingKeys: String, CodingKey { 91 | case jsonrpc, id, result, error 92 | } 93 | 94 | init(id: Int?, result: [String: Any]) { 95 | self.id = id 96 | self.result = result 97 | self.error = nil 98 | } 99 | 100 | init(id: Int?, error: MCPError) { 101 | self.id = id 102 | self.result = nil 103 | self.error = error 104 | } 105 | 106 | init(from decoder: Decoder) throws { 107 | let container = try decoder.container(keyedBy: CodingKeys.self) 108 | self.id = try container.decodeIfPresent(Int.self, forKey: .id) 109 | self.error = try container.decodeIfPresent(MCPError.self, forKey: .error) 110 | 111 | if container.contains(.result) { 112 | if let resultDict = try? container.decode([String: Any].self, forKey: .result) { 113 | self.result = resultDict 114 | } else { 115 | self.result = nil 116 | } 117 | } else { 118 | self.result = nil 119 | } 120 | } 121 | 122 | func encode(to encoder: Encoder) throws { 123 | var container = encoder.container(keyedBy: CodingKeys.self) 124 | try container.encode(jsonrpc, forKey: .jsonrpc) 125 | try container.encodeIfPresent(id, forKey: .id) 126 | 127 | if let result = result { 128 | try container.encode(result, forKey: .result) 129 | } 130 | 131 | try container.encodeIfPresent(error, forKey: .error) 132 | } 133 | } 134 | 135 | /// MCP Error 136 | struct MCPError: Codable, Error { 137 | let code: Int 138 | let message: String 139 | let data: [String: Any]? 140 | 141 | private enum CodingKeys: String, CodingKey { 142 | case code, message, data 143 | } 144 | 145 | init(code: Int, message: String, data: [String: Any]? = nil) { 146 | self.code = code 147 | self.message = message 148 | self.data = data 149 | } 150 | 151 | init(from decoder: Decoder) throws { 152 | let container = try decoder.container(keyedBy: CodingKeys.self) 153 | self.code = try container.decode(Int.self, forKey: .code) 154 | self.message = try container.decode(String.self, forKey: .message) 155 | 156 | if container.contains(.data) { 157 | if let dataDict = try? container.decode([String: Any].self, forKey: .data) { 158 | self.data = dataDict 159 | } else { 160 | self.data = nil 161 | } 162 | } else { 163 | self.data = nil 164 | } 165 | } 166 | 167 | func encode(to encoder: Encoder) throws { 168 | var container = encoder.container(keyedBy: CodingKeys.self) 169 | try container.encode(code, forKey: .code) 170 | try container.encode(message, forKey: .message) 171 | 172 | if let data = data { 173 | try container.encode(data, forKey: .data) 174 | } 175 | } 176 | } 177 | 178 | /// Tool definition for MCP 179 | struct MCPTool { 180 | let name: String 181 | let description: String 182 | let inputSchema: [String: Any] 183 | let handler: ([String: Any]) async -> [String: Any] 184 | 185 | init(name: String, description: String, inputSchema: [String: Any], handler: @escaping ([String: Any]) async -> [String: Any]) { 186 | self.name = name 187 | self.description = description 188 | self.inputSchema = inputSchema 189 | self.handler = handler 190 | } 191 | } 192 | 193 | /// Simple MCP Server implementation 194 | class MCPServer { 195 | private let name: String 196 | private let version: String 197 | private var tools: [String: MCPTool] = [:] 198 | private let logger = Logger(label: "MCPServer") 199 | private let debugMode: Bool 200 | 201 | init(name: String, version: String, debugMode: Bool = false) { 202 | self.name = name 203 | self.version = version 204 | self.debugMode = debugMode 205 | } 206 | 207 | func addTool(_ tool: MCPTool) { 208 | tools[tool.name] = tool 209 | if debugMode { 210 | logger.info("📝 Registered tool: \(tool.name)") 211 | } 212 | } 213 | 214 | func runStdio() async { 215 | if debugMode { 216 | logger.info("🚀 Starting MCP Server: \(name) v\(version)") 217 | logger.info("📡 Listening on STDIO...") 218 | } 219 | 220 | // Set stdin to line buffering mode 221 | setbuf(stdin, nil) 222 | 223 | // Main event loop - don't send anything until we receive a request 224 | while true { 225 | guard let line = readLine() else { 226 | if debugMode { 227 | logger.info("📛 STDIO closed, shutting down server") 228 | } 229 | break 230 | } 231 | 232 | if debugMode { 233 | logger.info("📥 Received line: \(line)") 234 | } 235 | 236 | await handleMessage(line) 237 | } 238 | } 239 | 240 | private func handleMessage(_ message: String) async { 241 | do { 242 | guard let data = message.data(using: .utf8) else { return } 243 | 244 | let decoder = JSONDecoder() 245 | let msg = try decoder.decode(MCPMessage.self, from: data) 246 | 247 | // Check if it's a notification (no id field) 248 | if msg.id == nil && msg.method != nil { 249 | // It's a notification - handle it without sending a response 250 | if debugMode { 251 | logger.info("📨 Received notification: \(msg.method!)") 252 | } 253 | 254 | // Handle specific notifications if needed 255 | switch msg.method! { 256 | case "notifications/initialized": 257 | // Claude Desktop sends this after initialize - just log it 258 | if debugMode { 259 | logger.info("✅ Client initialized successfully") 260 | } 261 | default: 262 | if debugMode { 263 | logger.info("🔕 Ignoring notification: \(msg.method!)") 264 | } 265 | } 266 | return 267 | } 268 | 269 | // It's a request - must have an id 270 | guard let method = msg.method, let id = msg.id else { 271 | if debugMode { 272 | logger.error("❌ Invalid message format - missing method or id") 273 | } 274 | return 275 | } 276 | 277 | if debugMode { 278 | logger.info("📨 Received request: \(method)") 279 | } 280 | 281 | switch method { 282 | case "initialize": 283 | await handleInitialize(id: id, params: msg.params) 284 | case "tools/list": 285 | await handleToolsList(id: id) 286 | case "tools/call": 287 | await handleToolCall(id: id, params: msg.params) 288 | default: 289 | await sendError(id: id, code: -32601, message: "Method not found: \(method)") 290 | } 291 | 292 | } catch { 293 | if debugMode { 294 | logger.error("❌ Failed to parse message: \(error)") 295 | } 296 | // Don't send parse errors for invalid messages 297 | } 298 | } 299 | 300 | private func handleInitialize(id: Int, params: [String: Any]?) async { 301 | let result: [String: Any] = [ 302 | "protocolVersion": "2024-11-05", 303 | "capabilities": [ 304 | "tools": [:] 305 | ], 306 | "serverInfo": [ 307 | "name": name, 308 | "version": version 309 | ] 310 | ] 311 | 312 | await sendResponse(id: id, result: result) 313 | } 314 | 315 | private func handleToolsList(id: Int) async { 316 | let toolsList = tools.values.map { tool in 317 | [ 318 | "name": tool.name, 319 | "description": tool.description, 320 | "inputSchema": tool.inputSchema 321 | ] 322 | } 323 | 324 | await sendResponse(id: id, result: ["tools": toolsList]) 325 | } 326 | 327 | private func handleToolCall(id: Int, params: [String: Any]?) async { 328 | guard let params = params, 329 | let toolName = params["name"] as? String, 330 | let arguments = params["arguments"] as? [String: Any] else { 331 | await sendError(id: id, code: -32602, message: "Invalid parameters") 332 | return 333 | } 334 | 335 | guard let tool = tools[toolName] else { 336 | await sendError(id: id, code: -32601, message: "Tool not found: \(toolName)") 337 | return 338 | } 339 | 340 | if debugMode { 341 | logger.info("🔧 Executing tool: \(toolName)") 342 | } 343 | 344 | let result = await tool.handler(arguments) 345 | 346 | // Convert result to proper MCP format 347 | let jsonData = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted) 348 | let jsonString = jsonData.flatMap { String(data: $0, encoding: .utf8) } ?? "{}" 349 | 350 | let mcpContent = [ 351 | [ 352 | "type": "text", 353 | "text": jsonString 354 | ] 355 | ] 356 | 357 | await sendResponse(id: id, result: ["content": mcpContent]) 358 | } 359 | 360 | private func sendResponse(id: Int?, result: [String: Any]) async { 361 | let response = MCPResponse(id: id, result: result) 362 | await sendMessage(response) 363 | } 364 | 365 | private func sendError(id: Int?, code: Int, message: String) async { 366 | let error = MCPError(code: code, message: message) 367 | let response = MCPResponse(id: id, error: error) 368 | await sendMessage(response) 369 | } 370 | 371 | private func sendMessage(_ message: T) async { 372 | do { 373 | let encoder = JSONEncoder() 374 | encoder.outputFormatting = [] 375 | let data = try encoder.encode(message) 376 | 377 | if let jsonString = String(data: data, encoding: .utf8) { 378 | print(jsonString) 379 | fflush(stdout) 380 | } 381 | } catch { 382 | if debugMode { 383 | logger.error("❌ Failed to encode message: \(error)") 384 | } 385 | } 386 | } 387 | } 388 | 389 | /// Convenience functions for creating tool schemas 390 | func createToolSchema( 391 | type: String = "object", 392 | properties: [String: [String: Any]] = [:], 393 | required: [String] = [] 394 | ) -> [String: Any] { 395 | var schema: [String: Any] = [ 396 | "type": type, 397 | "properties": properties 398 | ] 399 | 400 | if !required.isEmpty { 401 | schema["required"] = required 402 | } 403 | 404 | return schema 405 | } 406 | 407 | func createProperty(type: String, description: String, items: [String: Any]? = nil) -> [String: Any] { 408 | var property: [String: Any] = [ 409 | "type": type, 410 | "description": description 411 | ] 412 | 413 | if let items = items { 414 | property["items"] = items 415 | } 416 | 417 | return property 418 | } 419 | 420 | // MARK: - JSON Encoding/Decoding Extensions 421 | extension KeyedDecodingContainer { 422 | func decode(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any] { 423 | let container = try self.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) 424 | return try container.decode(type) 425 | } 426 | } 427 | 428 | extension KeyedEncodingContainer { 429 | mutating func encode(_ value: [String: Any], forKey key: K) throws { 430 | var container = self.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) 431 | try container.encode(value) 432 | } 433 | } 434 | 435 | extension UnkeyedDecodingContainer { 436 | mutating func decode(_ type: [String: Any].Type) throws -> [String: Any] { 437 | let container = try self.nestedContainer(keyedBy: JSONCodingKey.self) 438 | return try container.decode(type) 439 | } 440 | } 441 | 442 | extension UnkeyedEncodingContainer { 443 | mutating func encode(_ value: [String: Any]) throws { 444 | var container = self.nestedContainer(keyedBy: JSONCodingKey.self) 445 | try container.encode(value) 446 | } 447 | } 448 | 449 | extension KeyedDecodingContainer where K == JSONCodingKey { 450 | func decode(_ type: [String: Any].Type) throws -> [String: Any] { 451 | var dictionary = [String: Any]() 452 | 453 | for key in allKeys { 454 | if let boolValue = try? decode(Bool.self, forKey: key) { 455 | dictionary[key.stringValue] = boolValue 456 | } else if let stringValue = try? decode(String.self, forKey: key) { 457 | dictionary[key.stringValue] = stringValue 458 | } else if let intValue = try? decode(Int.self, forKey: key) { 459 | dictionary[key.stringValue] = intValue 460 | } else if let doubleValue = try? decode(Double.self, forKey: key) { 461 | dictionary[key.stringValue] = doubleValue 462 | } else if let nestedDictionary = try? decode([String: Any].self, forKey: key) { 463 | dictionary[key.stringValue] = nestedDictionary 464 | } else if let nestedArray = try? decode([Any].self, forKey: key) { 465 | dictionary[key.stringValue] = nestedArray 466 | } 467 | } 468 | return dictionary 469 | } 470 | } 471 | 472 | extension KeyedEncodingContainer where K == JSONCodingKey { 473 | mutating func encode(_ value: [String: Any]) throws { 474 | for (key, value) in value { 475 | let key = JSONCodingKey(stringValue: key)! 476 | switch value { 477 | case let value as Bool: 478 | try encode(value, forKey: key) 479 | case let value as String: 480 | try encode(value, forKey: key) 481 | case let value as Int: 482 | try encode(value, forKey: key) 483 | case let value as Double: 484 | try encode(value, forKey: key) 485 | case let value as [String: Any]: 486 | try encode(value, forKey: key) 487 | case let value as [Any]: 488 | try encode(value, forKey: key) 489 | default: 490 | continue 491 | } 492 | } 493 | } 494 | } 495 | 496 | extension KeyedDecodingContainer { 497 | func decode(_ type: [Any].Type, forKey key: K) throws -> [Any] { 498 | var container = try self.nestedUnkeyedContainer(forKey: key) 499 | return try container.decode(type) 500 | } 501 | } 502 | 503 | extension KeyedEncodingContainer { 504 | mutating func encode(_ value: [Any], forKey key: K) throws { 505 | var container = self.nestedUnkeyedContainer(forKey: key) 506 | try container.encode(value) 507 | } 508 | } 509 | 510 | extension UnkeyedDecodingContainer { 511 | mutating func decode(_ type: [Any].Type) throws -> [Any] { 512 | var array = [Any]() 513 | 514 | while !isAtEnd { 515 | if let boolValue = try? decode(Bool.self) { 516 | array.append(boolValue) 517 | } else if let stringValue = try? decode(String.self) { 518 | array.append(stringValue) 519 | } else if let intValue = try? decode(Int.self) { 520 | array.append(intValue) 521 | } else if let doubleValue = try? decode(Double.self) { 522 | array.append(doubleValue) 523 | } else if let nestedDictionary = try? decode([String: Any].self) { 524 | array.append(nestedDictionary) 525 | } else if let nestedArray = try? decode([Any].self) { 526 | array.append(nestedArray) 527 | } 528 | } 529 | return array 530 | } 531 | } 532 | 533 | extension UnkeyedEncodingContainer { 534 | mutating func encode(_ value: [Any]) throws { 535 | for value in value { 536 | switch value { 537 | case let value as Bool: 538 | try encode(value) 539 | case let value as String: 540 | try encode(value) 541 | case let value as Int: 542 | try encode(value) 543 | case let value as Double: 544 | try encode(value) 545 | case let value as [String: Any]: 546 | try encode(value) 547 | case let value as [Any]: 548 | try encode(value) 549 | default: 550 | continue 551 | } 552 | } 553 | } 554 | } 555 | 556 | struct JSONCodingKey: CodingKey { 557 | let stringValue: String 558 | let intValue: Int? 559 | 560 | init?(stringValue: String) { 561 | self.stringValue = stringValue 562 | self.intValue = nil 563 | } 564 | 565 | init?(intValue: Int) { 566 | self.stringValue = "\(intValue)" 567 | self.intValue = intValue 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | # CryptoAnalysisMCP v1.1 🚀 6 | 7 | **NEW: Now supports 7+ MILLION tokens through DexPaprika integration!** 🎉 8 | 9 | A Model Context Protocol (MCP) server for comprehensive cryptocurrency technical analysis. Built with Swift, it provides real-time price data, technical indicators, chart pattern detection, and trading signals for over 7 million cryptocurrencies - from Bitcoin to the newest meme coin on any DEX! 10 | 11 | ⚠️ **IMPORTANT FOR DAY TRADERS**: This tool requires a $99/mo Pro subscription for intraday analysis. The free tier only supports daily candles, making it suitable for swing traders and long-term investors only. 12 | 13 | ## 🆕 What's New in v1.1 14 | 15 | ### 🌟 DexPaprika Integration - 7+ MILLION Tokens! 16 | - **NO API KEY REQUIRED** for basic price data on ANY token 17 | - Access to **every token on every DEX** across 23+ blockchains 18 | - Automatic fallback: CoinPaprika → DexPaprika 19 | - Analyze that meme coin that launched 5 minutes ago! 20 | - Perfect for: 21 | - 🐸 Meme coin traders 22 | - 🦄 DeFi degens 23 | - 🚀 Early token hunters 24 | - 📊 Anyone tracking obscure tokens 25 | 26 | 🐦 **Follow [@m_pineapple__](https://x.com/m_pineapple__) for updates!** 27 | 28 | ### 🔧 New Liquidity & DEX Tools 29 | - **get_token_liquidity**: Track liquidity across all DEXes for any token 30 | - **search_tokens_by_network**: Find tokens on specific blockchains 31 | - **compare_dex_prices**: Compare token prices across different DEXes 32 | - **get_network_pools**: View top liquidity pools on any network 33 | - **get_dex_info**: Get information about DEXes on a network 34 | - **get_available_networks**: List all 23+ supported blockchains 35 | - **search_tokens_advanced**: Advanced search with liquidity/volume filters 36 | 37 | ## Features 38 | 39 | > 💡 **Not sure what to ask?** Check our [**Crypto Analysis Prompts Guide**](./PROMPTS.md) for inspiration! 40 | 41 | - **🆕 Universal Token Support**: 7+ MILLION tokens through DexPaprika integration 42 | - **🆕 Liquidity Pool Analytics**: Monitor liquidity, volume, and pool data across DEXes 43 | - **Dynamic Symbol Resolution**: Automatically supports all cryptocurrencies 44 | - **Real-time Price Data**: Current prices, volume, market cap, and percentage changes 45 | - **Technical Indicators**: RSI, MACD, Moving Averages, Bollinger Bands, and more 46 | - **Chart Pattern Detection**: Head & shoulders, triangles, double tops/bottoms 47 | - **Support & Resistance Levels**: Automatic identification of key price levels 48 | - **Trading Signals**: Buy/sell/hold recommendations based on technical analysis 49 | - **Multi-timeframe Analysis**: 4-hour, daily, weekly, and monthly timeframes 50 | - **Risk-adjusted Strategies**: Conservative, moderate, and aggressive trading approaches 51 | 52 | ## 🚀 Coming Soon 53 | 54 | We're actively working on exciting new features to make CryptoAnalysisMCP even more powerful: 55 | 56 | ### 🆕 Next Release (v1.2.0) 57 | ![image](https://github.com/user-attachments/assets/7f018851-c15a-464f-9391-be6fa24de61b) 58 | 59 | 60 | 61 | Want to suggest a feature? [Open an issue](https://github.com/M-Pineapple/CryptoAnalysisMCP/issues) on GitHub! 62 | 63 | ## ❓ Frequently Asked Questions 64 | 65 | ### Do I need a paid API key to use this MCP? 66 | 67 | **Short answer**: Depends on your trading style. 68 | 69 | ⚠️ **IMPORTANT**: Day traders and scalpers NEED a Pro subscription ($99/mo). The free tier only provides daily candles, which is useless for intraday trading. 70 | 71 | **What works WITHOUT any API key:** 72 | - ✅ Real-time price data (with slight delays) 73 | - ✅ Swing trading analysis (3-7 day trades) 74 | - ✅ Position trading (weeks to months) 75 | - ✅ Long-term investment analysis 76 | - ✅ All technical indicators on DAILY timeframe 77 | - ✅ 1 year of daily historical data 78 | - 🆕 Basic price data for 7+ MILLION tokens via DexPaprika 79 | - 🆕 Liquidity pool data across all major DEXes 80 | - 🆕 DEX price comparison and aggregation 81 | 82 | **What REQUIRES a Pro API key ($99/mo):** 83 | - ❌ Day trading (you need hourly/4h data) 84 | - ❌ Scalping (you need minute data) 85 | - ❌ Intraday patterns and signals 86 | - ❌ Real-time/low-latency updates 87 | - ❌ Historical data beyond 1 year 88 | - ❌ Any timeframe shorter than daily 89 | 90 | **How to get your FREE API key:** 91 | 1. Go to [CoinPaprika API](https://coinpaprika.com/api/) 92 | 2. Click "Start Free" 93 | 3. Register for an account 94 | 4. Get your API key 95 | 5. Add to Claude Desktop config: 96 | ```json 97 | { 98 | "mcpServers": { 99 | "crypto-analysis": { 100 | "command": "/path/to/crypto-analysis-mcp", 101 | "env": { 102 | "COINPAPRIKA_API_KEY": "your-free-api-key-here" 103 | } 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | The free tier includes: 110 | - ✅ 25,000 API calls per month 111 | - ✅ 1 year of daily historical data 112 | - ✅ 2,500+ cryptocurrencies 113 | 114 | **For advanced features**, upgrade to [CoinPaprika Pro](https://coinpaprika.com/api/): 115 | - ❌ 4-hour and hourly timeframes (Pro required) 116 | - ❌ Extended historical data beyond 1 year 117 | - ❌ Higher rate limits 118 | - ❌ Priority support 119 | 120 | ### Can I use CoinMarketCap or CoinGecko API instead? 121 | 122 | **Currently**: Not directly - this MCP is specifically built for CoinPaprika's API structure. 123 | 124 | **Coming in v1.2.0**: CoinMarketCap API support! 🎉 125 | 126 | Key differences: 127 | - **CoinMarketCap**: Different endpoint structure (support coming in v1.2.0!) 128 | - **CoinGecko**: Different data format (planned for future release) 129 | - **CoinPaprika**: Best coverage (71,000+ assets vs 10,000-20,000 for competitors) 130 | 131 | We chose CoinPaprika first because: 132 | - 3x more market coverage than competitors 133 | - More generous free tier 134 | - Better historical data access 135 | - Superior API reliability (99.9% uptime) 136 | 137 | Once v1.2.0 is released, you'll be able to switch between CoinPaprika and CoinMarketCap APIs with a simple configuration change! 138 | 139 | ### What cryptocurrencies are supported? 140 | 141 | **🆕 v1.1: Now supports 7+ MILLION tokens!** 142 | 143 | With our new DexPaprika integration: 144 | - ✅ **All 2,500+ CoinPaprika tokens** (major coins with full analysis) 145 | - ✅ **7+ MILLION DEX tokens** via DexPaprika (automatic fallback) 146 | - ✅ **Every token on every DEX** across 23+ blockchains 147 | - ✅ **Brand new tokens** - analyze tokens minutes after launch 148 | - ✅ **Obscure meme coins** - if it trades on a DEX, we have it 149 | - ✅ **NO API KEY NEEDED** for basic price data 150 | 151 | Examples: 152 | - Major coins: BTC, ETH, SOL (full technical analysis via CoinPaprika) 153 | - Popular memes: DOGE, SHIB, PEPE, WOJAK (price data from any source) 154 | - New launches: That token that launched 5 minutes ago on Uniswap 155 | - Any ERC-20, BEP-20, SPL token, or token on any supported chain 156 | 157 | Just use the ticker symbol - the MCP automatically finds it! 158 | 159 | ### Why am I getting 402 Payment Required errors? 160 | 161 | You're trying to use features that require a Pro subscription: 162 | 163 | **Common causes:** 164 | - Using any timeframe other than 'daily' (4h, 1h, 15m, etc.) 165 | - Requesting data older than 1 year 166 | - Exceeding rate limits (rare) 167 | 168 | **Solutions:** 169 | 1. **For swing trading/investing**: Just use 'daily' timeframe - it's free! 170 | 2. **For day trading**: You MUST [upgrade to CoinPaprika Pro](https://coinpaprika.com/api/) ($99/mo) 171 | 172 | **There is NO free option for day trading**. If you need intraday data, you need to pay. 173 | 174 | ### How accurate are the trading signals? 175 | 176 | ⚠️ **Important**: Trading signals are for informational purposes only! 177 | 178 | - Based on well-established technical indicators 179 | - No prediction is 100% accurate 180 | - Always do your own research 181 | - Never invest more than you can afford to lose 182 | - Consider multiple factors beyond technical analysis 183 | 184 | ### Can I use this for automated trading? 185 | 186 | While technically possible, we **strongly advise caution**: 187 | - This MCP provides analysis, not execution 188 | - Requires additional safety mechanisms 189 | - Needs proper risk management 190 | - Should be thoroughly backtested 191 | - Consider paper trading first 192 | 193 | ### How often does the data update? 194 | 195 | Depends on your API tier: 196 | - **Free tier**: ~1-5 minute delays 197 | - **Pro tier**: 30-second updates for prices 198 | - **Cached locally**: 1-5 minutes to reduce API calls 199 | 200 | ### Is my API key secure? 201 | 202 | Yes! Your API key: 203 | - Is never hardcoded 204 | - Only read from environment variables 205 | - Never logged or transmitted 206 | - Only used for CoinPaprika API calls 207 | - Follows security best practices 208 | 209 | ### Can I contribute to this project? 210 | 211 | Absolutely! We welcome contributions: 212 | - Bug fixes 213 | - New indicators 214 | - Performance improvements 215 | - Documentation updates 216 | - Feature suggestions 217 | 218 | See our [Contributing](#contributing) section for guidelines. 219 | 220 | ### Where can I get help? 221 | 222 | 1. Check this FAQ first 223 | 2. Read the [documentation](https://github.com/M-Pineapple/CryptoAnalysisMCP) 224 | 3. Search [existing issues](https://github.com/M-Pineapple/CryptoAnalysisMCP/issues) 225 | 4. Open a new issue with details 226 | 5. Join our community discussions 227 | 228 | ### Why Swift instead of Python/JavaScript? 229 | 230 | Swift offers: 231 | - Native macOS performance 232 | - Type safety and modern concurrency 233 | - Excellent memory management 234 | - Seamless Claude Desktop integration 235 | - Growing ecosystem for server-side development 236 | 237 | Plus, we love Swift! 🍍 238 | 239 | ## Prerequisites 240 | 241 | - macOS 10.15 or later 242 | - Swift 5.5 or later 243 | - Xcode 13+ (for development) 244 | - Claude Desktop 245 | 246 | ## Installation 247 | 248 | ### Prerequisites 249 | 250 | 1. **Get a FREE CoinPaprika API Key** (optional but recommended for technical analysis): 251 | - Visit [CoinPaprika API](https://coinpaprika.com/api/) 252 | - Click "Start Free" and register 253 | - Copy your API key for step 3 254 | - 🆕 Note: Basic price data now works without API key via DexPaprika! 255 | 256 | ### Quick Install 257 | 258 | 1. Clone the repository: 259 | ```bash 260 | git clone https://github.com/M-Pineapple/CryptoAnalysisMCP.git 261 | cd CryptoAnalysisMCP 262 | ``` 263 | 264 | 2. Build the project: 265 | ```bash 266 | ./build-release.sh 267 | ``` 268 | 269 | 3. Configure Claude Desktop by adding to `~/Library/Application Support/Claude/claude_desktop_config.json`: 270 | ```json 271 | { 272 | "mcpServers": { 273 | "crypto-analysis": { 274 | "command": "/path/to/CryptoAnalysisMCP/crypto-analysis-mcp", 275 | "env": { 276 | "COINPAPRIKA_API_KEY": "your-free-api-key-here" 277 | } 278 | } 279 | } 280 | } 281 | ``` 282 | 283 | 4. Restart Claude Desktop 284 | 285 | ### Global Installation (Optional) 286 | 287 | ```bash 288 | sudo cp ./.build/release/CryptoAnalysisMCP /usr/local/bin/crypto-analysis-mcp 289 | ``` 290 | 291 | Then use this in Claude Desktop config: 292 | ```json 293 | { 294 | "mcpServers": { 295 | "crypto-analysis": { 296 | "command": "/usr/local/bin/crypto-analysis-mcp" 297 | } 298 | } 299 | } 300 | ``` 301 | 302 | ## Usage 303 | 304 | ### 📝 Example Prompts 305 | 306 | **New to crypto analysis?** Check out our comprehensive [**Crypto Analysis Prompts Guide**](./PROMPTS.md) with 100+ example prompts for: 307 | - 🏃 Day Trading 308 | - 📊 Swing Trading 309 | - 💼 Long-term Investing 310 | - 📈 Technical Indicators 311 | - 🎯 Risk Management 312 | - And much more! 313 | 314 | ### Available Commands 315 | 316 | Once configured, you can use these commands in Claude: 317 | 318 | ### Get Current Price 319 | ``` 320 | crypto-analysis:get_crypto_price 321 | symbol: "BTC" 322 | ``` 323 | 324 | ### Technical Indicators 325 | ``` 326 | crypto-analysis:get_technical_indicators 327 | symbol: "ETH" 328 | timeframe: "daily" 329 | ``` 330 | 331 | ### Chart Pattern Detection 332 | ``` 333 | crypto-analysis:detect_chart_patterns 334 | symbol: "SOL" 335 | timeframe: "4h" 336 | ``` 337 | 338 | ### Trading Signals 339 | ``` 340 | crypto-analysis:get_trading_signals 341 | symbol: "ADA" 342 | risk_level: "moderate" 343 | timeframe: "daily" 344 | ``` 345 | 346 | ### Full Analysis 347 | ``` 348 | crypto-analysis:get_full_analysis 349 | symbol: "DOT" 350 | timeframe: "weekly" 351 | risk_level: "aggressive" 352 | ``` 353 | 354 | ### Support & Resistance 355 | ``` 356 | crypto-analysis:get_support_resistance 357 | symbol: "MATIC" 358 | timeframe: "daily" 359 | ``` 360 | 361 | ### Multi-timeframe Analysis 362 | ``` 363 | crypto-analysis:multi_timeframe_analysis 364 | symbol: "AVAX" 365 | ``` 366 | 367 | ### 🆕 NEW v1.1 Commands 368 | 369 | ### Get Token Liquidity 370 | ``` 371 | crypto-analysis:get_token_liquidity 372 | symbol: "PEPE" 373 | network: "ethereum" (optional) 374 | ``` 375 | 376 | ### Search Tokens by Network 377 | ``` 378 | crypto-analysis:search_tokens_by_network 379 | network: "solana" 380 | query: "meme" (optional) 381 | limit: 20 382 | ``` 383 | 384 | ### Compare DEX Prices 385 | ``` 386 | crypto-analysis:compare_dex_prices 387 | symbol: "SHIB" 388 | network: "ethereum" 389 | ``` 390 | 391 | ### Get Network Pools 392 | ``` 393 | crypto-analysis:get_network_pools 394 | network: "ethereum" 395 | sort_by: "volume_usd" 396 | limit: 10 397 | ``` 398 | 399 | ### Get Available Networks 400 | ``` 401 | crypto-analysis:get_available_networks 402 | ``` 403 | 404 | ## 💡 Quick Examples 405 | 406 | Here are some natural language prompts you can use: 407 | 408 | **1. Quick Analysis** 409 | ``` 410 | "Give me a quick technical analysis of [SYMBOL]" 411 | "Is [SYMBOL] bullish or bearish right now?" 412 | "What's the trend for [SYMBOL]?" 413 | ``` 414 | 415 | **2. Day Trading Focus** 416 | ``` 417 | "Analyze [SYMBOL] for day trading opportunities" 418 | "Show me scalping levels for [SYMBOL] today" 419 | "What are the intraday support and resistance for [SYMBOL]?" 420 | ``` 421 | 422 | **3. Swing Trading Analysis** 423 | ``` 424 | "Provide swing trading setup for [SYMBOL] with 3-7 day outlook" 425 | "Analyze [SYMBOL] patterns on daily timeframe for swing trades" 426 | "Give me entry, stop loss, and targets for swing trading [SYMBOL]" 427 | ``` 428 | 429 | **4. Full Institutional Analysis** 430 | ``` 431 | "Do a complete Wall Street analyst report on [SYMBOL]" 432 | "Analyze [SYMBOL] like a hedge fund would" 433 | "Give me all technical indicators, patterns, and signals for [SYMBOL]" 434 | ``` 435 | 436 | **5. Risk-Based Strategies** 437 | ``` 438 | "Show me conservative trading strategy for [SYMBOL]" 439 | "What's the aggressive play on [SYMBOL]?" 440 | "Give me risk-adjusted entries for [SYMBOL]" 441 | ``` 442 | 443 | **6. Specific Indicator Requests** 444 | ``` 445 | "What's the RSI and MACD saying about [SYMBOL]?" 446 | "Check Bollinger Bands squeeze on [SYMBOL]" 447 | "Are there any chart patterns forming on [SYMBOL]?" 448 | ``` 449 | 450 | **7. 🆕 Meme Coin & DEX Token Analysis** 451 | ``` 452 | "What's the price of WOJAK?" 453 | "Analyze that new PEPE fork on Ethereum" 454 | "Show me price data for [obscure token]" 455 | "Track this Uniswap token: [contract address]" 456 | ``` 457 | 458 | **8. 🆕 Liquidity & DEX Analytics** 459 | ``` 460 | "What's the liquidity for SHIB across all DEXes?" 461 | "Show me the top pools on Solana" 462 | "Compare PEPE prices on different DEXes" 463 | "Find high liquidity meme coins on BSC" 464 | "Which DEX has the best price for ETH?" 465 | "Show me all tokens on Arbitrum with >$1M liquidity" 466 | ``` 467 | 468 | 💡 **Replace [SYMBOL] with any cryptocurrency ticker** (BTC, ETH, SOL, etc.) 469 | 470 | 👉 **See 100+ more examples in our [Crypto Analysis Prompts Guide](./PROMPTS.md)** 471 | 472 | ## Supported Cryptocurrencies 473 | 474 | **🆕 v1.1 Update**: The MCP now supports **7+ MILLION tokens** through our dual-provider system: 475 | 476 | 1. **CoinPaprika** (Primary): 2,500+ major cryptocurrencies with full technical analysis 477 | 2. **DexPaprika** (Fallback): 7+ million DEX tokens across 23+ blockchains - NO API KEY REQUIRED! 478 | 479 | The MCP automatically: 480 | - Checks CoinPaprika first for established tokens (better data, more features) 481 | - Falls back to DexPaprika for any token not found 482 | - Caches results for optimal performance 483 | - Works with just the ticker symbol 484 | 485 | **Supported Networks via DexPaprika**: 486 | - Ethereum, BSC, Polygon, Arbitrum, Optimism, Base 487 | - Solana, Avalanche, Fantom, Aptos, Sui 488 | - And 12+ more chains! 489 | 490 | Just use any ticker symbol - if it exists on any DEX, we'll find it! 491 | 492 | ## Configuration 493 | 494 | ### API Key (Optional but Recommended) 495 | 496 | ⚠️ **Important**: 497 | - 🆕 Basic price data now works WITHOUT API key via DexPaprika! 498 | - Technical analysis features still require a FREE CoinPaprika API key 499 | 500 | #### Get your FREE API key: 501 | 502 | 1. Visit [CoinPaprika API](https://coinpaprika.com/api/) 503 | 2. Click "Start Free" 504 | 3. Create an account 505 | 4. Copy your API key 506 | 507 | #### Add to Claude Desktop: 508 | 509 | **Option 1 - Environment Variable (Recommended):** 510 | ```json 511 | { 512 | "mcpServers": { 513 | "crypto-analysis": { 514 | "command": "/path/to/crypto-analysis-mcp", 515 | "env": { 516 | "COINPAPRIKA_API_KEY": "your-api-key-here" 517 | } 518 | } 519 | } 520 | } 521 | ``` 522 | 523 | **Option 2 - System Environment:** 524 | ```bash 525 | export COINPAPRIKA_API_KEY="your-api-key-here" 526 | ``` 527 | 528 | ## Trading Style Compatibility 529 | 530 | ![image](https://github.com/user-attachments/assets/10a83419-dec3-43b8-95cc-31be3f01ee41) 531 | 532 | 533 | **Bottom Line**: If you're a day trader, you MUST get the Pro subscription. There's no workaround. 534 | 535 | ### Free vs Paid Tiers 536 | 537 | ![image](https://github.com/user-attachments/assets/841f123f-5ec6-4c93-a336-8b6b183852b2) 538 | 539 | 540 | ## Timeframes 541 | 542 | **Free Tier (No API Key):** 543 | - `daily` - Daily candles only ✅ 544 | 545 | **Pro Tier ($99/mo) - All timeframes:** 546 | - `5m` - 5-minute candles 547 | - `15m` - 15-minute candles 548 | - `30m` - 30-minute candles 549 | - `1h` - 1-hour candles 550 | - `4h` - 4-hour candles 551 | - `daily` - Daily candles 552 | - `weekly` - Weekly candles 553 | 554 | 💡 **Note**: Attempting to use any timeframe other than 'daily' without a Pro key will result in an error. 555 | 556 | ## Risk Levels 557 | 558 | - `conservative` - Lower risk, focus on strong signals 559 | - `moderate` - Balanced approach (default) 560 | - `aggressive` - Higher risk, more sensitive signals 561 | 562 | ## Development 563 | 564 | ### Building from Source 565 | 566 | ```bash 567 | # Clone the repository 568 | git clone https://github.com/M-Pineapple/CryptoAnalysisMCP.git 569 | cd CryptoAnalysisMCP 570 | 571 | # Build debug version 572 | swift build 573 | 574 | # Build release version 575 | swift build -c release 576 | 577 | # Run tests 578 | swift test 579 | ``` 580 | 581 | ### Project Structure 582 | 583 | ``` 584 | CryptoAnalysisMCP/ 585 | ├── Sources/ 586 | │ └── CryptoAnalysisMCP/ 587 | │ ├── Main.swift # Entry point 588 | │ ├── SimpleMCP.swift # MCP protocol implementation 589 | │ ├── CryptoDataProvider.swift # CoinPaprika API integration 590 | │ ├── DexPaprikaDataProvider.swift # 🆕 DexPaprika integration 591 | │ ├── TechnicalAnalyzer.swift # Indicators & calculations 592 | │ ├── ChartPatternRecognizer.swift # Pattern detection 593 | │ ├── SupportResistanceAnalyzer.swift # Support/resistance levels 594 | │ ├── AnalysisFormatters.swift # Output formatting 595 | │ └── Models.swift # Data models 596 | ├── Tests/ # Unit tests 597 | ├── Package.swift # Swift package manifest 598 | └── README.md # This file 599 | ``` 600 | 601 | ## Contributing 602 | 603 | Contributions are welcome! Please feel free to submit a Pull Request. 604 | 605 | 1. Fork the repository 606 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 607 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 608 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 609 | 5. Open a Pull Request 610 | 611 | ## License 612 | 613 | This project is licensed under the MIT License - see the LICENSE file for details. 614 | 615 | ## Acknowledgments 616 | 617 | - Built with Swift and the Model Context Protocol 618 | - Powered by CoinPaprika API for cryptocurrency data 619 | - 🆕 Enhanced with DexPaprika for 7+ million DEX tokens 620 | - Technical analysis algorithms based on industry standards 621 | - Special thanks to the CoinPaprika team for their support! 622 | 623 | ## Troubleshooting 624 | 625 | ### MCP not appearing in Claude 626 | 627 | 1. Ensure the path in `claude_desktop_config.json` is absolute 628 | 2. Check that the binary has execute permissions: `chmod +x crypto-analysis-mcp` 629 | 3. Restart Claude Desktop after configuration changes 630 | 631 | ### API Rate Limits 632 | 633 | The free tier of CoinPaprika has rate limits. If you encounter 402 errors, consider: 634 | - Using daily timeframe (most compatible with free tier) 635 | - Adding an API key for higher limits 636 | - Implementing request throttling 637 | 638 | ### Build Issues 639 | 640 | If you encounter build errors: 641 | 1. Ensure Swift 5.5+ is installed: `swift --version` 642 | 2. Clean the build: `swift package clean` 643 | 3. Update dependencies: `swift package update` 644 | 645 | ## 💖 Support This Project 646 | 647 | If CryptoAnalysisMCP has helped enhance your crypto analysis workflow or saved you time with technical indicators, consider supporting its development: 648 | 649 | Buy Me A Coffee 650 | 651 | Your support helps me: 652 | * Maintain and improve CryptoAnalysisMCP with new features 653 | * Keep the project open-source and free for everyone 654 | * Dedicate more time to addressing user requests and bug fixes 655 | * Explore new indicators and analysis techniques 656 | 657 | Thank you for considering supporting my work! 🙏 658 | 659 | ## Support 660 | 661 | For issues, questions, or suggestions, please open an issue on GitHub. 662 | 663 | ## Connect 664 | 665 | Follow me for updates and crypto analysis insights: 666 | - 🐦 Twitter/X: [@m_pineapple__](https://x.com/m_pineapple__) 667 | - 🐙 GitHub: [@M-Pineapple](https://github.com/M-Pineapple) 668 | 669 | --- 670 | 671 | Made with ❤️ by 🍍 672 | -------------------------------------------------------------------------------- /Sources/CryptoAnalysisMCP/Main.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | import Logging 4 | 5 | @main 6 | struct CryptoAnalysisMCP: AsyncParsableCommand { 7 | static let configuration = CommandConfiguration( 8 | commandName: "crypto-analysis-mcp", 9 | abstract: "A Model Context Protocol server for cryptocurrency technical analysis", 10 | version: "1.1.0" 11 | ) 12 | 13 | @Option(help: "Transport method") 14 | var transport: String = "stdio" 15 | 16 | @Flag(help: "Enable debug logging to stderr") 17 | var debug = false 18 | 19 | func run() async throws { 20 | // Set up logging - only if debug flag is set 21 | if debug { 22 | LoggingSystem.bootstrap { label in 23 | var handler = StreamLogHandler.standardError(label: label) 24 | handler.logLevel = .info 25 | return handler 26 | } 27 | } else { 28 | // Silent logger for production 29 | LoggingSystem.bootstrap { label in 30 | return SwiftLogNoOpLogHandler() 31 | } 32 | } 33 | 34 | let logger = Logger(label: "CryptoAnalysisMCP") 35 | logger.info("🚀 Starting Crypto Analysis MCP Server v1.1.0") 36 | 37 | // Create the analysis handler 38 | let analysisHandler = CryptoAnalysisHandler() 39 | 40 | // Create the MCP server 41 | let server = MCPServer( 42 | name: "crypto-analysis", 43 | version: "1.1.0", 44 | debugMode: debug 45 | ) 46 | 47 | // Register all analysis tools 48 | await registerTools(server: server, handler: analysisHandler) 49 | 50 | // Register v1.1 DexPaprika tools 51 | await registerDexPaprikaTools(server: server, handler: analysisHandler) 52 | 53 | logger.info("✅ Registered crypto analysis tools (v1.1 with full DexPaprika integration)") 54 | 55 | // Start the server based on transport 56 | switch transport { 57 | case "stdio": 58 | await server.runStdio() 59 | default: 60 | throw ValidationError("Unsupported transport: \(transport)") 61 | } 62 | } 63 | 64 | private func registerTools(server: MCPServer, handler: CryptoAnalysisHandler) async { 65 | // Price tool 66 | server.addTool(MCPTool( 67 | name: "get_crypto_price", 68 | description: "Get current price and market data for a cryptocurrency", 69 | inputSchema: createToolSchema( 70 | properties: [ 71 | "symbol": createProperty( 72 | type: "string", 73 | description: "Cryptocurrency symbol (e.g., BTC, ETH, ADA)" 74 | ) 75 | ], 76 | required: ["symbol"] 77 | ) 78 | ) { arguments in 79 | await handler.getCurrentPrice(arguments: arguments) 80 | }) 81 | 82 | // Technical indicators tool 83 | server.addTool(MCPTool( 84 | name: "get_technical_indicators", 85 | description: "Calculate technical indicators (RSI, MACD, SMA, EMA, Bollinger Bands)", 86 | inputSchema: createToolSchema( 87 | properties: [ 88 | "symbol": createProperty( 89 | type: "string", 90 | description: "Cryptocurrency symbol" 91 | ), 92 | "timeframe": createProperty( 93 | type: "string", 94 | description: "Timeframe: 4h, daily, weekly (default: daily)" 95 | ), 96 | "indicators": createProperty( 97 | type: "array", 98 | description: "Specific indicators to calculate (optional)", 99 | items: ["type": "string"] 100 | ) 101 | ], 102 | required: ["symbol"] 103 | ) 104 | ) { arguments in 105 | await handler.getTechnicalIndicators(arguments: arguments) 106 | }) 107 | 108 | // Chart patterns tool 109 | server.addTool(MCPTool( 110 | name: "detect_chart_patterns", 111 | description: "Detect chart patterns like head & shoulders, triangles, double tops/bottoms", 112 | inputSchema: createToolSchema( 113 | properties: [ 114 | "symbol": createProperty( 115 | type: "string", 116 | description: "Cryptocurrency symbol" 117 | ), 118 | "timeframe": createProperty( 119 | type: "string", 120 | description: "Timeframe: 4h, daily, weekly (default: daily)" 121 | ) 122 | ], 123 | required: ["symbol"] 124 | ) 125 | ) { arguments in 126 | await handler.detectChartPatterns(arguments: arguments) 127 | }) 128 | 129 | // Multi-timeframe analysis tool 130 | server.addTool(MCPTool( 131 | name: "multi_timeframe_analysis", 132 | description: "Analyze trends and signals across multiple timeframes", 133 | inputSchema: createToolSchema( 134 | properties: [ 135 | "symbol": createProperty( 136 | type: "string", 137 | description: "Cryptocurrency symbol" 138 | ) 139 | ], 140 | required: ["symbol"] 141 | ) 142 | ) { arguments in 143 | await handler.getMultiTimeframeAnalysis(arguments: arguments) 144 | }) 145 | 146 | // Trading signals tool 147 | server.addTool(MCPTool( 148 | name: "get_trading_signals", 149 | description: "Generate buy/sell/hold signals based on technical analysis", 150 | inputSchema: createToolSchema( 151 | properties: [ 152 | "symbol": createProperty( 153 | type: "string", 154 | description: "Cryptocurrency symbol" 155 | ), 156 | "risk_level": createProperty( 157 | type: "string", 158 | description: "Risk level: conservative, moderate, aggressive (default: moderate)" 159 | ), 160 | "timeframe": createProperty( 161 | type: "string", 162 | description: "Timeframe: 4h, daily, weekly (default: daily)" 163 | ) 164 | ], 165 | required: ["symbol"] 166 | ) 167 | ) { arguments in 168 | await handler.getTradingSignals(arguments: arguments) 169 | }) 170 | 171 | // Support/Resistance tool 172 | server.addTool(MCPTool( 173 | name: "get_support_resistance", 174 | description: "Find key support and resistance levels", 175 | inputSchema: createToolSchema( 176 | properties: [ 177 | "symbol": createProperty( 178 | type: "string", 179 | description: "Cryptocurrency symbol" 180 | ), 181 | "timeframe": createProperty( 182 | type: "string", 183 | description: "Timeframe: 4h, daily, weekly (default: daily)" 184 | ) 185 | ], 186 | required: ["symbol"] 187 | ) 188 | ) { arguments in 189 | await handler.getSupportResistance(arguments: arguments) 190 | }) 191 | 192 | // Full analysis tool 193 | server.addTool(MCPTool( 194 | name: "get_full_analysis", 195 | description: "Get comprehensive technical analysis including all indicators, patterns, and signals", 196 | inputSchema: createToolSchema( 197 | properties: [ 198 | "symbol": createProperty( 199 | type: "string", 200 | description: "Cryptocurrency symbol" 201 | ), 202 | "timeframe": createProperty( 203 | type: "string", 204 | description: "Timeframe: 4h, daily, weekly (default: daily)" 205 | ), 206 | "risk_level": createProperty( 207 | type: "string", 208 | description: "Risk level: conservative, moderate, aggressive (default: moderate)" 209 | ) 210 | ], 211 | required: ["symbol"] 212 | ) 213 | ) { arguments in 214 | await handler.getFullAnalysis(arguments: arguments) 215 | }) 216 | } 217 | } 218 | 219 | // MARK: - Crypto Analysis Handler 220 | 221 | actor CryptoAnalysisHandler { 222 | private let dataProvider = CryptoDataProvider() 223 | private let technicalAnalyzer = TechnicalAnalyzer() 224 | private let patternRecognizer = ChartPatternRecognizer() 225 | private let supportResistanceAnalyzer = SupportResistanceAnalyzer() 226 | let logger = Logger(label: "CryptoAnalysisHandler") 227 | 228 | // Cache for performance 229 | private var analysisCache: [String: (data: AnalysisResult, timestamp: Date)] = [:] 230 | private let cacheTimeout: TimeInterval = 120 // 2 minutes 231 | 232 | // Accessor for DexPaprika provider 233 | var dexPaprikaProvider: DexPaprikaDataProvider { 234 | get async { 235 | await dataProvider.dexPaprikaProvider 236 | } 237 | } 238 | 239 | // MARK: - Tool Implementation Methods 240 | 241 | func getCurrentPrice(arguments: [String: Any]) async -> [String: Any] { 242 | guard let symbol = arguments["symbol"] as? String else { 243 | return ["error": "Symbol is required"] 244 | } 245 | 246 | do { 247 | let priceData = try await dataProvider.getCurrentPrice(symbol: symbol.uppercased()) 248 | logger.info("Retrieved price data for \(symbol): $\(String(format: "%.2f", priceData.price))") 249 | return formatPriceData(priceData) 250 | } catch { 251 | logger.error("Failed to get price for \(symbol): \(error)") 252 | return ["error": "Failed to fetch price data: \(error.localizedDescription)"] 253 | } 254 | } 255 | 256 | func getTechnicalIndicators(arguments: [String: Any]) async -> [String: Any] { 257 | guard let symbol = arguments["symbol"] as? String else { 258 | return ["error": "Symbol is required"] 259 | } 260 | 261 | let timeframe = parseTimeframe(arguments["timeframe"] as? String) 262 | 263 | do { 264 | let historicalData = try await dataProvider.getHistoricalData( 265 | symbol: symbol.uppercased(), 266 | timeframe: timeframe, 267 | periods: 200 268 | ) 269 | 270 | guard !historicalData.isEmpty else { 271 | return ["error": "No historical data available for \(symbol)"] 272 | } 273 | 274 | let indicators = await technicalAnalyzer.calculateAllIndicators(data: historicalData) 275 | let latestIndicators = getLatestIndicatorValues(indicators) 276 | 277 | logger.info("Calculated \(indicators.count) indicators for \(symbol)") 278 | 279 | return [ 280 | "symbol": symbol.uppercased(), 281 | "timeframe": timeframe.rawValue, 282 | "timestamp": ISO8601DateFormatter().string(from: Date()), 283 | "indicators": latestIndicators, 284 | "data_points": historicalData.count 285 | ] 286 | } catch { 287 | logger.error("Failed to calculate indicators: \(error)") 288 | return ["error": error.localizedDescription] 289 | } 290 | } 291 | 292 | func detectChartPatterns(arguments: [String: Any]) async -> [String: Any] { 293 | guard let symbol = arguments["symbol"] as? String else { 294 | return ["error": "Symbol is required"] 295 | } 296 | 297 | let timeframe = parseTimeframe(arguments["timeframe"] as? String) 298 | 299 | do { 300 | let historicalData = try await dataProvider.getHistoricalData( 301 | symbol: symbol.uppercased(), 302 | timeframe: timeframe, 303 | periods: 100 304 | ) 305 | 306 | guard !historicalData.isEmpty else { 307 | return ["error": "No historical data available for \(symbol)"] 308 | } 309 | 310 | let patterns = await patternRecognizer.detectPatterns(data: historicalData) 311 | 312 | logger.info("Detected \(patterns.count) patterns for \(symbol)") 313 | 314 | return [ 315 | "symbol": symbol.uppercased(), 316 | "timeframe": timeframe.rawValue, 317 | "timestamp": ISO8601DateFormatter().string(from: Date()), 318 | "patterns": patterns.map(formatPattern), 319 | "pattern_count": patterns.count 320 | ] 321 | } catch { 322 | logger.error("Failed to detect patterns: \(error)") 323 | return ["error": error.localizedDescription] 324 | } 325 | } 326 | 327 | func getMultiTimeframeAnalysis(arguments: [String: Any]) async -> [String: Any] { 328 | guard let symbol = arguments["symbol"] as? String else { 329 | return ["error": "Symbol is required"] 330 | } 331 | 332 | var timeframeResults: [String: [String: Any]] = [:] 333 | let timeframes: [Timeframe] = [.fourHour, .daily, .weekly] 334 | 335 | for timeframe in timeframes { 336 | do { 337 | let data = try await dataProvider.getHistoricalData( 338 | symbol: symbol.uppercased(), 339 | timeframe: timeframe, 340 | periods: 100 341 | ) 342 | 343 | if !data.isEmpty { 344 | let indicators = await technicalAnalyzer.calculateAllIndicators(data: data) 345 | let patterns = await patternRecognizer.detectPatterns(data: data) 346 | let levels = await supportResistanceAnalyzer.findKeyLevels(data: data, timeframe: timeframe) 347 | 348 | let (overallSignal, confidence) = await technicalAnalyzer.generateOverallSignal(indicators: indicators) 349 | 350 | timeframeResults[timeframe.rawValue] = [ 351 | "indicators": getLatestIndicatorValues(indicators), 352 | "patterns": patterns.map(formatPattern), 353 | "key_levels": levels.prefix(5).map(formatSupportResistance), 354 | "overall_signal": overallSignal.rawValue, 355 | "confidence": confidence, 356 | "trend": determineTrend(from: indicators) 357 | ] 358 | } 359 | } catch { 360 | logger.warning("Failed to analyze \(timeframe.rawValue): \(error)") 361 | } 362 | } 363 | 364 | return [ 365 | "symbol": symbol.uppercased(), 366 | "timestamp": ISO8601DateFormatter().string(from: Date()), 367 | "timeframes": timeframeResults, 368 | "analysis_summary": generateMultiTimeframeSummary(timeframeResults) 369 | ] 370 | } 371 | 372 | func getTradingSignals(arguments: [String: Any]) async -> [String: Any] { 373 | guard let symbol = arguments["symbol"] as? String else { 374 | return ["error": "Symbol is required"] 375 | } 376 | 377 | let riskLevel = parseRiskLevel(arguments["risk_level"] as? String) 378 | let timeframe = parseTimeframe(arguments["timeframe"] as? String) 379 | 380 | do { 381 | let data = try await dataProvider.getHistoricalData( 382 | symbol: symbol.uppercased(), 383 | timeframe: timeframe, 384 | periods: 100 385 | ) 386 | 387 | let currentPrice = try await dataProvider.getCurrentPrice(symbol: symbol.uppercased()) 388 | let indicators = await technicalAnalyzer.calculateAllIndicators(data: data) 389 | let patterns = await patternRecognizer.detectPatterns(data: data) 390 | let levels = await supportResistanceAnalyzer.findKeyLevels(data: data) 391 | 392 | let signals = await generateTradingSignals( 393 | indicators: indicators, 394 | patterns: patterns, 395 | levels: levels, 396 | currentPrice: currentPrice, 397 | riskLevel: riskLevel 398 | ) 399 | 400 | return [ 401 | "symbol": symbol.uppercased(), 402 | "timeframe": timeframe.rawValue, 403 | "risk_level": riskLevel.rawValue, 404 | "current_price": currentPrice.price, 405 | "timestamp": ISO8601DateFormatter().string(from: Date()), 406 | "signals": signals 407 | ] 408 | } catch { 409 | logger.error("Failed to generate signals: \(error)") 410 | return ["error": error.localizedDescription] 411 | } 412 | } 413 | 414 | func getSupportResistance(arguments: [String: Any]) async -> [String: Any] { 415 | guard let symbol = arguments["symbol"] as? String else { 416 | return ["error": "Symbol is required"] 417 | } 418 | 419 | let timeframe = parseTimeframe(arguments["timeframe"] as? String) 420 | 421 | do { 422 | let data = try await dataProvider.getHistoricalData( 423 | symbol: symbol.uppercased(), 424 | timeframe: timeframe, 425 | periods: 100 426 | ) 427 | 428 | let levels = await supportResistanceAnalyzer.findKeyLevels(data: data, timeframe: timeframe) 429 | let currentPrice = try await dataProvider.getCurrentPrice(symbol: symbol.uppercased()) 430 | 431 | return [ 432 | "symbol": symbol.uppercased(), 433 | "timeframe": timeframe.rawValue, 434 | "current_price": currentPrice.price, 435 | "timestamp": ISO8601DateFormatter().string(from: Date()), 436 | "support_levels": levels.filter { $0.type == .support }.map(formatSupportResistance), 437 | "resistance_levels": levels.filter { $0.type == .resistance }.map(formatSupportResistance), 438 | "nearest_support": findNearestLevel(levels.filter { $0.type == .support }, to: currentPrice.price) as Any, 439 | "nearest_resistance": findNearestLevel(levels.filter { $0.type == .resistance }, to: currentPrice.price) as Any 440 | ] 441 | } catch { 442 | logger.error("Failed to find levels: \(error)") 443 | return ["error": error.localizedDescription] 444 | } 445 | } 446 | 447 | func getFullAnalysis(arguments: [String: Any]) async -> [String: Any] { 448 | guard let symbol = arguments["symbol"] as? String else { 449 | return ["error": "Symbol is required"] 450 | } 451 | 452 | let timeframe = parseTimeframe(arguments["timeframe"] as? String) 453 | let riskLevel = parseRiskLevel(arguments["risk_level"] as? String) 454 | let cacheKey = "\(symbol.uppercased())_\(timeframe.rawValue)_\(riskLevel.rawValue)" 455 | 456 | // Check cache 457 | if let cached = analysisCache[cacheKey], 458 | Date().timeIntervalSince(cached.timestamp) < cacheTimeout { 459 | logger.info("Returning cached analysis for \(symbol)") 460 | return formatAnalysisResult(cached.data) 461 | } 462 | 463 | do { 464 | logger.info("Performing full analysis for \(symbol)") 465 | 466 | let currentPrice = try await dataProvider.getCurrentPrice(symbol: symbol.uppercased()) 467 | let historicalData = try await dataProvider.getHistoricalData( 468 | symbol: symbol.uppercased(), 469 | timeframe: timeframe, 470 | periods: 200 471 | ) 472 | 473 | // Perform all analyses 474 | async let indicators = technicalAnalyzer.calculateAllIndicators(data: historicalData) 475 | async let patterns = patternRecognizer.detectPatterns(data: historicalData) 476 | async let levels = supportResistanceAnalyzer.findKeyLevels(data: historicalData, timeframe: timeframe) 477 | 478 | let (calculatedIndicators, detectedPatterns, keyLevels) = await (indicators, patterns, levels) 479 | 480 | let (overallSignal, confidence) = await technicalAnalyzer.generateOverallSignal(indicators: calculatedIndicators) 481 | 482 | let analysisResult = AnalysisResult( 483 | symbol: symbol.uppercased(), 484 | timestamp: Date(), 485 | currentPrice: currentPrice.price, 486 | indicators: calculatedIndicators, 487 | patterns: detectedPatterns, 488 | supportResistance: keyLevels, 489 | signals: [overallSignal], 490 | overallSignal: overallSignal, 491 | confidence: confidence, 492 | summary: generateComprehensiveSummary( 493 | symbol: symbol, 494 | price: currentPrice, 495 | indicators: calculatedIndicators, 496 | patterns: detectedPatterns, 497 | levels: keyLevels, 498 | signal: overallSignal, 499 | confidence: confidence 500 | ), 501 | recommendations: generateRecommendations( 502 | indicators: calculatedIndicators, 503 | patterns: detectedPatterns, 504 | levels: keyLevels, 505 | riskLevel: riskLevel 506 | ) 507 | ) 508 | 509 | // Cache the result 510 | analysisCache[cacheKey] = (analysisResult, Date()) 511 | 512 | logger.info("Completed full analysis for \(symbol)") 513 | 514 | return formatAnalysisResult(analysisResult) 515 | } catch { 516 | logger.error("Full analysis failed: \(error)") 517 | return ["error": error.localizedDescription] 518 | } 519 | } 520 | 521 | // MARK: - Helper Methods 522 | 523 | private func generateTradingSignals( 524 | indicators: [IndicatorResult], 525 | patterns: [ChartPattern], 526 | levels: [SupportResistanceLevel], 527 | currentPrice: PriceData, 528 | riskLevel: RiskLevel 529 | ) async -> [String: Any] { 530 | 531 | let (indicatorSignal, indicatorConfidence) = await technicalAnalyzer.generateOverallSignal(indicators: indicators) 532 | 533 | // Pattern-based signals 534 | let recentPatterns = patterns.filter { $0.confidence >= riskLevel.signalThreshold } 535 | let patternSignals = recentPatterns.map { $0.type.isBullish ? TradingSignal.buy : TradingSignal.sell } 536 | 537 | // Support/Resistance signals 538 | let nearestSupport = findNearestLevel(levels.filter { $0.type == .support }, to: currentPrice.price) 539 | let nearestResistance = findNearestLevel(levels.filter { $0.type == .resistance }, to: currentPrice.price) 540 | 541 | var levelSignal = TradingSignal.hold 542 | if let support = nearestSupport, abs(currentPrice.price - support) / currentPrice.price < 0.02 { 543 | levelSignal = .buy 544 | } else if let resistance = nearestResistance, abs(currentPrice.price - resistance) / currentPrice.price < 0.02 { 545 | levelSignal = .sell 546 | } 547 | 548 | // Combine signals 549 | let allSignals = [indicatorSignal, levelSignal] + patternSignals 550 | let signalCounts = allSignals.reduce(into: [TradingSignal: Int]()) { counts, signal in 551 | counts[signal, default: 0] += 1 552 | } 553 | 554 | let totalSignals = allSignals.count 555 | let buySignals = signalCounts[.buy, default: 0] + signalCounts[.strongBuy, default: 0] 556 | let sellSignals = signalCounts[.sell, default: 0] + signalCounts[.strongSell, default: 0] 557 | 558 | let finalSignal: TradingSignal 559 | let confidence: Double 560 | 561 | if Double(buySignals) / Double(totalSignals) >= 0.6 { 562 | finalSignal = .buy 563 | confidence = Double(buySignals) / Double(totalSignals) 564 | } else if Double(sellSignals) / Double(totalSignals) >= 0.6 { 565 | finalSignal = .sell 566 | confidence = Double(sellSignals) / Double(totalSignals) 567 | } else { 568 | finalSignal = .hold 569 | confidence = 0.5 570 | } 571 | 572 | return [ 573 | "primary_signal": finalSignal.rawValue, 574 | "confidence": confidence, 575 | "signal_breakdown": [ 576 | "indicator_signal": indicatorSignal.rawValue, 577 | "indicator_confidence": indicatorConfidence, 578 | "pattern_signals": patternSignals.map { $0.rawValue }, 579 | "level_signal": levelSignal.rawValue 580 | ], 581 | "risk_adjusted": confidence >= riskLevel.signalThreshold, 582 | "entry_price": currentPrice.price, 583 | "stop_loss": calculateStopLoss(signal: finalSignal, price: currentPrice.price, levels: levels) as Any, 584 | "take_profit": calculateTakeProfit(signal: finalSignal, price: currentPrice.price, levels: levels) as Any, 585 | "reasoning": generateSignalReasoning( 586 | signal: finalSignal, 587 | indicators: indicators, 588 | patterns: recentPatterns, 589 | levels: levels, 590 | currentPrice: currentPrice.price 591 | ) 592 | ] 593 | } 594 | 595 | private func generateMultiTimeframeSummary(_ timeframeResults: [String: [String: Any]]) -> String { 596 | let timeframes = timeframeResults.keys.sorted() 597 | let summary = "Multi-timeframe analysis shows: " 598 | 599 | let summaryParts = timeframes.compactMap { timeframe -> String? in 600 | guard let results = timeframeResults[timeframe], 601 | let trend = results["trend"] as? String, 602 | let signal = results["overall_signal"] as? String else { return nil } 603 | 604 | return "\(timeframe) \(trend.lowercased()) with \(signal.lowercased()) signal" 605 | } 606 | 607 | return summary + summaryParts.joined(separator: ", ") 608 | } 609 | } 610 | --------------------------------------------------------------------------------