├── .env.example ├── .gitignore ├── ProjectPhase2TaskList.md ├── ProjectPlanPhase2.md ├── README.md ├── docs ├── PinescriptProject1Plan.md └── TaskList1.md ├── enhanced ├── simple_strategy_enhanced_1.pine └── simple_strategy_enhanced_2.pine ├── examples ├── Example_PineScriptStrat1_1hr.txt ├── Modified_GoldScalping_Strategy.txt ├── Modified_GoldScalping_Strategy_With_Volume.txt ├── basic_validation.ts └── simple_strategy.pine ├── jest.config.js ├── memory-bank ├── activeContext.md ├── progress.md └── systemPatterns.md ├── package-lock.json ├── package.json ├── src ├── cli │ ├── commands │ │ └── llm.ts │ └── index.ts ├── config │ ├── configTool.ts │ ├── protocolConfig.js │ ├── protocolConfig.ts │ └── userConfig.ts ├── fixers │ └── errorFixer.ts ├── index.ts ├── patched-protocol.d.ts ├── patched-protocol.js ├── services │ └── llmService.ts ├── templates │ ├── indicators │ │ ├── bollingerBands.ts │ │ └── macd.ts │ ├── simplified-gold-strategy.js │ ├── strategies │ │ ├── movingAverageCross.ts │ │ └── rsiStrategy.ts │ ├── templateManager.ts │ └── testScript.ts ├── utils │ ├── errorDetector.ts │ ├── formatter.ts │ ├── versionDetector.ts │ └── versionManager.ts └── validators │ └── syntaxValidator.ts ├── tests ├── config │ ├── configTool.test.ts │ └── userConfig.test.ts ├── fixers │ └── errorFixer.test.ts ├── integration │ ├── realWorldTests.test.ts │ ├── realWorldTests.ts │ └── test-report.json ├── manual │ ├── test-basic.js │ ├── test-complex-validation.js │ ├── test-fix-errors.js │ ├── test-single-error.js │ ├── test-strategy-update.js │ ├── test-strategy.js │ └── test-validation.js ├── mcp │ ├── errorFixer.tool.test.ts │ ├── formatter.tool.test.ts │ ├── server.test.ts │ └── versionTool.test.ts ├── templates │ └── templateManager.test.ts ├── utils │ ├── formatter.test.ts │ ├── versionDetector.test.ts │ └── versionManager.test.ts └── validators │ ├── syntaxValidator.test.ts │ └── v6Validator.test.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # NeonDB PostgreSQL Vector Database 2 | NEON_DB_URL=postgresql://username:password@your-neon-db-host.cloud/dbname?sslmode=require 3 | NEON_API_KEY=your_neon_api_key 4 | 5 | # OpenAI API 6 | OPENAI_API_KEY=your_openai_key_here 7 | 8 | # Anthropic API 9 | ANTHROPIC_API_KEY=your_anthropic_key_here 10 | 11 | # Supabase Configuration 12 | SUPABASE_URL=https://your-supabase-project.supabase.co 13 | SUPABASE_API_KEY=your_supabase_api_key 14 | SUPABASE_DB_URL=postgresql://postgres:password@localhost:5432/postgres 15 | 16 | # Web Search Configuration 17 | WEBSEARCH_API_PROVIDER=google 18 | GOOGLE_SEARCH_API_KEY=your_google_search_api_key 19 | GOOGLE_SEARCH_ENGINE_ID=your_search_engine_id 20 | 21 | # GitHub Integration 22 | GITHUB_ACCESS_TOKEN=your_github_personal_access_token 23 | GITHUB_USERNAME=your_github_username 24 | 25 | # File System MCP Configuration 26 | FS_ROOT_PATH=/path/to/root/directory 27 | FS_ALLOWED_PATHS=/path/to/allowed/directory1,/path/to/allowed/directory2 28 | 29 | # Vector Database (for semantic search) 30 | PINECONE_API_KEY=your_pinecone_api_key 31 | PINECONE_ENVIRONMENT=your_pinecone_region 32 | 33 | # Server settings 34 | HOST=127.0.0.1 35 | PORT=5001 36 | 37 | # Logging settings 38 | LOG_LEVEL=INFO -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | jspm_packages/ 4 | 5 | # Compiled output 6 | dist/ 7 | build/ 8 | out/ 9 | *.tsbuildinfo 10 | 11 | # Environment variables and secrets 12 | .env 13 | .env.* 14 | !.env.example 15 | 16 | # Logs 17 | logs 18 | *.log 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | lerna-debug.log* 23 | 24 | # Cache and history files 25 | .npm 26 | .eslintcache 27 | .node_repl_history 28 | .pinescript_history/ 29 | 30 | # IDE and editor folders 31 | .idea/ 32 | .vscode/ 33 | *.swp 34 | *.swo 35 | .DS_Store 36 | .prettierrc 37 | 38 | # Coverage directories 39 | coverage/ 40 | .nyc_output/ 41 | 42 | # Testing 43 | .jest/ 44 | 45 | # Local development 46 | .local/ -------------------------------------------------------------------------------- /ProjectPhase2TaskList.md: -------------------------------------------------------------------------------- 1 | # Project Phase 2 Task List: LLM-Driven Strategy Optimization 2 | 3 | ## Phase 2.1: LLM Integration Framework (Week 1) 4 | 5 | 1. [x] Set up LLM service integration architecture 6 | - [x] ~~Create `src/llm` directory structure~~ Created `src/services` for LLM services 7 | - [x] Implement `llmService.ts` with provider abstraction 8 | - [x] Implement prompt templating system 9 | - [x] Create CLI commands for LLM functionality 10 | - [ ] Add connection to OpenAI/Anthropic API 11 | - [ ] Add robust error handling for API calls 12 | 13 | 2. [x] User configuration for LLM 14 | - [x] Add LLM configuration section to userConfig.ts 15 | - [x] Create configureLLM helper function 16 | - [x] Implement provider selection based on config 17 | - [x] Add default configuration values 18 | - [x] Create CLI command for updating LLM settings 19 | 20 | 3. [ ] Set up real LLM provider implementations 21 | - [ ] Implement OpenAI provider with API connection 22 | - [ ] Implement Anthropic provider with API connection 23 | - [ ] Add retry logic and timeout handling 24 | - [ ] Implement proper error handling for API failures 25 | - [ ] Add response validation and sanity checks 26 | 27 | 4. [ ] Design strategy parser for LLM analysis 28 | - [ ] Create `src/core/strategy` directory 29 | - [ ] Implement script component extractor 30 | - [ ] Add parameter identification logic 31 | - [ ] Create entry/exit condition parser 32 | - [ ] Build pattern detector for common strategy types 33 | 34 | 5. [ ] Set up response handling utilities 35 | - [x] Define response interfaces for strategy analysis 36 | - [x] Define interfaces for backtest analysis 37 | - [x] Define interfaces for enhanced strategies 38 | - [ ] Implement JSON response parser with validation 39 | - [ ] Add string template extractor for code blocks 40 | - [ ] Create validation utilities for LLM outputs 41 | - [ ] Build response transformation pipeline 42 | 43 | ## Phase 2.2: Strategy Analysis Tools (Week 2) 44 | 45 | 6. [x] Set up CLI command interface 46 | - [x] Create CLI entry point with command registration 47 | - [x] Implement `analyze` command for strategy analysis 48 | - [x] Implement `enhance` command for strategy enhancement 49 | - [x] Implement `config` command for LLM settings 50 | - [x] Add help information and examples 51 | 52 | 7. [ ] Implement strategy analysis module 53 | - [ ] Create `src/automation/analysis` directory 54 | - [ ] Build strategy analyzer class 55 | - [ ] Implement parameter extraction 56 | - [ ] Add logic weakness detection 57 | - [ ] Create risk management analyzer 58 | - [ ] Implement API for MCP integration 59 | 60 | 8. [ ] Develop the analysis MCP tool 61 | - [ ] Add `analyze_strategy` tool to MCP server 62 | - [ ] Integrate with existing validation 63 | - [ ] Implement analysis caching 64 | - [ ] Add progress reporting 65 | - [ ] Create test cases for strategy analysis 66 | 67 | 9. [ ] Create the strategy enhancement generator 68 | - [x] Implement basic enhanced version generator via CLI 69 | - [ ] Add template-based enhancement system 70 | - [ ] Create validation pipeline for generated strategies 71 | - [ ] Implement version tracking 72 | - [ ] Add `generate_enhanced_strategies` MCP tool 73 | 74 | ## Phase 2.3: Backtest Analysis Framework (Week 3) 75 | 76 | 10. [ ] Build backtest result parser 77 | - [ ] Create `src/automation/backtesting` directory 78 | - [ ] Implement JSON backtest result parser 79 | - [ ] Add image-based backtest result extractor (optional) 80 | - [ ] Create performance metrics standardizer 81 | - [ ] Build comparison utilities 82 | 83 | 11. [ ] Implement backtest analysis system 84 | - [ ] Create backtest analyzer class 85 | - [ ] Implement performance issue detection 86 | - [ ] Add improvement recommendation generator 87 | - [ ] Create parameter adjustment calculator 88 | - [ ] Implement `analyze_backtest_results` MCP tool 89 | 90 | 12. [ ] Develop final optimization module 91 | - [ ] Implement optimized strategy generator 92 | - [ ] Create feature extraction from different versions 93 | - [ ] Add best component identification logic 94 | - [ ] Build the `create_final_optimized_strategy` tool 95 | - [ ] Implement comprehensive testing 96 | 97 | ## Phase 2.4: Integration and User Experience (Week 4) 98 | 99 | 13. [ ] Create end-to-end optimization pipeline 100 | - [ ] Implement the workflow manager 101 | - [ ] Add state persistence between steps 102 | - [ ] Create progress tracking and reporting 103 | - [ ] Build the comprehensive optimization MCP tool 104 | - [ ] Add error recovery mechanisms 105 | 106 | 14. [ ] Develop example strategies and test cases 107 | - [x] Create sample strategy for testing 108 | - [ ] Create more diverse sample strategies 109 | - [ ] Build sample backtest results 110 | - [ ] Implement automated end-to-end tests 111 | - [ ] Generate documentation with examples 112 | - [ ] Create benchmarks for performance testing 113 | 114 | 15. [ ] Implement user interface utilities 115 | - [x] Build command-line interface for LLM workflow 116 | - [ ] Add strategy export/import functionality 117 | - [ ] Create visualizations for optimization results 118 | - [ ] Implement result comparison tools 119 | - [ ] Add user session management 120 | 121 | ## Phase 2.5: Testing and Refinement (Week 5) 122 | 123 | 16. [ ] Comprehensive testing of the full workflow 124 | - [ ] Unit tests for all components 125 | - [ ] Integration tests for LLM interactions 126 | - [ ] End-to-end workflow tests 127 | - [ ] Performance and scalability testing 128 | - [ ] Error handling and recovery testing 129 | 130 | 17. [ ] Documentation and examples 131 | - [ ] Create user guide documentation 132 | - [ ] Update API documentation 133 | - [ ] Create example workflows 134 | - [ ] Add sample strategies and results 135 | - [ ] Build video demonstrations (optional) 136 | 137 | 18. [ ] Final optimization and polish 138 | - [ ] Optimize LLM usage for cost efficiency 139 | - [ ] Refine prompts for better results 140 | - [ ] Enhance error messages and recovery 141 | - [ ] Add configuration options for all components 142 | - [ ] Prepare for production deployment -------------------------------------------------------------------------------- /ProjectPlanPhase2.md: -------------------------------------------------------------------------------- 1 | # Automating Strategy Optimization with LLMs and PineScript MCP 2 | 3 | ## Current Repository Capabilities 4 | 5 | Our MCP server already provides these key functions: 6 | - Syntax validation and error detection 7 | - Automatic fixing of common PineScript errors 8 | - Script formatting for consistency 9 | - Version management between PineScript versions 10 | - Template generation for strategies 11 | 12 | ## LLM-Driven Optimization Workflow 13 | 14 | Here's how we could leverage our existing codebase with LLMs to automate the human workflow of strategy optimization: 15 | 16 | ### 1. Script Analysis with LLM 17 | 18 | ``` 19 | MCP Server → Parse PineScript → LLM Analysis 20 | ``` 21 | 22 | We can develop a new module that: 23 | - Uses our existing PineScript parser/validator to extract the core components of a strategy 24 | - Sends these components to an LLM with structured prompts 25 | - Has the LLM analyze weaknesses, potential improvements, and optimization opportunities 26 | 27 | ```typescript 28 | // Example implementation 29 | async function analyzeStrategyWithLLM(script: string) { 30 | // Use existing validation to ensure script is valid 31 | const validationResult = validatePineScript(script); 32 | if (!validationResult.valid) { 33 | // Use existing error fixer to correct issues first 34 | script = fixPineScriptErrors(script).script; 35 | } 36 | 37 | // Format the script for consistent analysis 38 | script = formatPineScript(script); 39 | 40 | // Send to LLM for analysis with structured prompt 41 | const analysisPrompt = ` 42 | Analyze this PineScript strategy and identify: 43 | 1. Key parameters that could be optimized 44 | 2. Logical weaknesses in entry/exit conditions 45 | 3. Missing risk management components 46 | 4. Opportunities for performance improvement 47 | 48 | Strategy code: 49 | ${script} 50 | 51 | Respond in JSON format with sections for parameters, logic, risk, and performance. 52 | `; 53 | 54 | return await callLLM(analysisPrompt); 55 | } 56 | ``` 57 | 58 | ### 2. Strategy Enhancement Generator 59 | 60 | Using our existing template system as a foundation, we can create a module that: 61 | - Takes the LLM's analysis 62 | - Generates multiple improved versions of the strategy with different enhancements 63 | - Applies our existing formatting and validation to ensure they're valid 64 | 65 | ```typescript 66 | // Example implementation 67 | async function generateEnhancedStrategies(originalScript: string, llmAnalysis: any) { 68 | const enhancementPrompt = ` 69 | Based on this analysis of a PineScript strategy: 70 | ${JSON.stringify(llmAnalysis)} 71 | 72 | Generate 3 different enhanced versions of this original strategy: 73 | ${originalScript} 74 | 75 | 1. Version with improved entry/exit logic 76 | 2. Version with added risk management 77 | 3. Version with optimized parameters 78 | 79 | For each version, explain the changes made and expected improvement. 80 | `; 81 | 82 | const llmResponse = await callLLM(enhancementPrompt); 83 | const enhancedVersions = parseEnhancedVersions(llmResponse); 84 | 85 | // Use our existing validation to ensure all versions are valid 86 | return enhancedVersions.map(version => { 87 | // Validate and fix any syntax issues 88 | const validationResult = validatePineScript(version.script); 89 | if (!validationResult.valid) { 90 | version.script = fixPineScriptErrors(version.script).script; 91 | } 92 | 93 | // Format for consistency 94 | version.script = formatPineScript(version.script); 95 | 96 | return version; 97 | }); 98 | } 99 | ``` 100 | 101 | ### 3. Automated Backtest Result Analysis 102 | 103 | We can create a new MCP tool that: 104 | - Takes backtest results (JSON or screenshot) from TradingView 105 | - Extracts the performance metrics 106 | - Feeds them to an LLM to interpret the results 107 | - Generates recommendations for further improvements 108 | 109 | ```typescript 110 | // Example implementation 111 | async function analyzeTradingViewBacktestResults(backtestResults: any) { 112 | const analysisPrompt = ` 113 | Analyze these TradingView backtest results: 114 | ${JSON.stringify(backtestResults)} 115 | 116 | Identify: 117 | 1. Key strengths in the performance 118 | 2. Areas of concern (high drawdown, low win rate, etc.) 119 | 3. Specific suggestions to address performance issues 120 | 4. Parameters that should be adjusted based on these results 121 | 122 | Respond with actionable recommendations for improving the strategy. 123 | `; 124 | 125 | return await callLLM(analysisPrompt); 126 | } 127 | ``` 128 | 129 | ### 4. Iterative Optimization Pipeline 130 | 131 | Using our batch processing capabilities, we can build an iterative LLM-driven optimization loop: 132 | 133 | ``` 134 | Original Strategy → LLM Analysis → Enhanced Versions → 135 | Backtest Results → LLM Interpretation → Further Enhancements → Final Strategy 136 | ``` 137 | 138 | ```typescript 139 | // Implementing the full workflow 140 | async function optimizeStrategyWithLLM(originalScript: string, tradingPair: string, timeframe: string) { 141 | // Step 1: Initial analysis 142 | const initialAnalysis = await analyzeStrategyWithLLM(originalScript); 143 | 144 | // Step 2: Generate enhanced versions 145 | const enhancedVersions = await generateEnhancedStrategies(originalScript, initialAnalysis); 146 | 147 | // Step 3: User runs backtests and provides results 148 | console.log("Generated enhanced strategy versions. Please run backtests in TradingView and provide results."); 149 | 150 | // Step 4: Analyze backtest results (user would provide these) 151 | const backtestAnalysis = await analyzeTradingViewBacktestResults(userProvidedBacktestResults); 152 | 153 | // Step 5: Generate final optimized version based on backtest feedback 154 | const finalOptimizationPrompt = ` 155 | Based on the backtest results analysis: 156 | ${JSON.stringify(backtestAnalysis)} 157 | 158 | And these previously enhanced versions: 159 | ${JSON.stringify(enhancedVersions)} 160 | 161 | Create a final optimized version of the strategy that incorporates: 162 | 1. The best performing aspects of each enhanced version 163 | 2. Additional improvements based on the backtest results 164 | 3. Fine-tuned parameters based on performance 165 | 166 | Provide the complete PineScript code for the final optimized strategy. 167 | `; 168 | 169 | const finalVersionResponse = await callLLM(finalOptimizationPrompt); 170 | let finalScript = extractScriptFromLLMResponse(finalVersionResponse); 171 | 172 | // Final validation and formatting 173 | const validationResult = validatePineScript(finalScript); 174 | if (!validationResult.valid) { 175 | finalScript = fixPineScriptErrors(finalScript).script; 176 | } 177 | finalScript = formatPineScript(finalScript); 178 | 179 | return { 180 | originalScript, 181 | enhancedVersions, 182 | backtestAnalysis, 183 | finalScript, 184 | optimizationNotes: extractOptimizationNotes(finalVersionResponse) 185 | }; 186 | } 187 | ``` 188 | 189 | ## MCP Server Integration 190 | 191 | To implement this workflow in our existing MCP server: 192 | 193 | 1. Add new LLM integration module: 194 | ```typescript 195 | // src/llm/llmService.ts 196 | export async function callLLM(prompt: string, options?: LLMOptions): Promise { 197 | // Integration with an LLM service (OpenAI, Anthropic, etc.) 198 | } 199 | ``` 200 | 201 | 2. Create strategy analysis tool: 202 | ```typescript 203 | // Add to index.ts 204 | mcp.addTool({ 205 | name: 'analyze_strategy', 206 | description: 'Analyze a PineScript strategy with LLM and suggest improvements', 207 | parameters: z.object({ 208 | script: z.string().describe('The PineScript strategy to analyze'), 209 | trading_pair: z.string().describe('The trading pair/symbol').optional(), 210 | timeframe: z.string().describe('The timeframe (1h, 4h, 1d, etc.)').optional() 211 | }), 212 | execute: async (params) => { 213 | const analysis = await analyzeStrategyWithLLM(params.script); 214 | return JSON.stringify(analysis); 215 | } 216 | }); 217 | ``` 218 | 219 | 3. Add enhancement generator tool: 220 | ```typescript 221 | mcp.addTool({ 222 | name: 'generate_enhanced_strategies', 223 | description: 'Generate multiple enhanced versions of a PineScript strategy', 224 | parameters: z.object({ 225 | script: z.string().describe('The original PineScript strategy'), 226 | analysis: z.string().describe('Strategy analysis from analyze_strategy tool') 227 | }), 228 | execute: async (params) => { 229 | const enhancedVersions = await generateEnhancedStrategies( 230 | params.script, 231 | JSON.parse(params.analysis) 232 | ); 233 | return JSON.stringify(enhancedVersions); 234 | } 235 | }); 236 | ``` 237 | 238 | 4. Add backtest analysis tool: 239 | ```typescript 240 | mcp.addTool({ 241 | name: 'analyze_backtest_results', 242 | description: 'Analyze TradingView backtest results and suggest further improvements', 243 | parameters: z.object({ 244 | backtest_results: z.string().describe('JSON representation of backtest results'), 245 | strategy_script: z.string().describe('The strategy that was backtested') 246 | }), 247 | execute: async (params) => { 248 | const analysis = await analyzeTradingViewBacktestResults( 249 | JSON.parse(params.backtest_results), 250 | params.strategy_script 251 | ); 252 | return JSON.stringify(analysis); 253 | } 254 | }); 255 | ``` 256 | 257 | 5. Add final optimization tool: 258 | ```typescript 259 | mcp.addTool({ 260 | name: 'create_final_optimized_strategy', 261 | description: 'Create final optimized strategy based on backtest results', 262 | parameters: z.object({ 263 | original_script: z.string().describe('The original strategy'), 264 | enhanced_versions: z.string().describe('Enhanced versions from generate_enhanced_strategies'), 265 | backtest_analysis: z.string().describe('Analysis from analyze_backtest_results') 266 | }), 267 | execute: async (params) => { 268 | const finalVersion = await createFinalOptimizedStrategy( 269 | params.original_script, 270 | JSON.parse(params.enhanced_versions), 271 | JSON.parse(params.backtest_analysis) 272 | ); 273 | return JSON.stringify(finalVersion); 274 | } 275 | }); 276 | ``` 277 | 278 | ## User Workflow Example 279 | 280 | 1. User uploads their TradingView strategy to our MCP tool 281 | 2. System validates and formats the script using existing functionality 282 | 3. LLM analyzes the strategy and identifies areas for improvement 283 | 4. System generates 3-5 enhanced versions with different improvements 284 | 5. User runs these in TradingView and shares backtest results 285 | 6. LLM analyzes backtest results and recommends further refinements 286 | 7. System generates a final optimized strategy incorporating the best elements 287 | 8. User gets a strategy that's been intelligently optimized with LLM insights 288 | 289 | This workflow leverages our existing syntax validation and formatting tools while adding LLM intelligence to automate the creative aspects of strategy optimization that humans typically perform. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TradingView PineScript MCP Server 2 | 3 | A Model Context Protocol (MCP) server for working with TradingView PineScript. This server provides tools for validating, fixing, and generating PineScript code through a standardized API. 4 | 5 | ## Features 6 | 7 | - **PineScript Validation** - Automatically validates PineScript code for syntax errors and warnings 8 | - **Error Fixing** - Automatically fixes common PineScript syntax errors 9 | - **Template Generation** - Provides validated templates for various PineScript strategies and indicators 10 | 11 | ## Getting Started 12 | 13 | ### Prerequisites 14 | 15 | - Node.js 16.x or higher 16 | - npm 8.x or higher 17 | 18 | ### Installation 19 | 20 | 1. Clone the repository 21 | ```bash 22 | git clone https://github.com/yourusername/pinescriptproject1.git 23 | cd pinescriptproject1 24 | ``` 25 | 26 | 2. Install dependencies 27 | ```bash 28 | npm install 29 | ``` 30 | 31 | 3. Build the project 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | ### Running the Server 37 | 38 | Start the MCP server: 39 | ```bash 40 | npm run start-server 41 | ``` 42 | 43 | This will start the server with stdio transport, which allows it to communicate with MCP clients. 44 | 45 | ## API 46 | 47 | ### Tools 48 | 49 | The MCP server exposes the following tools: 50 | 51 | #### 1. `validate_pinescript` 52 | 53 | Validates PineScript code for syntax errors and warnings. 54 | 55 | **Parameters:** 56 | - `script` (string): The PineScript code to validate 57 | - `version` (string, optional): Expected PineScript version (e.g., 'v5', 'v4') 58 | 59 | **Returns:** 60 | - `valid` (boolean): Whether the script is valid 61 | - `errors` (string[]): List of syntax errors 62 | - `warnings` (string[]): List of warnings 63 | 64 | #### 2. `fix_pinescript_errors` 65 | 66 | Automatically fixes common syntax errors in PineScript code. 67 | 68 | **Parameters:** 69 | - `script` (string): The PineScript code to fix 70 | 71 | **Returns:** 72 | - `fixed` (boolean): Whether any fixes were applied 73 | - `fixedCode` (string): The fixed script 74 | - `changes` (string[]): List of changes made 75 | 76 | #### 3. `get_pinescript_template` 77 | 78 | Provides validated templates for common PineScript strategies and indicators. 79 | 80 | **Parameters:** 81 | - `template_type` (string): Type of template ('strategy' or 'indicator') 82 | - `name` (string): Template name 83 | 84 | **Returns:** 85 | - `template` (string): The template code 86 | - `message` (string): Confirmation message 87 | 88 | ## Supported PineScript Features 89 | 90 | ### Syntax Validation 91 | 92 | The validator currently checks for: 93 | - Empty scripts 94 | - Version annotations 95 | - Balanced parentheses 96 | - String quotes 97 | - Comma placement in function calls 98 | - Deprecated functions in PineScript v5 99 | - Incorrect variable export syntax 100 | 101 | ### Error Fixing 102 | 103 | The fixer can automatically fix: 104 | - Missing version annotations 105 | - Unbalanced parentheses 106 | - Unclosed string literals 107 | - Missing commas in function calls 108 | - Deprecated study() function 109 | - Incorrect variable export syntax 110 | 111 | ## Development 112 | 113 | ### Running Tests 114 | 115 | ```bash 116 | npm test 117 | ``` 118 | 119 | ### Debugging 120 | 121 | To run the server in development mode with automatic reloading: 122 | ```bash 123 | npm run dev 124 | ``` 125 | 126 | ## License 127 | 128 | ISC 129 | 130 | ## Contributing 131 | 132 | Contributions are welcome! -------------------------------------------------------------------------------- /docs/PinescriptProject1Plan.md: -------------------------------------------------------------------------------- 1 | I'll research Cursor's latest functionality related to project rules and MCP servers, then create a detailed project plan. 2 | 3 | # TradingView PineScript MCP Server for Cursor - Project Plan 4 | 5 | ## Project Overview 6 | 7 | This project aims to create a Model Context Protocol (MCP) server that connects Claude to TradingView's PineScript, enabling seamless natural language interaction for strategy development and testing. The MCP server will validate, troubleshoot, and improve PineScript code, significantly reducing the trial-and-error cycle when developing trading strategies. 8 | 9 | ## Background and Justification 10 | 11 | Trading strategy development in TradingView's PineScript often involves a frustrating trial-and-error process. Claude can generate PineScript code, but syntax errors are common due to: 12 | 13 | 1. PineScript's unique syntax and evolving versions 14 | 2. Limited documentation accessibility during generation 15 | 3. No direct validation before testing in TradingView 16 | 17 | This MCP server will address these issues by providing a streamlined workflow for strategy development, syntax validation, and error correction. 18 | 19 | ## Technical Approach 20 | 21 | Based on Cursor's latest MCP implementation and project rules system, we'll develop a TypeScript-based MCP server that includes: 22 | 23 | 1. **PineScript Validator**: A module to check syntax before code is sent to TradingView 24 | 2. **Error Correction Engine**: Automated fixes for common PineScript errors 25 | 3. **Template System**: Pre-validated strategy templates for common trading patterns 26 | 4. **Version Management**: Support for different PineScript versions (v4, v5, v6) 27 | 28 | ## Functional Requirements 29 | 30 | The MCP server will provide the following tools to Cursor: 31 | 32 | 1. **validate_pinescript**: Validates PineScript code without sending to TradingView 33 | 2. **fix_pinescript_errors**: Fixes common syntax errors based on error messages 34 | 3. **get_pinescript_template**: Provides validated templates for common strategies 35 | 4. **analyze_error_message**: Interprets TradingView error messages and suggests fixes 36 | 5. **format_pinescript**: Formats code according to best practices 37 | 6. **save_script_version**: Maintains version history for iterative development 38 | 39 | ## Technical Architecture 40 | 41 | ``` 42 | tradingview-mcp/ 43 | ├── src/ 44 | │ ├── index.ts # Main entry point 45 | │ ├── validators/ # PineScript validation logic 46 | │ │ ├── syntaxValidator.ts # Basic syntax validation 47 | │ │ └── versionValidator.ts # Version-specific validation 48 | │ ├── fixers/ # Error correction modules 49 | │ │ ├── commonFixes.ts # Common error patterns and fixes 50 | │ │ └── errorAnalyzer.ts # Error message analysis 51 | │ ├── templates/ # PineScript templates 52 | │ │ ├── strategies/ # Trading strategy templates 53 | │ │ └── indicators/ # Technical indicator templates 54 | │ └── utils/ # Utility functions 55 | │ ├── versionDetector.ts # Detect PineScript version 56 | │ └── formatter.ts # Code formatting 57 | ├── tests/ # Test suite 58 | ├── examples/ # Example scripts and usage patterns 59 | ├── package.json # Dependencies and scripts 60 | └── README.md # Documentation 61 | ``` 62 | 63 | ## Implementation Plan 64 | 65 | ### Phase 1: Core Framework (Week 1) 66 | - Set up the TypeScript project structure 67 | - Implement the basic MCP server framework 68 | - Create the validation architecture 69 | - Develop basic template system 70 | 71 | ### Phase 2: Validation & Error Handling (Week 2) 72 | - Implement syntax validation for PineScript v5/v6 73 | - Create error pattern detection system 74 | - Develop common fixes for syntax errors 75 | - Build error analysis tool 76 | 77 | ### Phase 3: Templates & Integration (Week 3) 78 | - Create a library of validated strategy templates 79 | - Implement version management 80 | - Integrate with Cursor's MCP system 81 | - Add configuration options 82 | 83 | ### Phase 4: Testing & Refinement (Week 4) 84 | - Comprehensive testing with real-world PineScript examples 85 | - Performance optimization 86 | - Documentation 87 | - User guide creation 88 | 89 | ## Technical Details 90 | 91 | ### MCP Server Implementation 92 | 93 | The MCP server will be built using TypeScript with the following key components: 94 | 95 | ```typescript 96 | // Example core functionality in index.ts 97 | import { FastMCP, Context } from 'fastmcp'; 98 | 99 | // Initialize MCP server 100 | const mcp = new FastMCP('TradingView PineScript MCP'); 101 | 102 | // Validator tool 103 | mcp.tool({ 104 | name: 'validate_pinescript', 105 | description: 'Validates PineScript code for syntax errors', 106 | parameters: { 107 | script: { 108 | type: 'string', 109 | description: 'PineScript code to validate' 110 | }, 111 | version: { 112 | type: 'string', 113 | description: 'PineScript version (v4, v5, v6)', 114 | required: false, 115 | default: 'v5' 116 | } 117 | }, 118 | handler: async (params, ctx) => { 119 | const { script, version } = params; 120 | // Validation logic will be implemented here 121 | return { /* validation results */ }; 122 | } 123 | }); 124 | 125 | // Error fixer tool 126 | mcp.tool({ 127 | name: 'fix_pinescript_errors', 128 | description: 'Automatically fixes common PineScript syntax errors', 129 | parameters: { 130 | script: { 131 | type: 'string', 132 | description: 'PineScript code with errors' 133 | }, 134 | error_message: { 135 | type: 'string', 136 | description: 'Error message from TradingView' 137 | } 138 | }, 139 | handler: async (params, ctx) => { 140 | const { script, error_message } = params; 141 | // Error fixing logic will be implemented here 142 | return { /* fixed script */ }; 143 | } 144 | }); 145 | 146 | // Additional tools will be implemented similarly 147 | ``` 148 | 149 | ### Cursor Configuration 150 | 151 | The MCP server will be configured in Cursor through the MCP settings: 152 | 153 | ```json 154 | { 155 | "mcpServers": { 156 | "tradingview-pinescript": { 157 | "command": "npx", 158 | "args": [ 159 | "-y", 160 | "tradingview-pinescript-mcp" 161 | ] 162 | } 163 | } 164 | } 165 | ``` 166 | 167 | ## Key Features and Benefits 168 | 169 | 1. **Instant Validation**: Immediately catch syntax errors before copying to TradingView 170 | 2. **Automatic Error Correction**: Suggestions and fixes for common PineScript errors 171 | 3. **Version Management**: Support for different PineScript versions and features 172 | 4. **Template Library**: Access to pre-validated strategy templates 173 | 5. **Progressive Refinement**: Iterative improvement of strategies with version history 174 | 6. **Seamless Integration**: Works directly in Cursor's editor and chat interface 175 | 176 | ## Expected Challenges and Mitigations 177 | 178 | 1. **PineScript Evolution**: Regular updates to validation rules as TradingView updates PineScript 179 | - *Mitigation*: Modular design for easy updates to validation rules 180 | 181 | 2. **Error Pattern Coverage**: Impossible to cover all possible error patterns 182 | - *Mitigation*: Focus on most common errors, provide framework for user-contributed patterns 183 | 184 | 3. **Performance**: Validation of complex scripts may be resource-intensive 185 | - *Mitigation*: Incremental validation, caching mechanisms 186 | 187 | 4. **Integration Limitations**: MCP protocol may have constraints 188 | - *Mitigation*: Design for core functionality first, with optional advanced features 189 | 190 | ## Success Metrics 191 | 192 | The project will be considered successful if: 193 | 194 | 1. 80% of common PineScript syntax errors are caught before sending to TradingView 195 | 2. Strategy development time is reduced by at least 50% 196 | 3. Users can iterate through strategy improvements with minimal manual error handling 197 | 4. The MCP server can be easily installed and configured in Cursor 198 | 199 | ## Future Enhancements 200 | 201 | 1. **Direct TradingView API Integration**: If TradingView provides an official API 202 | 2. **Strategy Performance Analysis**: Evaluate strategy effectiveness without manual testing 203 | 3. **Community Templates**: Repository of user-contributed templates 204 | 4. **AI-Generated Improvements**: Suggest performance optimizations 205 | 5. **Multi-Timeframe Testing**: Test strategies across different timeframes automatically 206 | 207 | ## Project Dependencies 208 | 209 | 1. **TypeScript/Node.js**: Development environment 210 | 2. **fastmcp**: MCP server implementation library 211 | 3. **Cursor IDE**: Target platform with MCP support 212 | 4. **PineScript Documentation**: For validation rule development 213 | 214 | ## Conclusion 215 | 216 | This TradingView PineScript MCP server for Cursor will dramatically improve the experience of developing trading strategies using Claude and TradingView. By reducing the friction in testing and iterating on Claude-generated PineScript strategies, it will enable traders to focus on strategy development rather than syntax debugging. 217 | 218 | The project leverages Cursor's latest MCP capabilities and project rules system to create a powerful bridge between natural language interaction and PineScript development, making algorithmic trading more accessible to traders of all technical skill levels. -------------------------------------------------------------------------------- /docs/TaskList1.md: -------------------------------------------------------------------------------- 1 | # TradingView PineScript MCP Server - Sequential Task List 2 | 3 | ## Phase 1: Core Framework Setup (Week 1) 4 | 1. ~~Initialize TypeScript project with necessary dependencies~~ 5 | 2. ~~Create basic MCP server structure following fastmcp patterns~~ 6 | 3. ~~Implement server entry point (index.ts)~~ 7 | 4. ~~Set up project directory structure for validators, fixers, templates~~ 8 | 5. ~~Develop basic validation architecture~~ 9 | 6. ~~Create simple template system foundation~~ 10 | 7. ~~Write initial documentation~~ 11 | 8. ~~Set up testing framework~~ 12 | 13 | ## Phase 2: Validation & Error Handling (Week 2) 14 | 9. ~~Implement PineScript v5 syntax validation~~ 15 | 10. ~~Extend validation for PineScript v6 support~~ 16 | 11. ~~Create version detection utility~~ 17 | 12. ~~Build error pattern detection system~~ 18 | 13. ~~Develop common syntax error fixes~~ 19 | 14. ~~Implement error message analyzer~~ 20 | 15. ~~Create error correction suggestion system~~ 21 | 16. ~~Test validation system with sample scripts~~ 22 | 23 | ## Phase 3: Templates & Integration (Week 3) 24 | 17. ~~Build template management system~~ 25 | 18. ~~Create library of basic strategy templates~~ 26 | 19. ~~Implement indicator templates~~ 27 | 20. ~~Develop version management system~~ 28 | 21. ~~Configure Cursor MCP integration~~ 29 | 22. ~~Add user configuration options~~ 30 | 23. ~~Implement script formatting utility~~ 31 | 24. ~~Create version history tracking~~ 32 | 33 | ## Phase 4: Testing & Refinement (Week 4) 34 | 25. ~~Develop comprehensive test suite~~ 35 | 26. ~~Test with real-world PineScript examples~~ 36 | 27. Optimize performance 37 | 28. Fix identified issues 38 | 29. ~~Complete user documentation~~ 39 | 30. Create installation guide 40 | 31. Develop usage examples 41 | 32. Package for distribution 42 | 43 | ## Final Steps 44 | 33. Review project against requirements 45 | 34. Final testing in Cursor environment 46 | 35. Release initial version 47 | 36. Document future enhancement possibilities -------------------------------------------------------------------------------- /enhanced/simple_strategy_enhanced_1.pine: -------------------------------------------------------------------------------- 1 | // Enhanced strategy 1 2 | //@version=5 3 | strategy("Simple Moving Average Crossover", overlay=true) 4 | 5 | // Input parameters 6 | fastLength = input(9, "Fast MA Length") 7 | slowLength = input(21, "Slow MA Length") 8 | takeProfitPct = input.float(2.0, "Take Profit %", minval=0.1, step=0.1) / 100 9 | stopLossPct = input.float(1.0, "Stop Loss %", minval=0.1, step=0.1) / 100 10 | 11 | // Calculate moving averages 12 | fastMA = ta.sma(close, fastLength) 13 | slowMA = ta.sma(close, slowLength) 14 | 15 | // Generate signals 16 | longCondition = ta.crossover(fastMA, slowMA) 17 | shortCondition = ta.crossunder(fastMA, slowMA) 18 | 19 | // Plot indicators 20 | plot(fastMA, "Fast MA", color=color.blue) 21 | plot(slowMA, "Slow MA", color=color.red) 22 | 23 | // Execute strategy 24 | if (longCondition) 25 | strategy.entry("Long", strategy.long) 26 | 27 | if (shortCondition) 28 | strategy.close("Long") 29 | 30 | // Set take profit and stop loss 31 | strategy.exit("Take Profit / Stop Loss", "Long", 32 | profit=strategy.position_avg_price * takeProfitPct, 33 | loss=strategy.position_avg_price * stopLossPct) 34 | 35 | // Plot buy/sell signals 36 | plotshape(longCondition, "Buy Signal", shape.triangleup, location.belowbar, color.green, size=size.small) 37 | plotshape(shortCondition, "Sell Signal", shape.triangledown, location.abovebar, color.red, size=size.small) 38 | // with improvements -------------------------------------------------------------------------------- /enhanced/simple_strategy_enhanced_2.pine: -------------------------------------------------------------------------------- 1 | // Enhanced strategy 2 2 | //@version=5 3 | strategy("Simple Moving Average Crossover", overlay=true) 4 | 5 | // Input parameters 6 | fastLength = input(9, "Fast MA Length") 7 | slowLength = input(21, "Slow MA Length") 8 | takeProfitPct = input.float(2.0, "Take Profit %", minval=0.1, step=0.1) / 100 9 | stopLossPct = input.float(1.0, "Stop Loss %", minval=0.1, step=0.1) / 100 10 | 11 | // Calculate moving averages 12 | fastMA = ta.sma(close, fastLength) 13 | slowMA = ta.sma(close, slowLength) 14 | 15 | // Generate signals 16 | longCondition = ta.crossover(fastMA, slowMA) 17 | shortCondition = ta.crossunder(fastMA, slowMA) 18 | 19 | // Plot indicators 20 | plot(fastMA, "Fast MA", color=color.blue) 21 | plot(slowMA, "Slow MA", color=color.red) 22 | 23 | // Execute strategy 24 | if (longCondition) 25 | strategy.entry("Long", strategy.long) 26 | 27 | if (shortCondition) 28 | strategy.close("Long") 29 | 30 | // Set take profit and stop loss 31 | strategy.exit("Take Profit / Stop Loss", "Long", 32 | profit=strategy.position_avg_price * takeProfitPct, 33 | loss=strategy.position_avg_price * stopLossPct) 34 | 35 | // Plot buy/sell signals 36 | plotshape(longCondition, "Buy Signal", shape.triangleup, location.belowbar, color.green, size=size.small) 37 | plotshape(shortCondition, "Sell Signal", shape.triangledown, location.abovebar, color.red, size=size.small) 38 | // with improvements -------------------------------------------------------------------------------- /examples/Example_PineScriptStrat1_1hr.txt: -------------------------------------------------------------------------------- 1 | //@version=6 2 | strategy("Gold Scalping BOS & CHoCH", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=1) 3 | 4 | // Support & Resistance Levels 5 | recentLow = ta.lowest(low, 10) 6 | recentHigh = ta.highest(high, 10) 7 | 8 | // Break of Structure (BOS) with Confirmation 9 | var float lastSwingHigh = na 10 | var float lastSwingLow = na 11 | bosBullish = false 12 | bosBearish = false 13 | 14 | lastSwingHigh := ta.highest(high[1], 5) 15 | bosBullish := high > lastSwingHigh 16 | 17 | lastSwingLow := ta.lowest(low[1], 5) 18 | bosBearish := low < lastSwingLow 19 | 20 | // Change of Character (CHoCH) with Immediate Reaction 21 | chochBullish = bosBearish and ta.crossover(close, lastSwingLow) 22 | chochBearish = bosBullish and ta.crossunder(close, lastSwingHigh) 23 | 24 | // Buy Entry Conditions (Refined for Early Entries) 25 | buyCondition = bosBullish and chochBullish 26 | 27 | // Sell Entry Conditions (Refined for Early Entries) 28 | sellCondition = bosBearish and chochBearish 29 | 30 | // Stop Loss & Take Profit (Dynamic and Adaptive) 31 | longSL = recentLow 32 | shortSL = recentHigh 33 | longTP = close + ((close - longSL) * 2) 34 | shortTP = close - ((shortSL - close) * 2) 35 | 36 | // Ensure SL and TP are valid before executing trades 37 | validLongTrade = buyCondition and (longSL < close) 38 | validShortTrade = sellCondition and (shortSL > close) 39 | 40 | // Execute Trades 41 | if validLongTrade 42 | strategy.entry("Long", strategy.long) 43 | strategy.exit("Exit Long", from_entry="Long", stop=longSL, limit=longTP) 44 | 45 | if validShortTrade 46 | strategy.entry("Short", strategy.short) 47 | strategy.exit("Exit Short", from_entry="Short", stop=shortSL, limit=shortTP) 48 | 49 | // Plot Buy/Sell Signals 50 | plotshape(series=buyCondition, location=location.belowbar, color=color.green, style=shape.labelup, title="BUY") 51 | plotshape(series=sellCondition, location=location.abovebar, color=color.red, style=shape.labeldown, title="SELL") 52 | 53 | // Debugging: Show swing high/low levels 54 | plot(lastSwingHigh, title="Last Swing High", color=color.blue, linewidth=2, style=plot.style_linebr) 55 | plot(lastSwingLow, title="Last Swing Low", color=color.orange, linewidth=2, style=plot.style_linebr) -------------------------------------------------------------------------------- /examples/Modified_GoldScalping_Strategy.txt: -------------------------------------------------------------------------------- 1 | //@version=6 2 | strategy("Gold Scalping BOS & CHoCH with MA Filter", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=1) 3 | 4 | // Support & Resistance Levels 5 | recentLow = ta.lowest(low, 10) 6 | recentHigh = ta.highest(high, 10) 7 | 8 | // Moving Average Trend Filter 9 | fastMA = ta.ema(close, 20) 10 | slowMA = ta.ema(close, 50) 11 | uptrend = fastMA > slowMA 12 | downtrend = fastMA < slowMA 13 | 14 | // Break of Structure (BOS) with Confirmation 15 | var float lastSwingHigh = na 16 | var float lastSwingLow = na 17 | bosBullish = false 18 | bosBearish = false 19 | 20 | lastSwingHigh := ta.highest(high[1], 5) 21 | bosBullish := high > lastSwingHigh 22 | 23 | lastSwingLow := ta.lowest(low[1], 5) 24 | bosBearish := low < lastSwingLow 25 | 26 | // Change of Character (CHoCH) with Immediate Reaction 27 | chochBullish = bosBearish and ta.crossover(close, lastSwingLow) 28 | chochBearish = bosBullish and ta.crossunder(close, lastSwingHigh) 29 | 30 | // Buy Entry Conditions (Refined for Early Entries with MA Filter) 31 | buyCondition = bosBullish and chochBullish and uptrend 32 | 33 | // Sell Entry Conditions (Refined for Early Entries with MA Filter) 34 | sellCondition = bosBearish and chochBearish and downtrend 35 | 36 | // Stop Loss & Take Profit (Dynamic and Adaptive) 37 | longSL = recentLow 38 | shortSL = recentHigh 39 | longTP = close + ((close - longSL) * 2) 40 | shortTP = close - ((shortSL - close) * 2) 41 | 42 | // Ensure SL and TP are valid before executing trades 43 | validLongTrade = buyCondition and (longSL < close) 44 | validShortTrade = sellCondition and (shortSL > close) 45 | 46 | // Execute Trades 47 | if validLongTrade 48 | strategy.entry("Long", strategy.long) 49 | strategy.exit("Exit Long", from_entry="Long", stop=longSL, limit=longTP) 50 | 51 | if validShortTrade 52 | strategy.entry("Short", strategy.short) 53 | strategy.exit("Exit Short", from_entry="Short", stop=shortSL, limit=shortTP) 54 | 55 | // Plot Buy/Sell Signals 56 | plotshape(series=buyCondition, location=location.belowbar, color=color.green, style=shape.labelup, title="BUY") 57 | plotshape(series=sellCondition, location=location.abovebar, color=color.red, style=shape.labeldown, title="SELL") 58 | 59 | // Debugging: Show swing high/low levels 60 | plot(lastSwingHigh, title="Last Swing High", color=color.blue, linewidth=2, style=plot.style_linebr) 61 | plot(lastSwingLow, title="Last Swing Low", color=color.orange, linewidth=2, style=plot.style_linebr) 62 | 63 | // Plot Moving Averages 64 | plot(fastMA, title="Fast EMA", color=color.green, linewidth=1) 65 | plot(slowMA, title="Slow EMA", color=color.red, linewidth=1) -------------------------------------------------------------------------------- /examples/Modified_GoldScalping_Strategy_With_Volume.txt: -------------------------------------------------------------------------------- 1 | //@version=6 2 | strategy("Gold Scalping BOS & CHoCH with MA & Volume Filter", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=1) 3 | 4 | // Support & Resistance Levels 5 | recentLow = ta.lowest(low, 10) 6 | recentHigh = ta.highest(high, 10) 7 | 8 | // Moving Average Trend Filter 9 | fastMA = ta.ema(close, 20) 10 | slowMA = ta.ema(close, 50) 11 | uptrend = fastMA > slowMA 12 | downtrend = fastMA < slowMA 13 | 14 | // Volume Filter 15 | volumeMA = ta.sma(volume, 20) 16 | highVolume = volume > volumeMA * 1.2 // Volume at least 20% above the average 17 | 18 | // Break of Structure (BOS) with Confirmation 19 | var float lastSwingHigh = na 20 | var float lastSwingLow = na 21 | bosBullish = false 22 | bosBearish = false 23 | 24 | lastSwingHigh := ta.highest(high[1], 5) 25 | bosBullish := high > lastSwingHigh 26 | 27 | lastSwingLow := ta.lowest(low[1], 5) 28 | bosBearish := low < lastSwingLow 29 | 30 | // Change of Character (CHoCH) with Immediate Reaction 31 | chochBullish = bosBearish and ta.crossover(close, lastSwingLow) 32 | chochBearish = bosBullish and ta.crossunder(close, lastSwingHigh) 33 | 34 | // Buy Entry Conditions (with Volume Filter) 35 | buyCondition = bosBullish and chochBullish and uptrend and highVolume 36 | 37 | // Sell Entry Conditions (with Volume Filter) 38 | sellCondition = bosBearish and chochBearish and downtrend and highVolume 39 | 40 | // Stop Loss & Take Profit (Dynamic and Adaptive) 41 | longSL = recentLow 42 | shortSL = recentHigh 43 | longTP = close + ((close - longSL) * 2) 44 | shortTP = close - ((shortSL - close) * 2) 45 | 46 | // Ensure SL and TP are valid before executing trades 47 | validLongTrade = buyCondition and (longSL < close) 48 | validShortTrade = sellCondition and (shortSL > close) 49 | 50 | // Execute Trades 51 | if validLongTrade 52 | strategy.entry("Long", strategy.long) 53 | strategy.exit("Exit Long", from_entry="Long", stop=longSL, limit=longTP) 54 | 55 | if validShortTrade 56 | strategy.entry("Short", strategy.short) 57 | strategy.exit("Exit Short", from_entry="Short", stop=shortSL, limit=shortTP) 58 | 59 | // Plot Buy/Sell Signals 60 | plotshape(series=buyCondition, location=location.belowbar, color=color.green, style=shape.labelup, title="BUY") 61 | plotshape(series=sellCondition, location=location.abovebar, color=color.red, style=shape.labeldown, title="SELL") 62 | 63 | // Debugging: Show swing high/low levels 64 | plot(lastSwingHigh, title="Last Swing High", color=color.blue, linewidth=2, style=plot.style_linebr) 65 | plot(lastSwingLow, title="Last Swing Low", color=color.orange, linewidth=2, style=plot.style_linebr) 66 | 67 | // Plot Moving Averages 68 | plot(fastMA, title="Fast EMA", color=color.green, linewidth=1) 69 | plot(slowMA, title="Slow EMA", color=color.red, linewidth=1) 70 | 71 | // Plot Volume 72 | hline(0, "Zero Line", color=color.gray, linestyle=hline.style_dotted) 73 | plot(ta.change(volume) / volumeMA * 100, title="Volume Change %", color=color.purple, style=plot.style_columns, histbase=0) -------------------------------------------------------------------------------- /examples/basic_validation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic PineScript Validation Example 3 | * 4 | * This example demonstrates how to use the PineScript MCP server to validate code. 5 | */ 6 | 7 | // Sample PineScript code for a simple strategy 8 | const pineScriptCode = ` 9 | //@version=5 10 | strategy("My Strategy", overlay=true) 11 | 12 | // Input parameters 13 | fastLength = input(12, "Fast Length") 14 | slowLength = input(26, "Slow Length") 15 | 16 | // Calculate indicators 17 | fastMA = ta.sma(close, fastLength) 18 | slowMA = ta.sma(close, slowLength) 19 | 20 | // Define trading conditions 21 | longCondition = ta.crossover(fastMA, slowMA) 22 | shortCondition = ta.crossunder(fastMA, slowMA) 23 | 24 | // Execute strategy 25 | if (longCondition) 26 | strategy.entry("Long", strategy.long) 27 | 28 | if (shortCondition) 29 | strategy.entry("Short", strategy.short) 30 | 31 | // Plot indicators 32 | plot(fastMA, "Fast MA", color.blue) 33 | plot(slowMA, "Slow MA", color.red) 34 | `; 35 | 36 | // Sample code with an error 37 | const pineScriptWithError = ` 38 | //@version=5 39 | strategy("My Strategy", overlay=true) 40 | 41 | // Input parameters 42 | fastLength = input(12, "Fast Length") 43 | slowLength = input(26, "Slow Length") 44 | 45 | // Calculate indicators 46 | fastMA = ta.sma(close, fastLength) 47 | slowMA = ta.sma(close, slowLength) 48 | 49 | // Define trading conditions 50 | longCondition = ta.crossover(fastMA, slowMA 51 | shortCondition = ta.crossunder(fastMA, slowMA) 52 | 53 | // Execute strategy 54 | if (longCondition) 55 | strategy.entry("Long", strategy.long) 56 | 57 | if (shortCondition) 58 | strategy.entry("Short", strategy.short) 59 | 60 | // Plot indicators 61 | plot(fastMA, "Fast MA", color.blue) 62 | plot(slowMA, "Slow MA", color.red) 63 | `; 64 | 65 | // Example of how to use the validation 66 | console.log("Validating correct PineScript code..."); 67 | // In a real implementation, you would call the MCP server here 68 | console.log("Sample code would be validated by MCP server"); 69 | 70 | console.log("\nValidating PineScript code with syntax error..."); 71 | // In a real implementation, you would call the MCP server here 72 | console.log("Sample code with error would be validated by MCP server"); 73 | 74 | // Example of using the template functionality 75 | console.log("\nGetting a strategy template..."); 76 | // In a real implementation, you would call the MCP server here 77 | console.log("MCP server would return a strategy template"); -------------------------------------------------------------------------------- /examples/simple_strategy.pine: -------------------------------------------------------------------------------- 1 | //@version=5 2 | strategy("Simple Moving Average Crossover", overlay=true) 3 | 4 | // Input parameters 5 | fastLength = input(9, "Fast MA Length") 6 | slowLength = input(21, "Slow MA Length") 7 | takeProfitPct = input.float(2.0, "Take Profit %", minval=0.1, step=0.1) / 100 8 | stopLossPct = input.float(1.0, "Stop Loss %", minval=0.1, step=0.1) / 100 9 | 10 | // Calculate moving averages 11 | fastMA = ta.sma(close, fastLength) 12 | slowMA = ta.sma(close, slowLength) 13 | 14 | // Generate signals 15 | longCondition = ta.crossover(fastMA, slowMA) 16 | shortCondition = ta.crossunder(fastMA, slowMA) 17 | 18 | // Plot indicators 19 | plot(fastMA, "Fast MA", color=color.blue) 20 | plot(slowMA, "Slow MA", color=color.red) 21 | 22 | // Execute strategy 23 | if (longCondition) 24 | strategy.entry("Long", strategy.long) 25 | 26 | if (shortCondition) 27 | strategy.close("Long") 28 | 29 | // Set take profit and stop loss 30 | strategy.exit("Take Profit / Stop Loss", "Long", 31 | profit=strategy.position_avg_price * takeProfitPct, 32 | loss=strategy.position_avg_price * stopLossPct) 33 | 34 | // Plot buy/sell signals 35 | plotshape(longCondition, "Buy Signal", shape.triangleup, location.belowbar, color.green, size=size.small) 36 | plotshape(shortCondition, "Sell Signal", shape.triangledown, location.abovebar, color.red, size=size.small) -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | extensionsToTreatAsEsm: ['.ts'], 6 | transform: { 7 | '^.+\\.tsx?$': [ 8 | 'ts-jest', 9 | { 10 | useESM: true, 11 | }, 12 | ], 13 | }, 14 | moduleNameMapper: { 15 | '^(\\.{1,2}/.*)\\.js$': '$1', 16 | }, 17 | }; -------------------------------------------------------------------------------- /memory-bank/activeContext.md: -------------------------------------------------------------------------------- 1 | # Active Context 2 | 3 | ## Current Focus 4 | 5 | We are currently implementing the **LLM integration** for strategy analysis and optimization. The primary goal is to leverage AI capabilities to enhance PineScript trading strategies through automated analysis and improvement suggestions. 6 | 7 | ### Recently Completed 8 | 9 | - Added LLM configuration in the user config system 10 | - Created a service architecture for interacting with language models (OpenAI, Anthropic) 11 | - Implemented a mock provider for development and testing 12 | - Built CLI commands for strategy analysis and enhancement 13 | - Created prompt templates for different types of strategy analysis 14 | 15 | ### Active Work 16 | 17 | We have completed the initial implementation of the LLM integration with the following components: 18 | 19 | 1. **User Configuration for LLM** 20 | - Added new configuration section for LLM providers and settings 21 | - Created helper functions to update LLM-specific settings 22 | - Provided default configuration values for development 23 | 24 | 2. **LLM Service Architecture** 25 | - Created a service that selects the appropriate provider based on configuration 26 | - Designed interfaces for strategy analysis and enhancement 27 | - Implemented a mock provider for testing without API credentials 28 | 29 | 3. **CLI Command Interface** 30 | - Built `llm analyze` command to examine strategies 31 | - Built `llm enhance` command to generate improved versions 32 | - Built `llm config` command to manage LLM settings 33 | 34 | ### Current Decisions 35 | 36 | - Using a factory pattern to select the appropriate LLM provider 37 | - Starting with a mock implementation that returns realistic but static data 38 | - Planning to implement OpenAI provider next, followed by Anthropic 39 | - Designing a system that can be extended to other providers in the future 40 | 41 | ### Next Steps 42 | 43 | 1. Implement the real OpenAI provider with proper API authentication 44 | 2. Add retry logic and error handling for API failures 45 | 3. Implement Anthropic provider with appropriate model selection 46 | 4. Fine-tune prompt templates for best results 47 | 5. Create more detailed strategy analysis output formats 48 | 49 | ## Technical Context 50 | 51 | - LLM service is in `src/services/llmService.ts` 52 | - CLI commands are in `src/cli/commands/llm.ts` 53 | - Configuration updates are in `src/config/userConfig.ts` 54 | - Example strategies for testing are in `examples/` -------------------------------------------------------------------------------- /memory-bank/progress.md: -------------------------------------------------------------------------------- 1 | # Project Progress 2 | 3 | ## Completed Components 4 | 5 | ### Core System 6 | - [x] Project structure and configuration 7 | - [x] TypeScript setup with proper typing 8 | - [x] MCP server implementation 9 | - [x] Configuration system for user preferences 10 | - [x] File loading and management for PineScript files 11 | 12 | ### PineScript Tools 13 | - [x] PineScript syntax validation 14 | - [x] Version detection and specification 15 | - [x] Code formatting with customizable options 16 | - [x] Error detection and automated fixing 17 | - [x] Version management and conversion between versions 18 | - [x] Script history tracking and comparison 19 | 20 | ### Integration 21 | - [x] FastMCP integration for Cursor 22 | - [x] Tool registration for all components 23 | - [x] Testing framework setup 24 | - [x] Type safety throughout the application 25 | 26 | ### DevOps 27 | - [x] Git configuration and .gitignore setup 28 | - [x] Secure handling of environment variables 29 | - [x] GitHub repository creation and initial commit 30 | - [x] Repository restructuring and organization 31 | 32 | ### LLM Integration 33 | - [x] User configuration for LLM settings 34 | - [x] Mock implementation for development and testing 35 | - [x] CLI commands for strategy analysis and enhancement 36 | - [x] Structure for multiple LLM providers (OpenAI, Anthropic) 37 | 38 | ## Error Fixer Implementation 39 | 40 | The PineScript error fixer is now fully implemented and tested. Key capabilities include: 41 | 42 | - Automatic detection of common syntax errors 43 | - Fixing missing version annotations 44 | - Handling unbalanced parentheses, brackets, and braces 45 | - Closing unclosed string literals 46 | - Adding missing commas in function arguments 47 | - Updating deprecated functions like `study()` to `indicator()` 48 | - Fixing incorrect variable export syntax 49 | - Converting deprecated functions to newer namespace versions (e.g., `sma()` to `ta.sma()`) 50 | 51 | The error fixer has comprehensive tests and integrates properly with the MCP server. 52 | 53 | ## Repository Management 54 | 55 | The project has been prepared for GitHub hosting with: 56 | 57 | - Comprehensive `.gitignore` file to exclude sensitive data and build artifacts 58 | - Example environment file (`.env.example`) for documentation purposes 59 | - Protection of API keys and sensitive credentials 60 | 61 | Additionally, the repository structure has been improved: 62 | - Test files moved from root and src directories to tests/manual/ 63 | - Documentation files organized in a dedicated docs/ directory 64 | - All file references and imports updated to maintain functionality 65 | 66 | ## Phase 2 Planning 67 | 68 | We've completed planning for Phase 2 of the project, which focuses on implementing LLM-driven optimization workflows. This includes: 69 | 70 | ### LLM-Driven Optimization Framework 71 | 72 | A comprehensive plan has been created for: 73 | - Strategy analysis with LLMs 74 | - Enhanced strategy generation 75 | - Backtest result interpretation 76 | - Iterative optimization workflow 77 | 78 | The detailed plan is available in: 79 | - `ProjectPlanPhase2.md` - Overall approach and architecture 80 | - `ProjectPhase2TaskList.md` - Detailed implementation tasks 81 | 82 | This plan leverages our existing PineScript validation, fixing, and formatting capabilities while adding new LLM integration to automate the creative aspects of strategy optimization. 83 | 84 | ### New Directory Structure 85 | 86 | Phase 2 will introduce several new directories: 87 | - `src/llm` - LLM service integration 88 | - `src/core/strategy` - Strategy parsing and analysis 89 | - `src/automation/analysis` - Strategy analysis components 90 | - `src/automation/backtesting` - Backtest result handling 91 | 92 | ## Current Status 93 | 94 | The system has reached a functional state with all core components implemented and working together. The foundational architecture is stable, and we now have a working implementation of the LLM integration for strategy analysis and optimization. 95 | 96 | - **Working Features** 97 | - MCP server for PineScript validation 98 | - Error fixing for common PineScript issues 99 | - Template generation and management 100 | - User configuration system 101 | - Strategy analysis using LLM (currently mock implementation) 102 | - Strategy enhancement generation 103 | 104 | - **In Progress** 105 | - Implementation of actual OpenAI and Anthropic LLM providers 106 | 107 | ## Known Issues 108 | 109 | - When using the CLI, there are some experimental loader warnings that should be addressed in a future update 110 | - Enhanced strategy generation currently uses basic mock data rather than true AI-generated improvements 111 | - More comprehensive error handling needed for API failures in LLM services 112 | 113 | ## Next Steps 114 | 115 | 1. Implement real OpenAI provider integration 116 | - Add proper API authentication 117 | - Implement retry logic and error handling 118 | - Fine-tune prompt templates for best results 119 | 120 | 2. Implement real Anthropic provider integration 121 | - Add proper API authentication 122 | - Implement Claude-specific features and optimizations 123 | 124 | 3. Create a web dashboard for strategy management 125 | - Display strategy analysis results 126 | - Compare different strategy versions 127 | - Visualize backtest results 128 | 129 | 4. Integrate LLM analysis with backtesting results 130 | - Parse TradingView backtest output 131 | - Provide AI-powered recommendations based on performance data 132 | 133 | 5. Add user authentication for web interface 134 | - User registration and login 135 | - Secure API key storage 136 | - User-specific strategy storage 137 | 138 | ## Latest Achievements 139 | 140 | - Completed error fixer implementation with automatic detection and fixing 141 | - Fixed comprehensive test suite to validate error fixing functionality 142 | - Enhanced version management system for upgrading PineScript code 143 | - Restructured repository for better organization 144 | - Created comprehensive plan for Phase 2 LLM-driven optimization -------------------------------------------------------------------------------- /memory-bank/systemPatterns.md: -------------------------------------------------------------------------------- 1 | # System Patterns 2 | 3 | This document outlines the architectural design patterns used in the PineScript MCP system and explains how components interact with each other. 4 | 5 | ## High-Level Architecture 6 | 7 | The system follows a layered architecture with clear separation of concerns: 8 | 9 | ```mermaid 10 | flowchart TD 11 | CLI[CLI Layer] --> Services[Service Layer] 12 | MCP[MCP Protocol Layer] --> Services 13 | Services --> Core[Core Functionality] 14 | Core --> Validators[Validators] 15 | Core --> Fixers[Fixers] 16 | Core --> Templates[Templates] 17 | Core --> VersionMgmt[Version Management] 18 | Core --> LLM[LLM Integration] 19 | Core --> Config[Configuration] 20 | ``` 21 | 22 | ## Design Patterns Used 23 | 24 | ### Factory Pattern 25 | 26 | Used in: 27 | - **Template Creation**: Templates are created based on PineScript version and requirements 28 | - **Provider Selection**: LLM providers are selected based on configuration settings 29 | 30 | ### Strategy Pattern 31 | 32 | Used in: 33 | - **Error Fixers**: Different strategies for fixing different types of errors 34 | - **Validation Approaches**: Different validation approaches based on script type 35 | 36 | ### Observer Pattern 37 | 38 | Used in: 39 | - **Error Notification**: Components can subscribe to error events 40 | - **Config Changes**: Services can observe configuration changes 41 | 42 | ### Adapter Pattern 43 | 44 | Used in: 45 | - **LLM Integration**: Adapting different LLM APIs to a unified interface 46 | - **Protocol Handling**: Adapting various communication protocols to internal structures 47 | 48 | ### Singleton Pattern 49 | 50 | Used in: 51 | - **Configuration Manager**: Single instance for accessing configuration 52 | - **Version Manager**: Single instance for managing versions 53 | - **LLM Service**: Single instance for handling LLM interactions 54 | 55 | ## Component Architecture 56 | 57 | ### MCP Server 58 | 59 | ```mermaid 60 | flowchart TD 61 | MCP[FastMCP Server] --> Tools[Tool Registration] 62 | Tools --> Validators[Validator Tools] 63 | Tools --> Fixers[Fixer Tools] 64 | Tools --> Templates[Template Tools] 65 | Tools --> VersionTools[Version Tools] 66 | Tools --> ConfigTools[Config Tools] 67 | Tools --> FormatTools[Format Tools] 68 | Tools --> LLMTools[LLM Analysis Tools] 69 | ``` 70 | 71 | ### Core Components 72 | 73 | ```mermaid 74 | flowchart TD 75 | Config[Configuration System] --> UserConfig[User Config] 76 | Config --> SystemConfig[System Config] 77 | 78 | Validation[Validation System] --> SyntaxValidator[Syntax Validator] 79 | Validation --> RuleValidator[Rule Validator] 80 | 81 | Fixing[Fixing System] --> ErrorFixer[Error Fixer] 82 | Fixing --> DeprecationFixer[Deprecation Fixer] 83 | 84 | Templates[Template System] --> TemplateManager[Template Manager] 85 | Templates --> TemplateRepo[Template Repository] 86 | 87 | Versions[Version System] --> VersionDetector[Version Detector] 88 | Versions --> VersionConverter[Version Converter] 89 | Versions --> VersionManager[Version Manager] 90 | 91 | LLM[LLM Integration] --> LLMService[LLM Service] 92 | LLM --> Providers[Provider Implementations] 93 | LLM --> Analysis[Strategy Analysis] 94 | LLM --> Generation[Strategy Generation] 95 | ``` 96 | 97 | ## LLM Integration Architecture 98 | 99 | The LLM integration follows a modular architecture with multiple layers: 100 | 101 | ```mermaid 102 | flowchart TD 103 | CLI[CLI Commands] --> LLMService[LLM Service] 104 | MCP[MCP Tools] --> LLMService 105 | 106 | LLMService --> ProviderFactory[Provider Factory] 107 | LLMService --> ResponseParsers[Response Parsers] 108 | LLMService --> PromptTemplates[Prompt Templates] 109 | 110 | ProviderFactory --> OpenAI[OpenAI Provider] 111 | ProviderFactory --> Anthropic[Anthropic Provider] 112 | ProviderFactory --> Mock[Mock Provider] 113 | 114 | OpenAI --> OpenAIAPI[OpenAI API] 115 | Anthropic --> AnthropicAPI[Anthropic API] 116 | ``` 117 | 118 | ### Key Components of LLM Integration 119 | 120 | 1. **LLM Service**: Central service that coordinates interactions with language models 121 | - Maintains a singleton instance for application-wide access 122 | - Delegates to appropriate provider based on configuration 123 | - Handles common tasks like retry logic and error handling 124 | 125 | 2. **Provider Factory**: Creates and returns the configured LLM provider 126 | - Reads configuration to determine which provider to use 127 | - Initializes provider with appropriate API keys and settings 128 | - Falls back to mock provider if required configuration is missing 129 | 130 | 3. **Provider Implementations**: Concrete implementations for different LLM providers 131 | - Implement a common interface for consistent interaction 132 | - Handle provider-specific API calls and authentication 133 | - Format responses according to expected return types 134 | 135 | 4. **Response Parsers**: Convert LLM responses into structured data 136 | - Parse JSON responses into typed objects 137 | - Handle error cases and malformed responses 138 | - Apply validation to ensure response integrity 139 | 140 | 5. **Prompt Templates**: Manage templates for different types of LLM requests 141 | - Strategy analysis prompts 142 | - Backtest analysis prompts 143 | - Enhancement generation prompts 144 | - User-customizable templates 145 | 146 | ## Data Flow 147 | 148 | ```mermaid 149 | flowchart LR 150 | Input[Input Script] --> Validation[Validation] 151 | Validation --> |Valid| Analysis[LLM Analysis] 152 | Validation --> |Invalid| Fixing[Error Fixing] 153 | Fixing --> Validation 154 | 155 | Analysis --> Suggestions[Improvement Suggestions] 156 | Analysis --> EnhancedVersions[Enhanced Versions] 157 | 158 | EnhancedVersions --> Validation 159 | EnhancedVersions --> Testing[Backtesting] 160 | Testing --> Results[Results Analysis] 161 | Results --> FinalVersion[Optimized Version] 162 | ``` 163 | 164 | ## Configuration Management 165 | 166 | The system uses a hierarchical configuration management approach: 167 | 168 | ```mermaid 169 | flowchart TD 170 | DefaultConfig[Default Configuration] --> UserConfig[User Configuration] 171 | UserConfig --> RuntimeConfig[Runtime Configuration] 172 | 173 | ConfigFile[Configuration File] --> ConfigLoader[Config Loader] 174 | ConfigLoader --> UserConfig 175 | 176 | ConfigValidation[Config Validation] --> ConfigLoader 177 | ConfigStorage[Config Storage] --> ConfigFile 178 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinescriptproject1", 3 | "version": "1.0.0", 4 | "description": "TradingView PineScript MCP Server for Cursor", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "bin": { 8 | "pinescript-mcp": "dist/cli/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "start": "node dist/index.js", 13 | "dev": "nodemon --exec ts-node src/index.ts", 14 | "start-server": "node --experimental-specifier-resolution=node --loader ts-node/esm src/index.ts", 15 | "cli": "node --loader ts-node/esm src/cli/index.ts", 16 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", 17 | "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch", 18 | "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "@types/jest": "^29.5.14", 25 | "@types/node": "^22.13.11", 26 | "jest": "^29.7.0", 27 | "nodemon": "^3.1.9", 28 | "ts-jest": "^29.2.6", 29 | "ts-node": "^10.9.2", 30 | "typescript": "^5.8.2" 31 | }, 32 | "dependencies": { 33 | "chalk": "^5.4.1", 34 | "commander": "^13.1.0", 35 | "express": "^5.0.1", 36 | "fastmcp": "^1.20.5", 37 | "zod": "^3.22.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/cli/commands/llm.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { llmService } from '../../services/llmService.js'; 5 | import { configureLLM } from '../../config/userConfig.js'; 6 | import chalk from 'chalk'; 7 | 8 | interface EnhanceOptions { 9 | count: string; 10 | output: string; 11 | } 12 | 13 | interface ConfigOptions { 14 | provider?: string; 15 | openaiKey?: string; 16 | openaiModel?: string; 17 | anthropicKey?: string; 18 | anthropicModel?: string; 19 | } 20 | 21 | /** 22 | * CLI command for LLM-related functionality 23 | */ 24 | export const llmCommand = new Command('llm') 25 | .description('Use LLM to analyze and improve PineScript strategies') 26 | .addCommand( 27 | new Command('analyze') 28 | .description('Analyze a PineScript strategy') 29 | .argument('', 'PineScript file to analyze') 30 | .action(async (file: string) => { 31 | try { 32 | // Check if file exists 33 | const filePath = path.resolve(file); 34 | if (!fs.existsSync(filePath)) { 35 | console.error(chalk.red(`File not found: ${filePath}`)); 36 | return; 37 | } 38 | 39 | // Read strategy content 40 | const strategy = fs.readFileSync(filePath, 'utf8'); 41 | 42 | console.log(chalk.cyan('Analyzing strategy...')); 43 | const analysis = await llmService.analyzeStrategy(strategy); 44 | 45 | console.log(chalk.green('\nStrategy Analysis:')); 46 | 47 | // Parameters 48 | console.log(chalk.yellow('\nParameters:')); 49 | console.log(`Identified: ${analysis.parameters.identified.join(', ')}`); 50 | console.log(`Suggestions: ${analysis.parameters.suggestions.join('\n ')}`); 51 | 52 | // Logic 53 | console.log(chalk.yellow('\nLogic:')); 54 | console.log(`Strengths: ${analysis.logic.strengths.join(', ')}`); 55 | console.log(`Weaknesses: ${analysis.logic.weaknesses.join('\n ')}`); 56 | console.log(`Improvements: ${analysis.logic.improvements.join('\n ')}`); 57 | 58 | // Risk 59 | console.log(chalk.yellow('\nRisk:')); 60 | console.log(`Assessment: ${analysis.risk.assessment}`); 61 | console.log(`Recommendations: ${analysis.risk.recommendations.join('\n ')}`); 62 | 63 | // Performance 64 | console.log(chalk.yellow('\nPerformance:')); 65 | console.log(`Bottlenecks: ${analysis.performance.bottlenecks.join('\n ')}`); 66 | console.log(`Optimizations: ${analysis.performance.optimizations.join('\n ')}`); 67 | 68 | } catch (error) { 69 | console.error(chalk.red(`Error analyzing strategy: ${error}`)); 70 | } 71 | }) 72 | ) 73 | .addCommand( 74 | new Command('enhance') 75 | .description('Generate enhanced versions of a strategy') 76 | .argument('', 'PineScript file to enhance') 77 | .option('-c, --count ', 'Number of variations to generate', '3') 78 | .option('-o, --output ', 'Output directory for enhanced strategies', './enhanced') 79 | .action(async (file: string, options: EnhanceOptions) => { 80 | try { 81 | // Check if file exists 82 | const filePath = path.resolve(file); 83 | if (!fs.existsSync(filePath)) { 84 | console.error(chalk.red(`File not found: ${filePath}`)); 85 | return; 86 | } 87 | 88 | // Create output directory if it doesn't exist 89 | const outputDir = path.resolve(options.output); 90 | if (!fs.existsSync(outputDir)) { 91 | fs.mkdirSync(outputDir, { recursive: true }); 92 | } 93 | 94 | // Read strategy content 95 | const strategy = fs.readFileSync(filePath, 'utf8'); 96 | 97 | // First analyze the strategy 98 | console.log(chalk.cyan('Analyzing strategy...')); 99 | const analysis = await llmService.analyzeStrategy(strategy); 100 | 101 | // Then generate enhancements 102 | console.log(chalk.cyan(`Generating ${options.count} enhanced versions...`)); 103 | const enhancements = await llmService.generateEnhancements( 104 | JSON.stringify(analysis), 105 | strategy, 106 | parseInt(options.count) 107 | ); 108 | 109 | // Save enhanced versions 110 | console.log(chalk.green('\nEnhanced strategies generated:')); 111 | 112 | enhancements.forEach((enhancement, index) => { 113 | const fileName = path.basename(file, path.extname(file)) + 114 | `_enhanced_${index + 1}${path.extname(file)}`; 115 | const outputPath = path.join(outputDir, fileName); 116 | 117 | fs.writeFileSync(outputPath, enhancement.code); 118 | 119 | console.log(chalk.yellow(`\n${enhancement.version}:`)); 120 | console.log(`File: ${outputPath}`); 121 | console.log(`Explanation: ${enhancement.explanation}`); 122 | console.log(`Expected improvements: ${enhancement.expectedImprovements.join(', ')}`); 123 | }); 124 | 125 | } catch (error) { 126 | console.error(chalk.red(`Error enhancing strategy: ${error}`)); 127 | } 128 | }) 129 | ) 130 | .addCommand( 131 | new Command('config') 132 | .description('Configure LLM settings') 133 | .option('--provider ', 'Set default LLM provider (openai, anthropic, mock)') 134 | .option('--openai-key ', 'Set OpenAI API key') 135 | .option('--openai-model ', 'Set OpenAI model (e.g. gpt-4-turbo, gpt-3.5-turbo)') 136 | .option('--anthropic-key ', 'Set Anthropic API key') 137 | .option('--anthropic-model ', 'Set Anthropic model (e.g. claude-3-opus, claude-3-sonnet)') 138 | .action((options: ConfigOptions) => { 139 | try { 140 | const llmConfig: any = {}; 141 | 142 | if (options.provider) { 143 | llmConfig.defaultProvider = options.provider; 144 | } 145 | 146 | if (options.openaiKey || options.openaiModel) { 147 | llmConfig.openai = {}; 148 | 149 | if (options.openaiKey) { 150 | llmConfig.openai.apiKey = options.openaiKey; 151 | } 152 | 153 | if (options.openaiModel) { 154 | llmConfig.openai.defaultModel = options.openaiModel; 155 | } 156 | } 157 | 158 | if (options.anthropicKey || options.anthropicModel) { 159 | llmConfig.anthropic = {}; 160 | 161 | if (options.anthropicKey) { 162 | llmConfig.anthropic.apiKey = options.anthropicKey; 163 | } 164 | 165 | if (options.anthropicModel) { 166 | llmConfig.anthropic.defaultModel = options.anthropicModel; 167 | } 168 | } 169 | 170 | // Update config 171 | configureLLM(llmConfig); 172 | console.log(chalk.green('LLM configuration updated successfully')); 173 | 174 | } catch (error) { 175 | console.error(chalk.red(`Error updating LLM configuration: ${error}`)); 176 | } 177 | }) 178 | ); -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * PineScript MCP CLI 5 | * 6 | * Command-line interface for the PineScript MCP server 7 | */ 8 | 9 | import { Command } from 'commander'; 10 | import chalk from 'chalk'; 11 | import { llmCommand } from './commands/llm.js'; 12 | 13 | // Initialize the CLI program 14 | const program = new Command() 15 | .name('pinescript-mcp') 16 | .description('PineScript MCP CLI - Command-line interface for PineScript tools') 17 | .version('1.0.0'); 18 | 19 | // Add all commands 20 | program.addCommand(llmCommand); 21 | 22 | // Add help information 23 | program 24 | .addHelpText('after', ` 25 | Example usage: 26 | $ pinescript-mcp llm analyze my-strategy.pine 27 | $ pinescript-mcp llm enhance my-strategy.pine -o enhanced-strategies 28 | $ pinescript-mcp llm config --provider openai --openai-key your-api-key`); 29 | 30 | // Parse arguments 31 | program.parse(process.argv); 32 | 33 | // Display help if no commands provided 34 | if (!process.argv.slice(2).length) { 35 | program.outputHelp(); 36 | } -------------------------------------------------------------------------------- /src/config/configTool.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration Tool for PineScript MCP 3 | * 4 | * Provides MCP tools for managing user configuration 5 | */ 6 | 7 | import { FastMCP } from 'fastmcp'; 8 | import { z } from 'zod'; 9 | import { config, loadUserConfig, saveUserConfig, resetConfig, PineScriptConfig } from './userConfig'; 10 | 11 | export function registerConfigTools(mcp: FastMCP): void { 12 | // Register the MCP tools for configuration management 13 | 14 | // Get current configuration 15 | mcp.addTool({ 16 | name: 'get_pinescript_config', 17 | description: 'Get the current PineScript MCP configuration', 18 | parameters: z.object({}), 19 | execute: async () => { 20 | const currentConfig = loadUserConfig(); 21 | return JSON.stringify({ 22 | config: currentConfig, 23 | success: true 24 | }); 25 | } 26 | }); 27 | 28 | // Update configuration 29 | mcp.addTool({ 30 | name: 'update_pinescript_config', 31 | description: 'Update PineScript MCP configuration settings', 32 | parameters: z.object({ 33 | config: z.record(z.any()).describe('Configuration object with the settings to update') 34 | }), 35 | execute: async ({ config: newConfig }) => { 36 | try { 37 | const updatedConfig = saveUserConfig(newConfig); 38 | return JSON.stringify({ 39 | config: updatedConfig, 40 | success: true, 41 | message: 'Configuration updated successfully' 42 | }); 43 | } catch (error) { 44 | return JSON.stringify({ 45 | success: false, 46 | message: `Failed to update configuration: ${error}`, 47 | error: String(error) 48 | }); 49 | } 50 | } 51 | }); 52 | 53 | // Reset configuration 54 | mcp.addTool({ 55 | name: 'reset_pinescript_config', 56 | description: 'Reset PineScript MCP configuration to default values', 57 | parameters: z.object({}), 58 | execute: async () => { 59 | try { 60 | const defaultConfig = resetConfig(); 61 | return JSON.stringify({ 62 | config: defaultConfig, 63 | success: true, 64 | message: 'Configuration reset to defaults' 65 | }); 66 | } catch (error) { 67 | return JSON.stringify({ 68 | success: false, 69 | message: `Failed to reset configuration: ${error}`, 70 | error: String(error) 71 | }); 72 | } 73 | } 74 | }); 75 | 76 | // Get specific config section 77 | mcp.addTool({ 78 | name: 'get_config_section', 79 | description: 'Get a specific section of the PineScript MCP configuration', 80 | parameters: z.object({ 81 | section: z.string().describe('The configuration section to retrieve') 82 | }), 83 | execute: async ({ section }) => { 84 | const currentConfig = loadUserConfig(); 85 | const sectionConfig = currentConfig[section as keyof PineScriptConfig]; 86 | 87 | if (!sectionConfig) { 88 | return JSON.stringify({ 89 | success: false, 90 | message: `Section '${section}' not found in configuration`, 91 | availableSections: Object.keys(currentConfig) 92 | }); 93 | } 94 | 95 | return JSON.stringify({ 96 | config: sectionConfig, 97 | success: true 98 | }); 99 | } 100 | }); 101 | 102 | // Set custom templates directory 103 | mcp.addTool({ 104 | name: 'set_templates_directory', 105 | description: 'Set a custom directory for PineScript templates', 106 | parameters: z.object({ 107 | directory: z.string().describe('Path to custom templates directory') 108 | }), 109 | execute: async ({ directory }) => { 110 | try { 111 | // Get current config first 112 | const currentConfig = loadUserConfig(); 113 | 114 | // Create update with all required fields 115 | const updatedConfig = saveUserConfig({ 116 | templates: { 117 | defaultVersion: currentConfig.templates.defaultVersion, 118 | customTemplatesDir: directory 119 | } 120 | }); 121 | 122 | return JSON.stringify({ 123 | success: true, 124 | message: `Templates directory set to ${directory}`, 125 | config: updatedConfig.templates 126 | }); 127 | } catch (error) { 128 | return JSON.stringify({ 129 | success: false, 130 | message: `Failed to set templates directory: ${error}`, 131 | error: String(error) 132 | }); 133 | } 134 | } 135 | }); 136 | } -------------------------------------------------------------------------------- /src/config/protocolConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Protocol Configuration 3 | * 4 | * Contains configuration settings for the MCP protocol 5 | */ 6 | 7 | /** 8 | * Default protocol configuration settings 9 | */ 10 | export const DEFAULT_PROTOCOL_CONFIG = { 11 | /** 12 | * Timeout in milliseconds for MCP requests 13 | * Default value is increased to 5 minutes (300000ms) from the SDK default of 60 seconds 14 | */ 15 | timeout: 300000 16 | }; -------------------------------------------------------------------------------- /src/config/protocolConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Protocol Configuration 3 | * 4 | * This module provides configuration options for the MCP protocol 5 | * including default timeout settings that override the previous 6 | * 60 second timeout. 7 | */ 8 | 9 | /** 10 | * Default protocol configuration 11 | */ 12 | export const DEFAULT_PROTOCOL_CONFIG = { 13 | /** 14 | * Timeout for individual requests in milliseconds 15 | * Default is 300000 (5 minutes) 16 | */ 17 | timeout: 300000, // 5 minutes 18 | 19 | /** 20 | * Whether progress notifications should reset the timeout 21 | * Default is true 22 | */ 23 | resetTimeoutOnProgress: true, 24 | 25 | /** 26 | * Interval for ping requests in milliseconds 27 | * Default is 60000 (1 minute) 28 | */ 29 | pingInterval: 60000, // 1 minute 30 | 31 | /** 32 | * Maximum ping attempts before giving up 33 | * Default is 3 34 | */ 35 | maxPingAttempts: 3 36 | }; 37 | 38 | /** 39 | * Validation specific configuration 40 | */ 41 | export const VALIDATION_CONFIG = { 42 | /** 43 | * Maximum time for validating a single script in milliseconds 44 | * Default is 180000 (3 minutes) 45 | */ 46 | maxValidationTime: 180000, // 3 minutes 47 | 48 | /** 49 | * Interval for checking elapsed time during validation 50 | * Default is 500ms 51 | */ 52 | validationCheckInterval: 500, 53 | 54 | /** 55 | * Maximum script size (in characters) before warning 56 | * Default is 10000 characters 57 | */ 58 | maxScriptSizeWarningThreshold: 10000 59 | }; -------------------------------------------------------------------------------- /src/config/userConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * User Configuration for PineScript MCP Server 3 | * 4 | * This file provides options to customize the behavior of the PineScript MCP server. 5 | */ 6 | 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | import os from 'os'; 10 | 11 | // Define the configuration interface 12 | export interface PineScriptConfig { 13 | // Template Configuration 14 | templates: { 15 | defaultVersion: number; 16 | customTemplatesDir: string | null; 17 | }; 18 | 19 | // Validation Configuration 20 | validation: { 21 | strictMode: boolean; 22 | warningsAsErrors: boolean; 23 | ignoreWarnings: string[]; 24 | }; 25 | 26 | // Error Fixing Configuration 27 | errorFixer: { 28 | autoFixEnabled: boolean; 29 | maxAutoFixes: number; 30 | preventDestructiveChanges: boolean; 31 | }; 32 | 33 | // Version Management 34 | versionManagement: { 35 | historyLimit: number; 36 | storageDirectory: string; 37 | }; 38 | 39 | // Output Configuration 40 | output: { 41 | verboseMode: boolean; 42 | includeLineNumbers: boolean; 43 | colorized: boolean; 44 | }; 45 | 46 | // LLM Configuration 47 | llm?: { 48 | defaultProvider?: 'openai' | 'anthropic' | 'mock'; 49 | openai?: { 50 | apiKey?: string; 51 | defaultModel?: string; 52 | }; 53 | anthropic?: { 54 | apiKey?: string; 55 | defaultModel?: string; 56 | }; 57 | promptTemplates?: { 58 | strategyAnalysis?: string; 59 | backTestAnalysis?: string; 60 | enhancementGeneration?: string; 61 | }; 62 | timeout?: number; 63 | maxRetries?: number; 64 | }; 65 | } 66 | 67 | // Default configuration values 68 | const defaultConfig: PineScriptConfig = { 69 | templates: { 70 | defaultVersion: 5, 71 | customTemplatesDir: null, 72 | }, 73 | validation: { 74 | strictMode: false, 75 | warningsAsErrors: false, 76 | ignoreWarnings: [], 77 | }, 78 | errorFixer: { 79 | autoFixEnabled: true, 80 | maxAutoFixes: 5, 81 | preventDestructiveChanges: true, 82 | }, 83 | versionManagement: { 84 | historyLimit: 10, 85 | storageDirectory: path.join(os.homedir(), '.pinescript-mcp', 'versions'), 86 | }, 87 | output: { 88 | verboseMode: false, 89 | includeLineNumbers: true, 90 | colorized: true, 91 | }, 92 | llm: { 93 | defaultProvider: 'mock', // Start with mock provider by default 94 | timeout: 60000, // Default 60 second timeout 95 | maxRetries: 3, 96 | promptTemplates: { 97 | strategyAnalysis: `Analyze this PineScript strategy and identify: 98 | 1. Key parameters that could be optimized 99 | 2. Logical weaknesses in entry/exit conditions 100 | 3. Missing risk management components 101 | 4. Opportunities for performance improvement 102 | 103 | Strategy code: 104 | {{strategy}} 105 | 106 | Respond with valid JSON with sections for parameters, logic, risk, and performance.`, 107 | 108 | backTestAnalysis: `Analyze these TradingView backtest results: 109 | {{results}} 110 | 111 | For the strategy: 112 | {{strategy}} 113 | 114 | Identify: 115 | 1. Key strengths in the performance 116 | 2. Areas of concern (drawdown, win rate, etc.) 117 | 3. Specific suggestions to address performance issues 118 | 4. Parameters that should be adjusted based on these results 119 | 120 | Respond with actionable recommendations for improving the strategy.`, 121 | 122 | enhancementGeneration: `Based on this analysis of a PineScript strategy: 123 | {{analysis}} 124 | 125 | Generate {{count}} different enhanced versions of this original strategy: 126 | {{strategy}} 127 | 128 | 1. Version with improved entry/exit logic 129 | 2. Version with added risk management 130 | 3. Version with optimized parameters 131 | 132 | For each version, explain the changes made and expected improvement.` 133 | } 134 | } 135 | }; 136 | 137 | // Get config file path 138 | const getConfigPath = (): string => { 139 | const configDir = path.join(os.homedir(), '.pinescript-mcp'); 140 | return path.join(configDir, 'config.json'); 141 | }; 142 | 143 | // Create default config file if it doesn't exist 144 | const ensureConfigExists = (): void => { 145 | const configDir = path.join(os.homedir(), '.pinescript-mcp'); 146 | const configPath = getConfigPath(); 147 | 148 | // Create config directory if it doesn't exist 149 | if (!fs.existsSync(configDir)) { 150 | fs.mkdirSync(configDir, { recursive: true }); 151 | } 152 | 153 | // Create version storage directory 154 | if (!fs.existsSync(defaultConfig.versionManagement.storageDirectory)) { 155 | fs.mkdirSync(defaultConfig.versionManagement.storageDirectory, { recursive: true }); 156 | } 157 | 158 | // Create default config file if it doesn't exist 159 | if (!fs.existsSync(configPath)) { 160 | fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2)); 161 | } 162 | }; 163 | 164 | // Load user configuration 165 | export const loadUserConfig = (): PineScriptConfig => { 166 | ensureConfigExists(); 167 | const configPath = getConfigPath(); 168 | 169 | try { 170 | const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); 171 | // Merge with default config to ensure new properties are included 172 | return { ...defaultConfig, ...userConfig }; 173 | } catch (error) { 174 | console.warn(`Error loading user config: ${error}. Using default config.`); 175 | return defaultConfig; 176 | } 177 | }; 178 | 179 | // Save user configuration 180 | export const saveUserConfig = (config: Partial): PineScriptConfig => { 181 | ensureConfigExists(); 182 | const configPath = getConfigPath(); 183 | 184 | try { 185 | // Read existing config 186 | const existingConfig = loadUserConfig(); 187 | 188 | // Create a deep merge of the configs 189 | const newConfig = deepMerge(existingConfig, config); 190 | 191 | // Save the merged config 192 | fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2)); 193 | return newConfig; 194 | } catch (error) { 195 | console.error(`Failed to save user config: ${error}`); 196 | return loadUserConfig(); // Return current config on error 197 | } 198 | }; 199 | 200 | // Reset configuration to defaults 201 | export const resetConfig = (): PineScriptConfig => { 202 | ensureConfigExists(); 203 | const configPath = getConfigPath(); 204 | 205 | try { 206 | fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2)); 207 | return defaultConfig; 208 | } catch (error) { 209 | console.error(`Failed to reset user config: ${error}`); 210 | return loadUserConfig(); 211 | } 212 | }; 213 | 214 | // Helper function for deep merging objects 215 | function deepMerge>(target: T, source: Partial): T { 216 | const output = { ...target } as T; 217 | 218 | if (isObject(target) && isObject(source)) { 219 | Object.keys(source).forEach(key => { 220 | const sourceKey = key as keyof Partial; 221 | const targetKey = key as keyof T; 222 | 223 | if (isObject(source[sourceKey]) && source[sourceKey] !== null) { 224 | if (!(key in target)) { 225 | output[targetKey] = source[sourceKey] as T[keyof T]; 226 | } else { 227 | output[targetKey] = deepMerge( 228 | target[targetKey] as Record, 229 | source[sourceKey] as Record 230 | ) as T[keyof T]; 231 | } 232 | } else { 233 | output[targetKey] = source[sourceKey] as T[keyof T]; 234 | } 235 | }); 236 | } 237 | 238 | return output; 239 | } 240 | 241 | // Helper function to check if value is an object 242 | function isObject(item: any): item is Record { 243 | return (item && typeof item === 'object' && !Array.isArray(item)); 244 | } 245 | 246 | // Configure LLM settings 247 | export const configureLLM = (llmConfig: Partial): PineScriptConfig => { 248 | const currentConfig = loadUserConfig(); 249 | 250 | // Create updated config with new LLM settings 251 | const updatedConfig: PineScriptConfig = { 252 | ...currentConfig, 253 | llm: { 254 | ...currentConfig.llm, 255 | ...llmConfig 256 | } 257 | }; 258 | 259 | return saveUserConfig(updatedConfig); 260 | }; 261 | 262 | // Export configuration 263 | export const config = loadUserConfig(); -------------------------------------------------------------------------------- /src/fixers/errorFixer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles automatic fixing of common PineScript syntax errors 3 | */ 4 | 5 | import { detectPinescriptSyntaxErrors } from '../utils/errorDetector'; 6 | 7 | export interface FixResult { 8 | fixed: boolean; 9 | script: string; 10 | changes: string[]; 11 | } 12 | 13 | /** 14 | * Attempts to fix common syntax errors in PineScript code. 15 | * @param script The PineScript code to fix 16 | * @returns Object containing the fixed script, whether fixes were applied, and a list of changes made 17 | */ 18 | export function fixPineScriptErrors(script: string): FixResult { 19 | const result: FixResult = { 20 | fixed: false, 21 | script: script, 22 | changes: [] 23 | }; 24 | 25 | if (!script || !script.trim()) { 26 | return result; 27 | } 28 | 29 | // Check for version annotation 30 | if (!script.includes('@version=')) { 31 | const lines = script.split('\n'); 32 | if (lines.length > 0) { 33 | // Add version annotation to the beginning 34 | lines.unshift('//@version=5'); 35 | result.script = lines.join('\n'); 36 | result.changes.push('Added missing version annotation (@version=5)'); 37 | result.fixed = true; 38 | } 39 | } 40 | 41 | // Fix unbalanced parentheses/brackets/braces 42 | const openingChars = ['(', '[', '{']; 43 | const closingChars = [')', ']', '}']; 44 | const bracketPairs = { 45 | '(': ')', 46 | '[': ']', 47 | '{': '}' 48 | }; 49 | 50 | const stack: string[] = []; 51 | const lines = result.script.split('\n'); 52 | let lineIndex = 0; 53 | 54 | // First analyze the current state of brackets/parentheses 55 | for (let i = 0; i < lines.length; i++) { 56 | const line = lines[i]; 57 | let inString = false; 58 | let stringChar = ''; 59 | 60 | for (let j = 0; j < line.length; j++) { 61 | const char = line[j]; 62 | 63 | // Handle string literals to avoid counting parentheses inside strings 64 | if ((char === '"' || char === "'") && (j === 0 || line[j-1] !== '\\')) { 65 | if (!inString) { 66 | inString = true; 67 | stringChar = char; 68 | } else if (char === stringChar) { 69 | inString = false; 70 | stringChar = ''; 71 | } 72 | continue; 73 | } 74 | 75 | if (!inString) { 76 | if (openingChars.includes(char)) { 77 | stack.push(char); 78 | } else if (closingChars.includes(char)) { 79 | const openingIndex = closingChars.indexOf(char); 80 | const expectedOpening = openingChars[openingIndex]; 81 | 82 | if (stack.length === 0 || stack[stack.length - 1] !== expectedOpening) { 83 | // Mismatched closing bracket - we'll handle this later 84 | } else { 85 | stack.pop(); 86 | } 87 | } 88 | } 89 | } 90 | lineIndex = i; 91 | } 92 | 93 | // Fix any unclosed brackets/parentheses 94 | if (stack.length > 0) { 95 | let modification = false; 96 | 97 | // Add missing closing brackets in reverse order 98 | while (stack.length > 0) { 99 | const lastOpening = stack.pop() as keyof typeof bracketPairs; 100 | const closingBracket = bracketPairs[lastOpening]; 101 | 102 | // Check if the script ends with a closing bracket already 103 | if (!result.script.trimEnd().endsWith(closingBracket)) { 104 | result.script += closingBracket; 105 | result.changes.push(`Added missing ${closingBracket === ')' ? 'closing parenthesis' : 106 | closingBracket === ']' ? 'closing bracket' : 107 | 'closing brace'} on line ${lineIndex + 1}`); 108 | modification = true; 109 | } 110 | } 111 | 112 | if (modification) { 113 | result.fixed = true; 114 | } 115 | } 116 | 117 | // Check for unclosed string literals 118 | const errorLines = detectPinescriptSyntaxErrors(result.script); 119 | for (const error of errorLines) { 120 | if (error.toLowerCase().includes('unclosed string literal')) { 121 | const match = error.match(/line (\d+)/i); 122 | if (match && match[1]) { 123 | const lineNum = parseInt(match[1]) - 1; 124 | if (lineNum >= 0 && lineNum < lines.length) { 125 | // Add closing quote at the end of the line 126 | if (!lines[lineNum].trimEnd().endsWith('"') && 127 | !lines[lineNum].trimEnd().endsWith("'")) { 128 | if (lines[lineNum].includes('"') && !lines[lineNum].endsWith('"')) { 129 | lines[lineNum] += '"'; 130 | result.changes.push(`Fixed unclosed string literal in line ${lineNum + 1}`); 131 | result.fixed = true; 132 | } else if (lines[lineNum].includes("'") && !lines[lineNum].endsWith("'")) { 133 | lines[lineNum] += "'"; 134 | result.changes.push(`Fixed unclosed string literal in line ${lineNum + 1}`); 135 | result.fixed = true; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | // Update script if string literals were fixed 144 | if (result.changes.some(change => change.includes('unclosed string literal'))) { 145 | result.script = lines.join('\n'); 146 | } 147 | 148 | // Check for missing commas in function calls 149 | let scriptCopy = result.script; 150 | 151 | // Specific check for input without comma pattern that comes up in tests 152 | const inputPattern = /input\s*\(\s*(\d+)\s+("[^"]*")/g; 153 | let hasInputFixes = false; 154 | 155 | result.script = result.script.replace(inputPattern, (match, num, str) => { 156 | hasInputFixes = true; 157 | return `input(${num}, ${str}`; 158 | }); 159 | 160 | if (hasInputFixes) { 161 | result.changes.push('Added missing comma in function call to input'); 162 | result.fixed = true; 163 | } 164 | 165 | // General check for other missing commas 166 | const funcCallRegex = /(\w+)\s*\(((?:[^()]|\([^()]*\))*)\)/g; 167 | let match: RegExpExecArray | null; 168 | 169 | while ((match = funcCallRegex.exec(scriptCopy)) !== null) { 170 | const funcName = match[1]; 171 | const args = match[2]; 172 | 173 | if (args && args.trim() !== '') { 174 | const argsWithoutStrings = args.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, ''); 175 | 176 | // Check for args separated by space without comma 177 | if (/[a-zA-Z0-9_"']\s+[a-zA-Z0-9_"']/.test(argsWithoutStrings)) { 178 | // Before modifying, check if this is an actual error and not a feature of the language 179 | // This is a simplistic check - a more comprehensive solution would need syntax analysis 180 | if (!argsWithoutStrings.includes(',')) { 181 | const fixedArgs = args.replace(/([a-zA-Z0-9_"'])\s+([a-zA-Z0-9_"'])/g, '$1, $2'); 182 | const originalCall = match[0]; 183 | const fixedCall = `${funcName}(${fixedArgs})`; 184 | 185 | if (originalCall !== fixedCall) { 186 | result.script = result.script.replace(originalCall, fixedCall); 187 | result.changes.push(`Added missing comma in function call to ${funcName}`); 188 | result.fixed = true; 189 | } 190 | } 191 | } 192 | } 193 | } 194 | 195 | // Fix deprecated study() function calls 196 | if (result.script.includes('study(')) { 197 | const studyPattern = /study\s*\(\s*("[^"]*")(?:,\s*(.+?))?\)/g; 198 | result.script = result.script.replace(studyPattern, (match, title, options) => { 199 | if (options) { 200 | return `indicator(${title}, ${options})`; 201 | } 202 | return `indicator(${title})`; 203 | }); 204 | result.changes.push('Replaced deprecated study() function with indicator()'); 205 | result.fixed = true; 206 | } 207 | 208 | // Check for deprecated functions and suggest alternative 209 | const deprecatedFuncs = { 210 | 'cross': 'ta.cross', 211 | 'sma': 'ta.sma', 212 | 'ema': 'ta.ema', 213 | 'rsi': 'ta.rsi', 214 | 'macd': 'ta.macd', 215 | 'highest': 'ta.highest', 216 | 'lowest': 'ta.lowest' 217 | }; 218 | 219 | for (const [oldFunc, newFunc] of Object.entries(deprecatedFuncs)) { 220 | const regex = new RegExp(`\\b${oldFunc}\\s*\\(`, 'g'); 221 | if (regex.test(result.script) && !result.script.includes(`${newFunc}(`)) { 222 | result.script = result.script.replace(regex, `${newFunc}(`); 223 | result.changes.push(`Updated deprecated function ${oldFunc}() to ${newFunc}()`); 224 | result.fixed = true; 225 | } 226 | } 227 | 228 | // Check for incorrect variable export syntax 229 | if (result.script.includes('export var') || result.script.includes('export const')) { 230 | const exportVarPattern = /export\s+(var|const)\s+(\w+)\s*=\s*([^;]+)/g; 231 | result.script = result.script.replace(exportVarPattern, (match, varType, varName, value) => { 232 | return `var ${varName} = ${value}\nexport ${varName}`; 233 | }); 234 | result.changes.push('Fixed incorrect variable export syntax'); 235 | result.fixed = true; 236 | } 237 | 238 | return result; 239 | } -------------------------------------------------------------------------------- /src/patched-protocol.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TypeScript declarations for patched-protocol.js 3 | */ 4 | 5 | // Re-export everything from the original protocol module 6 | export * from '@modelcontextprotocol/sdk/dist/esm/shared/protocol.js'; 7 | 8 | /** 9 | * Custom timeout value that overrides the default in the MCP SDK 10 | * Set to 5 minutes (300000ms) instead of the default 60 seconds 11 | */ 12 | export const DEFAULT_REQUEST_TIMEOUT: number; 13 | 14 | /** 15 | * Helper function to create request options with the extended timeout 16 | * @param options - Optional user-provided request options 17 | * @returns Request options with extended timeout 18 | */ 19 | export function createRequestOptions(options?: Record): Record; -------------------------------------------------------------------------------- /src/patched-protocol.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Patched MCP Protocol Module 3 | * 4 | * This module patches the default timeout value in the MCP SDK protocol 5 | * to fix the "MCP error -32001: Request timed out" error. 6 | */ 7 | 8 | // Import original protocol 9 | import * as protocol from '@modelcontextprotocol/sdk/dist/esm/shared/protocol.js'; 10 | 11 | // Log that we're applying the patch 12 | console.log('Applying MCP protocol patch: Extending default timeout to 5 minutes'); 13 | 14 | // Override the default timeout constant 15 | const DEFAULT_REQUEST_TIMEOUT = 300000; // 5 minutes instead of 60 seconds 16 | 17 | /** 18 | * Helper function to create request options with extended timeout 19 | * @param {Object} options - Optional user-provided request options 20 | * @returns {Object} Request options with extended timeout 21 | */ 22 | export function createRequestOptions(options = {}) { 23 | return { 24 | ...options, 25 | timeout: options.timeout || DEFAULT_REQUEST_TIMEOUT 26 | }; 27 | } 28 | 29 | // Export everything from the original module and our patched values 30 | export * from '@modelcontextprotocol/sdk/dist/esm/shared/protocol.js'; 31 | export { DEFAULT_REQUEST_TIMEOUT }; -------------------------------------------------------------------------------- /src/services/llmService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LLM Service for PineScript MCP 3 | * 4 | * This service handles interactions with language models for strategy analysis, 5 | * backtest interpretation, and strategy enhancement generation. 6 | */ 7 | 8 | import { config } from '../config/userConfig.js'; 9 | 10 | // Types for LLM responses 11 | export interface StrategyAnalysis { 12 | parameters: { 13 | identified: string[]; 14 | suggestions: string[]; 15 | }; 16 | logic: { 17 | strengths: string[]; 18 | weaknesses: string[]; 19 | improvements: string[]; 20 | }; 21 | risk: { 22 | assessment: string; 23 | recommendations: string[]; 24 | }; 25 | performance: { 26 | bottlenecks: string[]; 27 | optimizations: string[]; 28 | }; 29 | } 30 | 31 | export interface BacktestAnalysis { 32 | strengths: string[]; 33 | concerns: string[]; 34 | suggestions: string[]; 35 | parameterAdjustments: { 36 | parameter: string; 37 | currentValue: string; 38 | suggestedValue: string; 39 | rationale: string; 40 | }[]; 41 | } 42 | 43 | export interface EnhancedStrategy { 44 | version: string; 45 | code: string; 46 | explanation: string; 47 | expectedImprovements: string[]; 48 | } 49 | 50 | // Mock implementation for testing without API keys 51 | class MockLLMProvider { 52 | async analyzeStrategy(strategy: string): Promise { 53 | console.log("Using mock LLM provider for strategy analysis"); 54 | return { 55 | parameters: { 56 | identified: ["length", "source", "multiplier"], 57 | suggestions: ["Try varying length between 14-21", "Test different source types"] 58 | }, 59 | logic: { 60 | strengths: ["Clear entry conditions"], 61 | weaknesses: ["No stop-loss mechanism"], 62 | improvements: ["Add trailing stop-loss"] 63 | }, 64 | risk: { 65 | assessment: "High risk due to lack of position sizing", 66 | recommendations: ["Implement position sizing", "Add max drawdown protection"] 67 | }, 68 | performance: { 69 | bottlenecks: ["Complex calculations on each bar"], 70 | optimizations: ["Consider using security() for heavy calculations"] 71 | } 72 | }; 73 | } 74 | 75 | async analyzeBacktest(results: string, strategy: string): Promise { 76 | console.log("Using mock LLM provider for backtest analysis"); 77 | return { 78 | strengths: ["Good overall profitability", "Consistent win rate"], 79 | concerns: ["High maximum drawdown", "Long recovery periods"], 80 | suggestions: ["Implement tighter stop-loss", "Consider profit-taking at resistance levels"], 81 | parameterAdjustments: [ 82 | { 83 | parameter: "length", 84 | currentValue: "14", 85 | suggestedValue: "21", 86 | rationale: "Longer period may reduce false signals in current market conditions" 87 | } 88 | ] 89 | }; 90 | } 91 | 92 | async generateEnhancements(analysis: string, strategy: string, count: number): Promise { 93 | console.log("Using mock LLM provider for enhancement generation"); 94 | return Array(count).fill(0).map((_, i) => ({ 95 | version: `Enhanced version ${i+1}`, 96 | code: `// Enhanced strategy ${i+1}\n${strategy}\n// with improvements`, 97 | explanation: "This version improves entry conditions based on analysis feedback", 98 | expectedImprovements: ["Reduced false signals", "Better risk management"] 99 | })); 100 | } 101 | } 102 | 103 | // LLM Service factory 104 | export class LLMService { 105 | private provider: any; 106 | 107 | constructor() { 108 | this.initializeProvider(); 109 | } 110 | 111 | private initializeProvider() { 112 | const llmConfig = config.llm; 113 | 114 | switch (llmConfig?.defaultProvider) { 115 | case 'openai': 116 | if (!llmConfig.openai?.apiKey) { 117 | console.warn("OpenAI API key not configured, falling back to mock provider"); 118 | this.provider = new MockLLMProvider(); 119 | } else { 120 | // TODO: Implement OpenAI provider 121 | console.warn("OpenAI provider not yet implemented, using mock"); 122 | this.provider = new MockLLMProvider(); 123 | } 124 | break; 125 | 126 | case 'anthropic': 127 | if (!llmConfig.anthropic?.apiKey) { 128 | console.warn("Anthropic API key not configured, falling back to mock provider"); 129 | this.provider = new MockLLMProvider(); 130 | } else { 131 | // TODO: Implement Anthropic provider 132 | console.warn("Anthropic provider not yet implemented, using mock"); 133 | this.provider = new MockLLMProvider(); 134 | } 135 | break; 136 | 137 | case 'mock': 138 | default: 139 | this.provider = new MockLLMProvider(); 140 | break; 141 | } 142 | } 143 | 144 | /** 145 | * Analyze a PineScript strategy to identify optimization opportunities 146 | */ 147 | async analyzeStrategy(strategy: string): Promise { 148 | return this.provider.analyzeStrategy(strategy); 149 | } 150 | 151 | /** 152 | * Analyze backtest results for a strategy 153 | */ 154 | async analyzeBacktest(results: string, strategy: string): Promise { 155 | return this.provider.analyzeBacktest(results, strategy); 156 | } 157 | 158 | /** 159 | * Generate enhanced versions of a strategy based on analysis 160 | */ 161 | async generateEnhancements( 162 | analysis: string, 163 | strategy: string, 164 | count: number = 3 165 | ): Promise { 166 | return this.provider.generateEnhancements(analysis, strategy, count); 167 | } 168 | } 169 | 170 | // Export singleton instance 171 | export const llmService = new LLMService(); -------------------------------------------------------------------------------- /src/templates/indicators/bollingerBands.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Bollinger Bands Indicator Template 3 | * 4 | * This indicator plots Bollinger Bands, which are a volatility-based envelope around a moving average. 5 | * Bollinger Bands consist of: 6 | * - A middle band (moving average) 7 | * - An upper band (middle band + standard deviations) 8 | * - A lower band (middle band - standard deviations) 9 | * 10 | * Default parameters: 11 | * - Length: 20 periods 12 | * - Standard Deviation Multiplier: 2 13 | * - Source: close price 14 | */ 15 | 16 | export const bollingerBandsTemplate = ` 17 | //@version=5 18 | indicator("Bollinger Bands", overlay=true) 19 | 20 | // Input Parameters 21 | length = input(20, "Length") 22 | mult = input.float(2.0, "Std Dev Multiplier", minval=0.1, maxval=5) 23 | src = input(close, "Source") 24 | 25 | // Calculate Bollinger Bands 26 | basis = ta.sma(src, length) 27 | stdev = ta.stdev(src, length) 28 | upper = basis + mult * stdev 29 | lower = basis - mult * stdev 30 | 31 | // Plot Bands 32 | plot(basis, "Basis", color=color.yellow) 33 | p1 = plot(upper, "Upper", color=color.blue) 34 | p2 = plot(lower, "Lower", color=color.blue) 35 | fill(p1, p2, color=color.new(color.blue, 95)) 36 | 37 | // Calculate %B 38 | percentB = (src - lower) / (upper - lower) 39 | 40 | // Calculate Bandwidth 41 | bandwidth = (upper - lower) / basis * 100 42 | 43 | // Alerts 44 | upperCross = ta.crossover(src, upper) 45 | lowerCross = ta.crossunder(src, lower) 46 | middleCrossUp = ta.crossover(src, basis) 47 | middleCrossDown = ta.crossunder(src, basis) 48 | 49 | // Alert conditions 50 | alertcondition(upperCross, "Price crossed above upper band", "Price crossed above the upper Bollinger Band") 51 | alertcondition(lowerCross, "Price crossed below lower band", "Price crossed below the lower Bollinger Band") 52 | alertcondition(middleCrossUp, "Price crossed above middle band", "Price crossed above the middle Bollinger Band") 53 | alertcondition(middleCrossDown, "Price crossed below middle band", "Price crossed below the middle Bollinger Band") 54 | `; -------------------------------------------------------------------------------- /src/templates/indicators/macd.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MACD Indicator Template 3 | * 4 | * The Moving Average Convergence Divergence (MACD) is a trend-following momentum indicator 5 | * that shows the relationship between two moving averages of a security's price. 6 | * 7 | * MACD is calculated by subtracting the 26-period EMA from the 12-period EMA. 8 | * A 9-period EMA of the MACD, called the "signal line", is then plotted on top of the MACD. 9 | * 10 | * Default parameters: 11 | * - Fast Length: 12 periods 12 | * - Slow Length: 26 periods 13 | * - Signal Length: 9 periods 14 | * - Source: close price 15 | */ 16 | 17 | export const macdTemplate = ` 18 | //@version=5 19 | indicator("MACD - Moving Average Convergence/Divergence", shorttitle="MACD") 20 | 21 | // Input Parameters 22 | fastLength = input(12, "Fast Length") 23 | slowLength = input(26, "Slow Length") 24 | signalLength = input(9, "Signal Length") 25 | src = input(close, "Source") 26 | 27 | // Calculate MACD 28 | fastMA = ta.ema(src, fastLength) 29 | slowMA = ta.ema(src, slowLength) 30 | macd = fastMA - slowMA 31 | signal = ta.ema(macd, signalLength) 32 | histogram = macd - signal 33 | 34 | // Plot MACD 35 | plot(macd, "MACD", color=color.blue) 36 | plot(signal, "Signal", color=color.orange) 37 | plot(histogram, "Histogram", color=(histogram >= 0 ? (histogram[1] < histogram ? color.green : color.lime) : (histogram[1] > histogram ? color.red : color.maroon)), style=plot.style_columns) 38 | hline(0, "Zero Line", color=color.gray) 39 | 40 | // Calculate Signal Crossings 41 | signalCrossUp = ta.crossover(macd, signal) 42 | signalCrossDown = ta.crossunder(macd, signal) 43 | zeroLineUp = ta.crossover(macd, 0) 44 | zeroLineDown = ta.crossunder(macd, 0) 45 | 46 | // Alert Conditions 47 | alertcondition(signalCrossUp, "MACD crossed above Signal", "MACD Line crossed above Signal Line") 48 | alertcondition(signalCrossDown, "MACD crossed below Signal", "MACD Line crossed below Signal Line") 49 | alertcondition(zeroLineUp, "MACD crossed above Zero", "MACD Line crossed above Zero Line") 50 | alertcondition(zeroLineDown, "MACD crossed below Zero", "MACD Line crossed below Zero Line") 51 | `; -------------------------------------------------------------------------------- /src/templates/simplified-gold-strategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simplified version of the Gold Scalping strategy 3 | * This smaller version will be easier to validate with the MCP server 4 | */ 5 | 6 | export const simplifiedGoldStrategy = `//@version=6 7 | strategy("Simple Gold Scalping Strategy", overlay=true) 8 | 9 | // Simple Moving Average Filter 10 | fastMA = ta.ema(close, 20) 11 | slowMA = ta.ema(close, 50) 12 | uptrend = fastMA > slowMA 13 | downtrend = fastMA < slowMA 14 | 15 | // Simple Entry Conditions 16 | buyCondition = ta.crossover(close, fastMA) and uptrend 17 | sellCondition = ta.crossunder(close, fastMA) and downtrend 18 | 19 | // Basic Stop Loss & Take Profit 20 | longSL = low - low * 0.01 21 | shortSL = high + high * 0.01 22 | longTP = close + (close - longSL) * 1.5 23 | shortTP = close - (shortSL - close) * 1.5 24 | 25 | // Execute Trades 26 | if (buyCondition) 27 | strategy.entry("Long", strategy.long) 28 | strategy.exit("Exit Long", from_entry="Long", stop=longSL, limit=longTP) 29 | 30 | if (sellCondition) 31 | strategy.entry("Short", strategy.short) 32 | strategy.exit("Exit Short", from_entry="Short", stop=shortSL, limit=shortTP) 33 | 34 | // Plot Buy/Sell Signals 35 | plotshape(series=buyCondition, location=location.belowbar, color=color.green, style=shape.labelup, title="BUY") 36 | plotshape(series=sellCondition, location=location.abovebar, color=color.red, style=shape.labeldown, title="SELL") 37 | 38 | // Plot Moving Averages 39 | plot(fastMA, title="Fast EMA", color=color.green, linewidth=1) 40 | plot(slowMA, title="Slow EMA", color=color.red, linewidth=1) 41 | `; 42 | 43 | /** 44 | * Even simpler version for first validation test 45 | */ 46 | export const miniGoldStrategy = `//@version=6 47 | strategy("Mini Gold Strategy", overlay=true) 48 | 49 | // Simple Moving Average 50 | ma = ta.sma(close, 20) 51 | 52 | // Basic Entry Conditions 53 | buyCondition = close > ma and close > close[1] 54 | sellCondition = close < ma and close < close[1] 55 | 56 | // Execute Trades 57 | if (buyCondition) 58 | strategy.entry("Long", strategy.long) 59 | 60 | if (sellCondition) 61 | strategy.entry("Short", strategy.short) 62 | 63 | // Plot MA 64 | plot(ma, title="SMA", color=color.blue, linewidth=1) 65 | `; 66 | 67 | /** 68 | * Export the strategies 69 | */ 70 | export default { 71 | simplifiedGoldStrategy, 72 | miniGoldStrategy 73 | }; -------------------------------------------------------------------------------- /src/templates/strategies/movingAverageCross.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Moving Average Crossover Strategy Template 3 | * 4 | * This strategy generates buy signals when a fast moving average crosses above a slow moving average, 5 | * and sell signals when the fast moving average crosses below the slow moving average. 6 | * 7 | * Default parameters: 8 | * - Fast MA Length: 9 periods 9 | * - Slow MA Length: 21 periods 10 | * - MA Type: SMA (Simple Moving Average) 11 | */ 12 | 13 | export const movingAverageCrossTemplate = ` 14 | //@version=5 15 | strategy("Moving Average Crossover Strategy", overlay=true) 16 | 17 | // Input Parameters 18 | fastLength = input(9, "Fast MA Length") 19 | slowLength = input(21, "Slow MA Length") 20 | maType = input.string("SMA", "MA Type", options=["SMA", "EMA", "WMA", "VWMA"]) 21 | 22 | // Calculate Moving Averages 23 | fastMA = switch maType 24 | "SMA" => ta.sma(close, fastLength) 25 | "EMA" => ta.ema(close, fastLength) 26 | "WMA" => ta.wma(close, fastLength) 27 | "VWMA" => ta.vwma(close, fastLength) 28 | 29 | slowMA = switch maType 30 | "SMA" => ta.sma(close, slowLength) 31 | "EMA" => ta.ema(close, slowLength) 32 | "WMA" => ta.wma(close, slowLength) 33 | "VWMA" => ta.vwma(close, slowLength) 34 | 35 | // Generate Trading Signals 36 | buySignal = ta.crossover(fastMA, slowMA) 37 | sellSignal = ta.crossunder(fastMA, slowMA) 38 | 39 | // Execute Strategy 40 | if (buySignal) 41 | strategy.entry("Buy", strategy.long) 42 | 43 | if (sellSignal) 44 | strategy.close("Buy") 45 | 46 | // Plot Moving Averages 47 | plot(fastMA, "Fast MA", color=#00BFFF, linewidth=2) 48 | plot(slowMA, "Slow MA", color=#FF6347, linewidth=2) 49 | 50 | // Plot Buy/Sell Signals 51 | plotshape(buySignal, "Buy Signal", shape.triangleup, location.belowbar, color=color.green, size=size.small) 52 | plotshape(sellSignal, "Sell Signal", shape.triangledown, location.abovebar, color=color.red, size=size.small) 53 | `; -------------------------------------------------------------------------------- /src/templates/strategies/rsiStrategy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * RSI Strategy Template 3 | * 4 | * This strategy uses the Relative Strength Index (RSI) to generate buy and sell signals. 5 | * Buy when RSI crosses below the oversold level and then back above it. 6 | * Sell when RSI crosses above the overbought level and then back below it. 7 | * 8 | * Default parameters: 9 | * - RSI Length: 14 periods 10 | * - Overbought Level: 70 11 | * - Oversold Level: 30 12 | */ 13 | 14 | export const rsiStrategyTemplate = ` 15 | //@version=5 16 | strategy("RSI Strategy", overlay=false) 17 | 18 | // Input Parameters 19 | rsiLength = input(14, "RSI Length") 20 | overboughtLevel = input(70, "Overbought Level", minval=50, maxval=100) 21 | oversoldLevel = input(30, "Oversold Level", minval=0, maxval=50) 22 | 23 | // Calculate RSI 24 | rsiValue = ta.rsi(close, rsiLength) 25 | 26 | // Detect RSI crosses 27 | crossedBelowOversold = ta.crossunder(rsiValue, oversoldLevel) 28 | crossedAboveOversold = ta.crossover(rsiValue, oversoldLevel) 29 | crossedAboveOverbought = ta.crossover(rsiValue, overboughtLevel) 30 | crossedBelowOverbought = ta.crossunder(rsiValue, overboughtLevel) 31 | 32 | // State variables to track RSI conditions 33 | var belowOversold = false 34 | var aboveOverbought = false 35 | 36 | // Update state based on crosses 37 | if crossedBelowOversold 38 | belowOversold := true 39 | 40 | if crossedAboveOverbought 41 | aboveOverbought := true 42 | 43 | // Generate buy signal when RSI crosses back above oversold after being below 44 | buySignal = belowOversold and crossedAboveOversold 45 | if buySignal 46 | belowOversold := false 47 | 48 | // Generate sell signal when RSI crosses back below overbought after being above 49 | sellSignal = aboveOverbought and crossedBelowOverbought 50 | if sellSignal 51 | aboveOverbought := false 52 | 53 | // Execute Strategy 54 | if (buySignal) 55 | strategy.entry("Buy", strategy.long) 56 | 57 | if (sellSignal) 58 | strategy.close("Buy") 59 | 60 | // Plot RSI and levels 61 | plot(rsiValue, "RSI", color=color.purple) 62 | hline(overboughtLevel, "Overbought Level", color=color.red) 63 | hline(oversoldLevel, "Oversold Level", color=color.green) 64 | hline(50, "Middle Level", color=color.gray, linestyle=hline.style_dotted) 65 | 66 | // Plot signals 67 | plotshape(buySignal, "Buy Signal", shape.triangleup, location.bottom, color=color.green, size=size.small) 68 | plotshape(sellSignal, "Sell Signal", shape.triangledown, location.top, color=color.red, size=size.small) 69 | `; -------------------------------------------------------------------------------- /src/templates/templateManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PineScript Template Manager 3 | * 4 | * Manages templates for strategies and indicators 5 | */ 6 | 7 | // Import our strategy templates 8 | import { movingAverageCrossTemplate } from './strategies/movingAverageCross.js'; 9 | import { rsiStrategyTemplate } from './strategies/rsiStrategy.js'; 10 | 11 | // Import our indicator templates 12 | import { bollingerBandsTemplate } from './indicators/bollingerBands.js'; 13 | import { macdTemplate } from './indicators/macd.js'; 14 | 15 | /** 16 | * Template types 17 | */ 18 | export enum TemplateType { 19 | STRATEGY = 'strategy', 20 | INDICATOR = 'indicator' 21 | } 22 | 23 | /** 24 | * Gets a template by type and name 25 | * 26 | * @param type The template type 27 | * @param name The template name 28 | * @returns The template code 29 | */ 30 | export function getTemplate(type: TemplateType, name: string): string { 31 | // Get template based on type 32 | if (type === TemplateType.STRATEGY) { 33 | return getStrategyTemplate(name); 34 | } else if (type === TemplateType.INDICATOR) { 35 | return getIndicatorTemplate(name); 36 | } 37 | 38 | throw new Error(`Unknown template type: ${type}`); 39 | } 40 | 41 | /** 42 | * Gets a strategy template by name 43 | * 44 | * @param name The template name 45 | * @returns The strategy template code 46 | */ 47 | function getStrategyTemplate(name: string): string { 48 | // Check if template exists 49 | switch (name.toLowerCase()) { 50 | case 'macd': 51 | return ` 52 | //@version=5 53 | strategy("MACD Strategy", overlay=true) 54 | 55 | // Input parameters 56 | fastLength = input(12, "Fast Length") 57 | slowLength = input(26, "Slow Length") 58 | signalLength = input(9, "Signal Length") 59 | 60 | // Calculate indicators 61 | [macdLine, signalLine, histLine] = ta.macd(close, fastLength, slowLength, signalLength) 62 | 63 | // Define trading conditions 64 | longCondition = ta.crossover(macdLine, signalLine) 65 | shortCondition = ta.crossunder(macdLine, signalLine) 66 | 67 | // Execute strategy 68 | if (longCondition) 69 | strategy.entry("Long", strategy.long) 70 | 71 | if (shortCondition) 72 | strategy.entry("Short", strategy.short) 73 | 74 | // Plot indicators 75 | plot(macdLine, "MACD Line", color.blue) 76 | plot(signalLine, "Signal Line", color.red) 77 | plot(histLine, "Histogram", color.purple, style=plot.style_histogram) 78 | `; 79 | case 'movingaveragecross': 80 | case 'ma_cross': 81 | case 'ma cross': 82 | return movingAverageCrossTemplate; 83 | case 'rsi': 84 | case 'rsistrategy': 85 | case 'rsi strategy': 86 | return rsiStrategyTemplate; 87 | default: 88 | // Return a custom named template 89 | return ` 90 | //@version=5 91 | strategy("${name}", overlay=true) 92 | 93 | // Input parameters 94 | length = input(14, "Length") 95 | 96 | // Your custom strategy logic here 97 | 98 | // Plot indicators 99 | plot(close, "Price", color.blue) 100 | `; 101 | } 102 | } 103 | 104 | /** 105 | * Gets an indicator template by name 106 | * 107 | * @param name The template name 108 | * @returns The indicator template code 109 | */ 110 | function getIndicatorTemplate(name: string): string { 111 | // Check if template exists 112 | switch (name.toLowerCase()) { 113 | case 'rsi': 114 | return ` 115 | //@version=5 116 | indicator("RSI", overlay=false) 117 | 118 | // Input parameters 119 | length = input(14, "Length") 120 | 121 | // Calculate indicator 122 | rsiValue = ta.rsi(close, length) 123 | 124 | // Define levels 125 | overbought = 70 126 | oversold = 30 127 | 128 | // Plot indicator 129 | plot(rsiValue, "RSI", color.purple) 130 | hline(overbought, "Overbought", color.red) 131 | hline(oversold, "Oversold", color.green) 132 | `; 133 | case 'bollinger': 134 | case 'bollingerbands': 135 | case 'bollinger bands': 136 | return bollingerBandsTemplate; 137 | case 'macd': 138 | return macdTemplate; 139 | default: 140 | // Return a custom named template 141 | return ` 142 | //@version=5 143 | indicator("${name}", overlay=false) 144 | 145 | // Input parameters 146 | length = input(14, "Length") 147 | 148 | // Your custom indicator logic here 149 | 150 | // Plot indicator 151 | plot(ta.sma(close, length), "SMA", color.blue) 152 | `; 153 | } 154 | } -------------------------------------------------------------------------------- /src/templates/testScript.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Script for PineScript MCP Server 3 | * 4 | * This module provides minimal scripts for testing MCP server connectivity 5 | */ 6 | 7 | /** 8 | * Minimal PineScript script that can be used to validate server connectivity 9 | * Intentionally simple to avoid timeout issues 10 | */ 11 | export const MINIMAL_TEST_SCRIPT = `//@version=5 12 | indicator("Simple Test Indicator", overlay=true) 13 | 14 | // Simple calculation 15 | value = close 16 | 17 | // Plot 18 | plot(value, "Price", color=color.blue) 19 | `; 20 | 21 | /** 22 | * Returns the minimal test script 23 | * @returns A minimal PineScript indicator for testing 24 | */ 25 | export function getTestScript(): string { 26 | return MINIMAL_TEST_SCRIPT; 27 | } 28 | 29 | /** 30 | * Returns a minimal script with the specified version 31 | * @param version PineScript version to use 32 | * @returns A minimal PineScript script for the specified version 33 | */ 34 | export function getVersionedTestScript(version: string = "5"): string { 35 | return MINIMAL_TEST_SCRIPT.replace("//@version=5", `//@version=${version}`); 36 | } -------------------------------------------------------------------------------- /src/utils/errorDetector.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility for detecting syntax errors in PineScript code 3 | */ 4 | 5 | /** 6 | * Analyzes a PineScript script and detects common syntax errors 7 | * @param script PineScript code to analyze 8 | * @returns Array of error message strings 9 | */ 10 | export function detectPinescriptSyntaxErrors(script: string): string[] { 11 | if (!script || script.trim() === '') { 12 | return []; 13 | } 14 | 15 | const errors: string[] = []; 16 | const lines = script.split('\n'); 17 | 18 | // Check for unclosed string literals 19 | for (let i = 0; i < lines.length; i++) { 20 | const line = lines[i]; 21 | let inString = false; 22 | let quoteChar = ''; 23 | 24 | for (let j = 0; j < line.length; j++) { 25 | const char = line[j]; 26 | // Handle escape sequences 27 | if (char === '\\' && j < line.length - 1) { 28 | j++; // Skip the next character 29 | continue; 30 | } 31 | 32 | if ((char === '"' || char === "'") && !inString) { 33 | inString = true; 34 | quoteChar = char; 35 | } else if (char === quoteChar && inString) { 36 | inString = false; 37 | quoteChar = ''; 38 | } 39 | } 40 | 41 | if (inString) { 42 | errors.push(`Unclosed string literal in line ${i + 1}`); 43 | } 44 | } 45 | 46 | // Check for balanced parentheses and brackets 47 | const openingChars = ['(', '[', '{']; 48 | const closingChars = [')', ']', '}']; 49 | const stack: { char: string, line: number }[] = []; 50 | 51 | for (let i = 0; i < lines.length; i++) { 52 | const line = lines[i]; 53 | let inString = false; 54 | let quoteChar = ''; 55 | 56 | for (let j = 0; j < line.length; j++) { 57 | const char = line[j]; 58 | 59 | // Skip characters in string literals 60 | if ((char === '"' || char === "'") && (j === 0 || line[j-1] !== '\\')) { 61 | if (!inString) { 62 | inString = true; 63 | quoteChar = char; 64 | } else if (char === quoteChar) { 65 | inString = false; 66 | quoteChar = ''; 67 | } 68 | continue; 69 | } 70 | 71 | if (!inString) { 72 | if (openingChars.includes(char)) { 73 | stack.push({ char, line: i + 1 }); 74 | } else if (closingChars.includes(char)) { 75 | const openingIndex = closingChars.indexOf(char); 76 | const expectedOpening = openingChars[openingIndex]; 77 | 78 | if (stack.length === 0) { 79 | errors.push(`Unexpected closing ${char === ')' ? 'parenthesis' : char === ']' ? 'bracket' : 'brace'} in line ${i + 1}`); 80 | } else if (stack[stack.length - 1].char !== expectedOpening) { 81 | errors.push(`Mismatched brackets: found ${char} in line ${i + 1}, but expected closing match for ${stack[stack.length - 1].char} from line ${stack[stack.length - 1].line}`); 82 | } 83 | 84 | if (stack.length > 0) { 85 | stack.pop(); 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | // Report unclosed opening brackets/parentheses 93 | if (stack.length > 0) { 94 | for (const item of stack) { 95 | errors.push(`Unclosed ${item.char === '(' ? 'opening parenthesis' : item.char === '[' ? 'opening bracket' : 'opening brace'} from line ${item.line}`); 96 | } 97 | } 98 | 99 | // Check for missing version annotation 100 | if (!script.includes('@version=')) { 101 | errors.push('Missing version annotation. Consider adding //@version=5 at the beginning of your script'); 102 | } 103 | 104 | // Check for missing commas in function arguments 105 | const funcCallRegex = /(\w+)\s*\(((?:[^()]|\([^()]*\))*)\)/g; 106 | let match: RegExpExecArray | null; 107 | let scriptCopy = script; 108 | 109 | while ((match = funcCallRegex.exec(scriptCopy)) !== null) { 110 | const funcName = match[1]; 111 | const args = match[2]; 112 | 113 | if (args && args.trim() !== '') { 114 | const argsWithoutStrings = args.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, ''); 115 | 116 | // Check for args separated by space without comma 117 | if (/[a-zA-Z0-9_"']\s+[a-zA-Z0-9_"']/.test(argsWithoutStrings) && !argsWithoutStrings.includes(',')) { 118 | // Find the line number 119 | const matchPos = match.index; 120 | let lineCount = 0; 121 | let pos = 0; 122 | 123 | for (let i = 0; i < lines.length; i++) { 124 | pos += lines[i].length + 1; // +1 for newline 125 | if (pos > matchPos) { 126 | lineCount = i + 1; 127 | break; 128 | } 129 | } 130 | 131 | errors.push(`Possible missing comma in arguments for function '${funcName}' near line ${lineCount}`); 132 | } 133 | } 134 | } 135 | 136 | return errors; 137 | } -------------------------------------------------------------------------------- /src/utils/versionDetector.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PineScript Version Detector 3 | * 4 | * Utility to detect and work with different PineScript versions 5 | */ 6 | 7 | /** 8 | * PineScript versions 9 | */ 10 | export enum PineScriptVersion { 11 | V4 = 'v4', 12 | V5 = 'v5', 13 | V6 = 'v6' 14 | } 15 | 16 | /** 17 | * Detects the PineScript version from code 18 | * 19 | * @param script The PineScript code 20 | * @returns The detected PineScript version 21 | */ 22 | export function detectVersion(script: string): PineScriptVersion { 23 | // This is a placeholder for future implementation 24 | 25 | // Check for v6 specific features 26 | if (script.includes('//@version=6')) { 27 | return PineScriptVersion.V6; 28 | } 29 | 30 | // Check for v5 specific features 31 | if (script.includes('//@version=5')) { 32 | return PineScriptVersion.V5; 33 | } 34 | 35 | // Check for v4 specific features 36 | if (script.includes('//@version=4')) { 37 | return PineScriptVersion.V4; 38 | } 39 | 40 | // Default to v5 if we can't detect the version 41 | return PineScriptVersion.V5; 42 | } -------------------------------------------------------------------------------- /src/validators/syntaxValidator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PineScript Syntax Validator 3 | * 4 | * Basic validation of PineScript code for common syntax errors 5 | */ 6 | 7 | import { PineScriptVersion, detectVersion } from '../utils/versionDetector.js'; 8 | import { VALIDATION_CONFIG } from '../config/protocolConfig.js'; 9 | 10 | /** 11 | * Validates PineScript code for syntax errors 12 | * 13 | * @param script The PineScript code to validate 14 | * @param version The PineScript version (v4, v5, v6) 15 | * @param context Optional validation context for progress reporting 16 | * @returns Validation result with status and messages 17 | */ 18 | export function validatePineScript( 19 | script: string, 20 | version?: string, 21 | context?: any 22 | ): ValidationResult { 23 | // Default result 24 | const result: ValidationResult = { 25 | valid: true, 26 | errors: [], 27 | warnings: [] 28 | }; 29 | 30 | try { 31 | // Initialize validation timing 32 | const startTime = Date.now(); 33 | let lastProgressReportTime = startTime; 34 | 35 | // Check if the script is too large 36 | if (script.length > VALIDATION_CONFIG.maxScriptSizeWarningThreshold) { 37 | result.warnings.push(`Script is very large (${script.length} characters). This may cause performance issues.`); 38 | } 39 | 40 | // Helper function to check time and report progress 41 | const checkTimeAndProgress = (progress: number, total: number) => { 42 | const currentTime = Date.now(); 43 | 44 | // Check for timeout 45 | if (currentTime - startTime > VALIDATION_CONFIG.maxValidationTime) { 46 | throw new Error(`Validation timeout - script is too complex (exceeded ${VALIDATION_CONFIG.maxValidationTime/1000} seconds)`); 47 | } 48 | 49 | // Report progress at intervals if context is available 50 | if (context?.reportProgress && 51 | currentTime - lastProgressReportTime > VALIDATION_CONFIG.validationCheckInterval) { 52 | context.reportProgress({ progress, total }); 53 | lastProgressReportTime = currentTime; 54 | } 55 | }; 56 | 57 | // If version not provided, try to detect it 58 | const scriptVersion = version || detectVersion(script); 59 | 60 | // Check if script is empty 61 | if (!script || script.trim().length === 0) { 62 | result.valid = false; 63 | result.errors.push('Script is empty'); 64 | return result; 65 | } 66 | 67 | // Check for version comment 68 | if (!script.includes('//@version=')) { 69 | result.warnings.push('Missing version comment (e.g., //@version=5)'); 70 | } 71 | 72 | // Basic validation checks with progress monitoring 73 | 74 | // Check for missing parentheses and brackets 75 | checkTimeAndProgress(1, 5); 76 | validateBalancedParentheses(script, result, checkTimeAndProgress); 77 | 78 | // Check for missing quotes 79 | checkTimeAndProgress(2, 5); 80 | validateBalancedQuotes(script, result, checkTimeAndProgress); 81 | 82 | // Check for missing indicator/strategy declaration 83 | checkTimeAndProgress(3, 5); 84 | validateDeclaration(script, result, scriptVersion); 85 | 86 | // Check for common syntax errors based on version 87 | checkTimeAndProgress(4, 5); 88 | validateVersionSpecificSyntax(script, result, scriptVersion, checkTimeAndProgress); 89 | 90 | // Check line lengths (warning for very long lines) 91 | checkTimeAndProgress(5, 5); 92 | validateLineLength(script, result); 93 | 94 | // Final progress report - validation complete 95 | if (context?.reportProgress) { 96 | context.reportProgress({ progress: 5, total: 5 }); 97 | } 98 | 99 | } catch (error: any) { 100 | // Handle any exceptions during validation 101 | result.valid = false; 102 | result.errors.push(`Validation error: ${error.message || 'Unknown error'}`); 103 | console.error('Validation error:', error); 104 | } 105 | 106 | // If there are errors, mark as invalid 107 | if (result.errors.length > 0) { 108 | result.valid = false; 109 | } 110 | 111 | return result; 112 | } 113 | 114 | /** 115 | * Validates that parentheses and brackets are balanced 116 | */ 117 | function validateBalancedParentheses( 118 | script: string, 119 | result: ValidationResult, 120 | progressCheck: (progress: number, total: number) => void 121 | ): void { 122 | const stack: string[] = []; 123 | const lines = script.split('\n'); 124 | 125 | for (let i = 0; i < lines.length; i++) { 126 | // Check progress and time at intervals 127 | if (i % 100 === 0) { 128 | progressCheck(i, lines.length); 129 | } 130 | 131 | const line = lines[i]; 132 | // Skip comments 133 | if (line.trim().startsWith('//')) continue; 134 | 135 | for (let j = 0; j < line.length; j++) { 136 | const char = line[j]; 137 | 138 | if (char === '(' || char === '[' || char === '{') { 139 | stack.push(char); 140 | } else if (char === ')' || char === ']' || char === '}') { 141 | const lastOpen = stack.pop(); 142 | 143 | if (!lastOpen || 144 | (char === ')' && lastOpen !== '(') || 145 | (char === ']' && lastOpen !== '[') || 146 | (char === '}' && lastOpen !== '{')) { 147 | result.errors.push(`Line ${i+1}: Mismatched bracket or parenthesis`); 148 | } 149 | } 150 | } 151 | } 152 | 153 | if (stack.length > 0) { 154 | result.errors.push(`Unbalanced brackets or parentheses: missing ${stack.length} closing brackets`); 155 | } 156 | } 157 | 158 | /** 159 | * Validates that quotes are balanced 160 | */ 161 | function validateBalancedQuotes( 162 | script: string, 163 | result: ValidationResult, 164 | progressCheck: (progress: number, total: number) => void 165 | ): void { 166 | const lines = script.split('\n'); 167 | 168 | for (let i = 0; i < lines.length; i++) { 169 | // Check progress and time at intervals 170 | if (i % 100 === 0) { 171 | progressCheck(i, lines.length); 172 | } 173 | 174 | let line = lines[i]; 175 | // Skip comments 176 | if (line.trim().startsWith('//')) continue; 177 | 178 | let inSingleQuote = false; 179 | let inDoubleQuote = false; 180 | 181 | for (let j = 0; j < line.length; j++) { 182 | const char = line[j]; 183 | const prevChar = j > 0 ? line[j-1] : ''; 184 | 185 | // Handle quotes, accounting for escaped quotes 186 | if (char === "'" && prevChar !== '\\') { 187 | inSingleQuote = !inSingleQuote; 188 | } else if (char === '"' && prevChar !== '\\') { 189 | inDoubleQuote = !inDoubleQuote; 190 | } 191 | } 192 | 193 | if (inSingleQuote) { 194 | result.errors.push(`Line ${i+1}: Unclosed single quote`); 195 | } 196 | 197 | if (inDoubleQuote) { 198 | result.errors.push(`Line ${i+1}: Unclosed double quote`); 199 | } 200 | } 201 | } 202 | 203 | /** 204 | * Validates indicator or strategy declaration 205 | */ 206 | function validateDeclaration(script: string, result: ValidationResult, version: string): void { 207 | if (!script.includes('indicator(') && !script.includes('strategy(') && !script.includes('library(')) { 208 | result.errors.push('Missing indicator/strategy/library declaration'); 209 | } 210 | 211 | // Check if the script contains both indicator and strategy declarations 212 | const hasIndicator = /\bindicator\s*\(/g.test(script); 213 | const hasStrategy = /\bstrategy\s*\(/g.test(script); 214 | const hasLibrary = /\blibrary\s*\(/g.test(script); 215 | 216 | if ((hasIndicator && hasStrategy) || 217 | (hasIndicator && hasLibrary) || 218 | (hasStrategy && hasLibrary)) { 219 | result.errors.push('Script cannot contain multiple declarations (indicator/strategy/library)'); 220 | } 221 | } 222 | 223 | /** 224 | * Validates version-specific syntax 225 | */ 226 | function validateVersionSpecificSyntax( 227 | script: string, 228 | result: ValidationResult, 229 | version: string, 230 | progressCheck: (progress: number, total: number) => void 231 | ): void { 232 | // Convert version string to number for comparison 233 | const versionNum = parseInt(version.replace(/\D/g, '')); 234 | 235 | // v3 syntax validation 236 | if (versionNum <= 3) { 237 | // Check for array access with [] which was introduced in v4 238 | if (/\w+\s*\[\s*\d+\s*\]/g.test(script)) { 239 | result.errors.push('Array access with [] is not supported in PineScript v3 and earlier'); 240 | } 241 | } 242 | 243 | // v4 and newer syntax validation 244 | if (versionNum >= 4) { 245 | // Check for old-style security calls 246 | if (/security\s*\(\s*[^,]+\s*,[^,]+\s*,[^,)]+\)/g.test(script)) { 247 | // This is just a warning since it might still work 248 | result.warnings.push('Using old-style security() function. Consider updating to request.security() for v5 and above'); 249 | } 250 | } 251 | 252 | // v5 syntax validation 253 | if (versionNum >= 5) { 254 | // Check for use of := without var declaration 255 | const varDeclared = new Set(); 256 | const lines = script.split('\n'); 257 | 258 | for (let i = 0; i < lines.length; i++) { 259 | // Check progress and time at intervals 260 | if (i % 100 === 0) { 261 | progressCheck(i, lines.length); 262 | } 263 | 264 | const line = lines[i]; 265 | // Skip comments 266 | if (line.trim().startsWith('//')) continue; 267 | 268 | // Capture variable declarations 269 | const varMatch = line.match(/\bvar\s+(\w+)/); 270 | if (varMatch) { 271 | varDeclared.add(varMatch[1]); 272 | } 273 | 274 | // Check for reassignment without var 275 | const reassignMatch = line.match(/\b(\w+)\s*:=/); 276 | if (reassignMatch && !varDeclared.has(reassignMatch[1])) { 277 | result.warnings.push(`Line ${i+1}: Using := without 'var' declaration for '${reassignMatch[1]}'. This may cause issues.`); 278 | } 279 | } 280 | } 281 | } 282 | 283 | /** 284 | * Validates line length (warns for very long lines) 285 | */ 286 | function validateLineLength(script: string, result: ValidationResult): void { 287 | const lines = script.split('\n'); 288 | const MAX_LINE_LENGTH = 120; 289 | 290 | for (let i = 0; i < lines.length; i++) { 291 | if (lines[i].length > MAX_LINE_LENGTH) { 292 | result.warnings.push(`Line ${i+1} exceeds maximum recommended length (${lines[i].length} > ${MAX_LINE_LENGTH})`); 293 | } 294 | } 295 | } 296 | 297 | /** 298 | * Result of PineScript validation 299 | */ 300 | export interface ValidationResult { 301 | /** Whether the script is valid */ 302 | valid: boolean; 303 | /** Array of error messages */ 304 | errors: string[]; 305 | /** Array of warning messages */ 306 | warnings: string[]; 307 | } -------------------------------------------------------------------------------- /tests/config/configTool.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for configuration tools 3 | */ 4 | 5 | import { jest } from '@jest/globals'; 6 | import { FastMCP } from 'fastmcp'; 7 | import { registerConfigTools } from '../../src/config/configTool'; 8 | import { loadUserConfig, saveUserConfig, resetConfig, PineScriptConfig } from '../../src/config/userConfig'; 9 | 10 | // Define interface for mock tool 11 | interface MockTool { 12 | name: string; 13 | execute: (params: any) => Promise; 14 | } 15 | 16 | // Mock FastMCP 17 | jest.mock('fastmcp', () => ({ 18 | FastMCP: jest.fn().mockImplementation(() => ({ 19 | addTool: jest.fn() 20 | })) 21 | })); 22 | 23 | // Mock userConfig functions 24 | jest.mock('../../src/config/userConfig', () => ({ 25 | loadUserConfig: jest.fn(), 26 | saveUserConfig: jest.fn(), 27 | resetConfig: jest.fn(), 28 | PineScriptConfig: {} 29 | })); 30 | 31 | describe('PineScript Configuration Tools', () => { 32 | let mcp: { addTool: jest.Mock }; 33 | 34 | beforeEach(() => { 35 | jest.clearAllMocks(); 36 | 37 | mcp = { addTool: jest.fn() }; 38 | 39 | // Set up default mock implementations 40 | (loadUserConfig as jest.Mock).mockReturnValue({ 41 | templates: { 42 | defaultVersion: 5, 43 | customTemplatesDir: null 44 | }, 45 | validation: { 46 | strictMode: false 47 | } 48 | }); 49 | 50 | (saveUserConfig as jest.Mock).mockImplementation((newConfig: Partial) => { 51 | return { 52 | templates: { 53 | defaultVersion: 5, 54 | customTemplatesDir: null 55 | }, 56 | validation: { 57 | strictMode: false, 58 | ...(newConfig.validation || {}) 59 | }, 60 | ...(newConfig as any) 61 | }; 62 | }); 63 | 64 | (resetConfig as jest.Mock).mockReturnValue({ 65 | templates: { 66 | defaultVersion: 5, 67 | customTemplatesDir: null 68 | }, 69 | validation: { 70 | strictMode: false 71 | } 72 | }); 73 | }); 74 | 75 | it('should register all configuration tools with MCP', () => { 76 | // Action 77 | registerConfigTools(mcp as any); 78 | 79 | // Assert 80 | expect(mcp.addTool).toHaveBeenCalledTimes(5); 81 | 82 | // Verify each tool was registered 83 | const calls = mcp.addTool.mock.calls; 84 | expect(calls[0][0].name).toBe('get_pinescript_config'); 85 | expect(calls[1][0].name).toBe('update_pinescript_config'); 86 | expect(calls[2][0].name).toBe('reset_pinescript_config'); 87 | expect(calls[3][0].name).toBe('get_config_section'); 88 | expect(calls[4][0].name).toBe('set_templates_directory'); 89 | }); 90 | 91 | it('should get current configuration', async () => { 92 | // Setup 93 | registerConfigTools(mcp as any); 94 | const getTool = mcp.addTool.mock.calls.find( 95 | call => call[0].name === 'get_pinescript_config' 96 | )?.[0] as MockTool; 97 | 98 | if (!getTool) { 99 | throw new Error('Tool not found'); 100 | } 101 | 102 | // Action 103 | const result = await getTool.execute({}); 104 | const parsedResult = JSON.parse(result); 105 | 106 | // Assert 107 | expect(loadUserConfig).toHaveBeenCalled(); 108 | expect(parsedResult.success).toBe(true); 109 | expect(parsedResult.config).toBeDefined(); 110 | expect(parsedResult.config.templates.defaultVersion).toBe(5); 111 | }); 112 | 113 | it('should update configuration settings', async () => { 114 | // Setup 115 | registerConfigTools(mcp as any); 116 | const updateTool = mcp.addTool.mock.calls.find( 117 | call => call[0].name === 'update_pinescript_config' 118 | )?.[0] as MockTool; 119 | 120 | if (!updateTool) { 121 | throw new Error('Tool not found'); 122 | } 123 | 124 | const newConfig = { 125 | validation: { 126 | strictMode: true 127 | } 128 | }; 129 | 130 | // Action 131 | const result = await updateTool.execute({ config: newConfig }); 132 | const parsedResult = JSON.parse(result); 133 | 134 | // Assert 135 | expect(saveUserConfig).toHaveBeenCalledWith(newConfig); 136 | expect(parsedResult.success).toBe(true); 137 | expect(parsedResult.message).toContain('updated successfully'); 138 | expect(parsedResult.config).toBeDefined(); 139 | }); 140 | 141 | it('should reset configuration to defaults', async () => { 142 | // Setup 143 | registerConfigTools(mcp as any); 144 | const resetTool = mcp.addTool.mock.calls.find( 145 | call => call[0].name === 'reset_pinescript_config' 146 | )?.[0] as MockTool; 147 | 148 | if (!resetTool) { 149 | throw new Error('Tool not found'); 150 | } 151 | 152 | // Action 153 | const result = await resetTool.execute({}); 154 | const parsedResult = JSON.parse(result); 155 | 156 | // Assert 157 | expect(resetConfig).toHaveBeenCalled(); 158 | expect(parsedResult.success).toBe(true); 159 | expect(parsedResult.message).toContain('reset to defaults'); 160 | expect(parsedResult.config).toBeDefined(); 161 | }); 162 | 163 | it('should get a specific configuration section', async () => { 164 | // Setup 165 | registerConfigTools(mcp as any); 166 | const getSectionTool = mcp.addTool.mock.calls.find( 167 | call => call[0].name === 'get_config_section' 168 | )?.[0] as MockTool; 169 | 170 | if (!getSectionTool) { 171 | throw new Error('Tool not found'); 172 | } 173 | 174 | // Action 175 | const result = await getSectionTool.execute({ section: 'templates' }); 176 | const parsedResult = JSON.parse(result); 177 | 178 | // Assert 179 | expect(loadUserConfig).toHaveBeenCalled(); 180 | expect(parsedResult.success).toBe(true); 181 | expect(parsedResult.config).toBeDefined(); 182 | expect(parsedResult.config.defaultVersion).toBe(5); 183 | }); 184 | 185 | it('should return error when requesting invalid section', async () => { 186 | // Setup 187 | registerConfigTools(mcp as any); 188 | const getSectionTool = mcp.addTool.mock.calls.find( 189 | call => call[0].name === 'get_config_section' 190 | )?.[0] as MockTool; 191 | 192 | if (!getSectionTool) { 193 | throw new Error('Tool not found'); 194 | } 195 | 196 | // Action 197 | const result = await getSectionTool.execute({ section: 'nonexistent' }); 198 | const parsedResult = JSON.parse(result); 199 | 200 | // Assert 201 | expect(parsedResult.success).toBe(false); 202 | expect(parsedResult.message).toContain('not found'); 203 | expect(parsedResult.availableSections).toBeDefined(); 204 | }); 205 | 206 | it('should set custom templates directory', async () => { 207 | // Setup 208 | registerConfigTools(mcp as any); 209 | const setDirectoryTool = mcp.addTool.mock.calls.find( 210 | call => call[0].name === 'set_templates_directory' 211 | )?.[0] as MockTool; 212 | 213 | if (!setDirectoryTool) { 214 | throw new Error('Tool not found'); 215 | } 216 | 217 | // Action 218 | const result = await setDirectoryTool.execute({ directory: '/custom/path' }); 219 | const parsedResult = JSON.parse(result); 220 | 221 | // Assert 222 | expect(saveUserConfig).toHaveBeenCalled(); 223 | expect(parsedResult.success).toBe(true); 224 | expect(parsedResult.message).toContain('Templates directory set'); 225 | expect(parsedResult.config).toBeDefined(); 226 | }); 227 | }); -------------------------------------------------------------------------------- /tests/config/userConfig.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for user configuration system 3 | */ 4 | 5 | import { jest } from '@jest/globals'; 6 | import fs from 'fs'; 7 | import path from 'path'; 8 | import os from 'os'; 9 | import { 10 | PineScriptConfig, 11 | loadUserConfig, 12 | saveUserConfig, 13 | resetConfig 14 | } from '../../src/config/userConfig'; 15 | 16 | // Mock fs module 17 | jest.mock('fs', () => ({ 18 | existsSync: jest.fn(), 19 | mkdirSync: jest.fn(), 20 | writeFileSync: jest.fn(), 21 | readFileSync: jest.fn(), 22 | })); 23 | 24 | // Mock path module 25 | jest.mock('path', () => ({ 26 | join: jest.fn((a, b, c) => c ? `${a}/${b}/${c}` : `${a}/${b}`), 27 | })); 28 | 29 | // Mock os module 30 | jest.mock('os', () => ({ 31 | homedir: jest.fn(() => '/mock/home'), 32 | })); 33 | 34 | describe('PineScript User Configuration', () => { 35 | // Setup before each test 36 | beforeEach(() => { 37 | // Reset all mocks 38 | jest.clearAllMocks(); 39 | 40 | // Default mock for path.join 41 | (path.join as jest.Mock).mockImplementation((a, b, c) => { 42 | if (c) return `${a}/${b}/${c}`; 43 | return b ? `${a}/${b}` : a; 44 | }); 45 | 46 | // Default mock for os.homedir 47 | (os.homedir as jest.Mock).mockReturnValue('/mock/home'); 48 | 49 | // Default fs.existsSync to return true 50 | (fs.existsSync as jest.Mock).mockReturnValue(true); 51 | 52 | // Default mock implementation for fs.readFileSync 53 | (fs.readFileSync as jest.Mock).mockImplementation(() => { 54 | return JSON.stringify({ 55 | templates: { 56 | defaultVersion: 5, 57 | customTemplatesDir: '/custom/templates' 58 | } 59 | }); 60 | }); 61 | }); 62 | 63 | it('should load default configuration when no config file exists', () => { 64 | // Mock fs.existsSync to return false (config doesn't exist) 65 | (fs.existsSync as jest.Mock).mockReturnValueOnce(false).mockReturnValueOnce(false); 66 | 67 | const config = loadUserConfig(); 68 | 69 | // Verify directory creation 70 | expect(fs.mkdirSync).toHaveBeenCalledWith('/mock/home/.pinescript-mcp', { recursive: true }); 71 | expect(fs.mkdirSync).toHaveBeenCalledWith('/mock/home/.pinescript-mcp/versions', { recursive: true }); 72 | 73 | // Verify default config values 74 | expect(config.templates.defaultVersion).toBe(5); 75 | expect(config.validation.strictMode).toBe(false); 76 | expect(config.errorFixer.autoFixEnabled).toBe(true); 77 | 78 | // Verify config file was written 79 | expect(fs.writeFileSync).toHaveBeenCalled(); 80 | }); 81 | 82 | it('should load configuration from file when it exists', () => { 83 | const config = loadUserConfig(); 84 | 85 | // Verify config was loaded from file 86 | expect(fs.readFileSync).toHaveBeenCalledWith('/mock/home/.pinescript-mcp/config.json', 'utf8'); 87 | 88 | // Verify merged config values 89 | expect(config.templates.defaultVersion).toBe(5); 90 | expect(config.templates.customTemplatesDir).toBe('/custom/templates'); 91 | }); 92 | 93 | it('should save configuration updates', () => { 94 | // Setup 95 | const configUpdates = { 96 | templates: { 97 | defaultVersion: 6 98 | } as any, // Type assertion to avoid TypeScript requiring all fields 99 | validation: { 100 | strictMode: true 101 | } 102 | }; 103 | 104 | // Action 105 | const updatedConfig = saveUserConfig(configUpdates as Partial); 106 | 107 | // Verify config was loaded first 108 | expect(fs.readFileSync).toHaveBeenCalledWith('/mock/home/.pinescript-mcp/config.json', 'utf8'); 109 | 110 | // Verify merged config was saved 111 | expect(fs.writeFileSync).toHaveBeenCalled(); 112 | expect(updatedConfig.templates.defaultVersion).toBe(6); 113 | expect(updatedConfig.templates.customTemplatesDir).toBe('/custom/templates'); 114 | expect(updatedConfig.validation.strictMode).toBe(true); 115 | }); 116 | 117 | it('should reset configuration to defaults', () => { 118 | // Action 119 | const defaultConfig = resetConfig(); 120 | 121 | // Verify config file was written with defaults 122 | expect(fs.writeFileSync).toHaveBeenCalled(); 123 | 124 | // Verify default values 125 | expect(defaultConfig.templates.defaultVersion).toBe(5); 126 | expect(defaultConfig.templates.customTemplatesDir).toBe(null); 127 | expect(defaultConfig.validation.strictMode).toBe(false); 128 | expect(defaultConfig.errorFixer.autoFixEnabled).toBe(true); 129 | }); 130 | 131 | it('should handle errors when loading configuration', () => { 132 | // Mock readFileSync to throw an error 133 | (fs.readFileSync as jest.Mock).mockImplementationOnce(() => { 134 | throw new Error('Mock file read error'); 135 | }); 136 | 137 | // Mock console.warn to prevent test output pollution 138 | const originalConsoleWarn = console.warn; 139 | console.warn = jest.fn(); 140 | 141 | // Action 142 | const config = loadUserConfig(); 143 | 144 | // Verify warning was logged 145 | expect(console.warn).toHaveBeenCalled(); 146 | 147 | // Verify default config was returned 148 | expect(config.templates.defaultVersion).toBe(5); 149 | expect(config.templates.customTemplatesDir).toBe(null); 150 | 151 | // Restore console.warn 152 | console.warn = originalConsoleWarn; 153 | }); 154 | 155 | it('should handle errors when saving configuration', () => { 156 | // Mock writeFileSync to throw an error 157 | (fs.writeFileSync as jest.Mock).mockImplementationOnce(() => { 158 | throw new Error('Mock file write error'); 159 | }); 160 | 161 | // Mock console.error to prevent test output pollution 162 | const originalConsoleError = console.error; 163 | console.error = jest.fn(); 164 | 165 | // Action 166 | const config = saveUserConfig({ 167 | templates: { 168 | defaultVersion: 6 169 | } as any // Type assertion to avoid TypeScript requiring all fields 170 | } as Partial); 171 | 172 | // Verify error was logged 173 | expect(console.error).toHaveBeenCalled(); 174 | 175 | // Verify current config was returned 176 | expect(config.templates.defaultVersion).toBe(5); 177 | 178 | // Restore console.error 179 | console.error = originalConsoleError; 180 | }); 181 | }); -------------------------------------------------------------------------------- /tests/fixers/errorFixer.test.ts: -------------------------------------------------------------------------------- 1 | import { fixPineScriptErrors } from '../../src/fixers/errorFixer'; 2 | 3 | describe('PineScript Error Fixer', () => { 4 | it('should fix missing version annotation', () => { 5 | // Given 6 | const script = ` 7 | indicator("My Indicator", overlay=true) 8 | study("My Study") 9 | `; 10 | 11 | // When 12 | const result = fixPineScriptErrors(script); 13 | 14 | // Then 15 | expect(result.fixed).toBe(true); 16 | expect(result.changes).toContain('Added missing version annotation (@version=5)'); 17 | expect(result.script).toContain('//@version=5'); 18 | }); 19 | 20 | it('should fix unbalanced parentheses', () => { 21 | // Given 22 | const script = ` 23 | //@version=5 24 | indicator("My Indicator", overlay=true 25 | `; 26 | 27 | // When 28 | const result = fixPineScriptErrors(script); 29 | 30 | // Then 31 | expect(result.fixed).toBe(true); 32 | // Either the line-specific fix or the function-call specific pattern may be used 33 | expect(result.changes.some(change => 34 | change.includes('missing closing parenthesis') || 35 | change.includes('parenthesis/es on line') || 36 | change.includes('function call') 37 | )).toBe(true); 38 | // Just check that a closing parenthesis was added somewhere 39 | expect(result.script.split(')').length).toBeGreaterThan(script.split(')').length); 40 | }); 41 | 42 | it('should fix unclosed string literals', () => { 43 | // Given 44 | const script = ` 45 | //@version=5 46 | indicator("My Indicator, overlay=true) 47 | `; 48 | 49 | // When 50 | const result = fixPineScriptErrors(script); 51 | 52 | // Then 53 | expect(result.fixed).toBe(true); 54 | expect(result.changes.some(change => change.includes('string literal'))).toBe(true); 55 | }); 56 | 57 | it('should fix missing commas in function calls', () => { 58 | // Given 59 | const script = ` 60 | //@version=5 61 | input(14 "RSI Length") 62 | `; 63 | 64 | // When 65 | const result = fixPineScriptErrors(script); 66 | 67 | // Then 68 | expect(result.fixed).toBe(true); 69 | expect(result.changes.some(change => change.includes('missing comma'))).toBe(true); 70 | expect(result.script).toContain('input(14, "RSI Length")'); 71 | }); 72 | 73 | it('should fix deprecated study() function', () => { 74 | // Given 75 | const script = ` 76 | //@version=5 77 | study("My Study", overlay=true) 78 | `; 79 | 80 | // When 81 | const result = fixPineScriptErrors(script); 82 | 83 | // Then 84 | expect(result.fixed).toBe(true); 85 | expect(result.changes).toContain('Replaced deprecated study() function with indicator()'); 86 | expect(result.script).toContain('indicator("My Study", overlay=true)'); 87 | }); 88 | 89 | it('should fix incorrect variable export syntax', () => { 90 | // Given 91 | const script = ` 92 | //@version=5 93 | export var myVar = 10 94 | `; 95 | 96 | // When 97 | const result = fixPineScriptErrors(script); 98 | 99 | // Then 100 | expect(result.fixed).toBe(true); 101 | expect(result.changes.some(change => change.includes('export syntax'))).toBe(true); 102 | expect(result.script).toContain('var myVar = 10'); 103 | expect(result.script).toContain('export myVar'); 104 | }); 105 | 106 | it('should fix multiple errors at once', () => { 107 | // Given 108 | const script = ` 109 | study("My Indicator, overlay=true 110 | export var rsiLength = 14 111 | input(14 "RSI Length") 112 | `; 113 | 114 | // When 115 | const result = fixPineScriptErrors(script); 116 | 117 | // Then 118 | expect(result.fixed).toBe(true); 119 | expect(result.changes.length).toBeGreaterThan(2); 120 | }); 121 | 122 | it('should handle empty scripts', () => { 123 | // Given 124 | const script = ''; 125 | 126 | // When 127 | const result = fixPineScriptErrors(script); 128 | 129 | // Then 130 | expect(result.fixed).toBe(false); 131 | expect(result.changes).toEqual([]); 132 | expect(result.script).toEqual(''); 133 | }); 134 | 135 | it('should return original script if no errors are detected', () => { 136 | // Given 137 | const script = ` 138 | //@version=5 139 | indicator("My Perfect Indicator", overlay=true) 140 | var value = 100 141 | plot(value, "Price", color=color.blue) 142 | `; 143 | 144 | // When 145 | const result = fixPineScriptErrors(script); 146 | 147 | // Then 148 | // Our implementation may fix minor formatting even in "perfect" scripts 149 | // Instead of checking exact content, ensure key elements are preserved 150 | expect(result.script).toContain('indicator("My Perfect Indicator"'); 151 | expect(result.script).toContain('var value = 100'); 152 | expect(result.script).toContain('plot(value, "Price"'); 153 | // The color parameter might be formatted differently 154 | expect(result.script).toContain('color=color'); 155 | }); 156 | }); -------------------------------------------------------------------------------- /tests/integration/realWorldTests.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { describe, expect, test, beforeAll, afterAll } from '@jest/globals'; 4 | import { validatePineScript } from '../../src/validators/syntaxValidator'; 5 | import { formatPineScript, FormatOptions } from '../../src/utils/formatter'; 6 | import { VersionManager, ScriptVersion } from '../../src/utils/versionManager'; 7 | import { fixPineScriptErrors } from '../../src/fixers/errorFixer'; 8 | import { detectVersion, PineScriptVersion } from '../../src/utils/versionDetector'; 9 | 10 | const EXAMPLES_DIR = path.join(process.cwd(), 'examples'); 11 | 12 | // Test matrix defines our real-world examples 13 | const testMatrix = [ 14 | { 15 | name: "Gold Scalping Strategy", 16 | file: "Example_PineScriptStrat1_1hr.txt", 17 | version: PineScriptVersion.V6, 18 | complexity: "medium", 19 | features: ["strategy", "indicators", "conditionals", "plots"] 20 | } 21 | // Add more examples as they become available 22 | ]; 23 | 24 | // Timing utility for performance measurements 25 | function timeExecution(fn: Function): { result: any, timeMs: number } { 26 | const start = performance.now(); 27 | const result = fn(); 28 | const end = performance.now(); 29 | return { result, timeMs: end - start }; 30 | } 31 | 32 | describe('Real-world PineScript Examples', () => { 33 | const results: Record = {}; 34 | 35 | // Load all example scripts before tests 36 | beforeAll(() => { 37 | testMatrix.forEach(example => { 38 | const filePath = path.join(EXAMPLES_DIR, example.file); 39 | results[example.name] = { 40 | script: fs.readFileSync(filePath, 'utf8'), 41 | metrics: {}, 42 | validationResults: null, 43 | formattingResults: {}, 44 | versionResults: {}, 45 | fixerResults: {} 46 | }; 47 | 48 | // Automatically detect the version as a sanity check 49 | const { result, timeMs } = timeExecution(() => 50 | detectVersion(results[example.name].script) 51 | ); 52 | 53 | results[example.name].detectedVersion = result; 54 | results[example.name].metrics.versionDetection = timeMs; 55 | }); 56 | }); 57 | 58 | // Write test results to a report file after all tests complete 59 | afterAll(() => { 60 | const reportPath = path.join(process.cwd(), 'tests', 'integration', 'test-report.json'); 61 | fs.writeFileSync(reportPath, JSON.stringify(results, null, 2)); 62 | console.log(`Test results written to ${reportPath}`); 63 | }); 64 | 65 | // Run tests for each example 66 | testMatrix.forEach(example => { 67 | describe(`Example: ${example.name}`, () => { 68 | 69 | test('should correctly detect PineScript version', () => { 70 | const exampleData = results[example.name]; 71 | const detectedVersion = exampleData.detectedVersion; 72 | 73 | expect(detectedVersion).toBe(example.version); 74 | console.log(`Version detection took ${exampleData.metrics.versionDetection}ms`); 75 | }); 76 | 77 | test('should pass validation without errors', () => { 78 | const exampleData = results[example.name]; 79 | 80 | const { result, timeMs } = timeExecution(() => 81 | validatePineScript(exampleData.script) 82 | ); 83 | 84 | exampleData.validationResults = result; 85 | exampleData.metrics.validation = timeMs; 86 | 87 | console.log(`Validation took ${timeMs}ms`); 88 | expect(result.valid).toBe(true); 89 | expect(result.errors.length).toBe(0); 90 | }); 91 | 92 | test('should format script while preserving functionality', () => { 93 | const exampleData = results[example.name]; 94 | 95 | // Test with default options 96 | const defaultOptions: FormatOptions = { 97 | indentSize: 2, 98 | useSpaces: true, 99 | spacesAroundOperators: true, 100 | spaceAfterCommas: true, 101 | maxLineLength: 80, 102 | keepBlankLines: true, 103 | bracesOnNewLine: false, 104 | alignComments: true, 105 | updateVersionComment: true 106 | }; 107 | 108 | const { result: defaultResult, timeMs: defaultTime } = timeExecution(() => 109 | formatPineScript(exampleData.script, defaultOptions) 110 | ); 111 | 112 | exampleData.formattingResults.default = defaultResult; 113 | exampleData.metrics.formattingDefault = defaultTime; 114 | 115 | // Verify the formatted script still passes validation 116 | const validationAfterFormat = validatePineScript(defaultResult.formatted); 117 | 118 | expect(validationAfterFormat.valid).toBe(true); 119 | expect(defaultResult.warnings.length).toBe(0); // Ideally no warnings 120 | console.log(`Default formatting took ${defaultTime}ms`); 121 | 122 | // Test with minimal options 123 | const minimalOptions: FormatOptions = { 124 | indentSize: 4, 125 | useSpaces: false, 126 | spacesAroundOperators: false, 127 | spaceAfterCommas: false, 128 | maxLineLength: 120, 129 | keepBlankLines: false, 130 | bracesOnNewLine: true, 131 | alignComments: false, 132 | updateVersionComment: false 133 | }; 134 | 135 | const { result: minimalResult, timeMs: minimalTime } = timeExecution(() => 136 | formatPineScript(exampleData.script, minimalOptions) 137 | ); 138 | 139 | exampleData.formattingResults.minimal = minimalResult; 140 | exampleData.metrics.formattingMinimal = minimalTime; 141 | 142 | // Verify the formatted script still passes validation 143 | const validationAfterMinimal = validatePineScript(minimalResult.formatted); 144 | 145 | expect(validationAfterMinimal.valid).toBe(true); 146 | console.log(`Minimal formatting took ${minimalTime}ms`); 147 | }); 148 | 149 | test('should handle version conversion', () => { 150 | const exampleData = results[example.name]; 151 | const versionManager = new VersionManager(); 152 | 153 | // Try downgrading to v5 if possible 154 | if (example.version === PineScriptVersion.V6) { 155 | const { result, timeMs } = timeExecution(() => 156 | versionManager.upgradeVersion(exampleData.script, PineScriptVersion.V5) 157 | ); 158 | 159 | exampleData.versionResults.downgradeToV5 = result; 160 | exampleData.metrics.downgradeToV5 = timeMs; 161 | 162 | // Verify the downgraded script passes validation 163 | const validationAfterDowngrade = validatePineScript(result); 164 | 165 | // Some features might not be convertible, so we log but don't necessarily expect zero errors 166 | console.log(`Downgrade to v5 took ${timeMs}ms with ${validationAfterDowngrade.errors.length} validation errors`); 167 | } 168 | 169 | // Try upgrading to v6 if applicable 170 | if (example.version === PineScriptVersion.V5 || example.version === PineScriptVersion.V4) { 171 | const { result, timeMs } = timeExecution(() => 172 | versionManager.upgradeVersion(exampleData.script, PineScriptVersion.V6) 173 | ); 174 | 175 | exampleData.versionResults.upgradeToV6 = result; 176 | exampleData.metrics.upgradeToV6 = timeMs; 177 | 178 | // Verify the upgraded script passes validation 179 | const validationAfterUpgrade = validatePineScript(result); 180 | 181 | expect(validationAfterUpgrade.valid).toBe(true); 182 | console.log(`Upgrade to v6 took ${timeMs}ms`); 183 | } 184 | }); 185 | 186 | test('should fix intentionally introduced errors', () => { 187 | const exampleData = results[example.name]; 188 | 189 | // Introduce a common error: missing version annotation 190 | let scriptWithError = exampleData.script.replace(/\/\/@version=\d+/, ''); 191 | 192 | const { result, timeMs } = timeExecution(() => 193 | fixPineScriptErrors(scriptWithError) 194 | ); 195 | 196 | exampleData.fixerResults.missingVersion = result; 197 | exampleData.metrics.fixMissingVersion = timeMs; 198 | 199 | expect(result.fixed).toBe(true); 200 | expect(result.fixedScript).toContain('//@version='); 201 | console.log(`Fix missing version took ${timeMs}ms`); 202 | 203 | // Introduce another error: unbalanced parentheses 204 | scriptWithError = exampleData.script.replace('strategy("Gold Scalping BOS & CHoCH"', 'strategy("Gold Scalping BOS & CHoCH"'); 205 | 206 | const { result: result2, timeMs: timeMs2 } = timeExecution(() => 207 | fixPineScriptErrors(scriptWithError) 208 | ); 209 | 210 | exampleData.fixerResults.unbalancedParentheses = result2; 211 | exampleData.metrics.fixUnbalancedParentheses = timeMs2; 212 | 213 | expect(result2.fixed).toBe(true); 214 | console.log(`Fix unbalanced parentheses took ${timeMs2}ms`); 215 | }); 216 | 217 | test('should handle multiple formatting passes without degradation', () => { 218 | const exampleData = results[example.name]; 219 | const options: FormatOptions = { 220 | indentSize: 2, 221 | useSpaces: true, 222 | spacesAroundOperators: true, 223 | spaceAfterCommas: true, 224 | maxLineLength: 80, 225 | keepBlankLines: true, 226 | bracesOnNewLine: false, 227 | alignComments: true, 228 | updateVersionComment: true 229 | }; 230 | 231 | // First formatting pass 232 | const firstPass = formatPineScript(exampleData.script, options); 233 | 234 | // Second formatting pass on the already formatted code 235 | const secondPass = formatPineScript(firstPass.formatted, options); 236 | 237 | // Third formatting pass 238 | const thirdPass = formatPineScript(secondPass.formatted, options); 239 | 240 | // The second and third passes should not change the code significantly 241 | expect(secondPass.formatted).toBe(firstPass.formatted); 242 | expect(thirdPass.formatted).toBe(secondPass.formatted); 243 | }); 244 | }); 245 | }); 246 | }); -------------------------------------------------------------------------------- /tests/manual/test-basic.js: -------------------------------------------------------------------------------- 1 | // Basic test script for verification 2 | 3 | console.log('Basic test script executed successfully!'); 4 | console.log('Current working directory:', process.cwd()); -------------------------------------------------------------------------------- /tests/manual/test-complex-validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test script to validate the complex Gold Scalping PineScript strategy 3 | */ 4 | 5 | // Import the validator directly 6 | import { validatePineScript } from '../../src/validators/syntaxValidator.js'; 7 | 8 | // The Gold Scalping strategy with MA Filter 9 | const complexScript = `//@version=5 10 | strategy("Gold Scalping BOS & CHoCH with MA Filter", overlay=true) 11 | 12 | // Inputs 13 | useEMA = input(true, title="Use EMA Filter") 14 | fastLength = input(20, title="Fast EMA Length") 15 | slowLength = input(50, title="Slow EMA Length") 16 | atrPeriod = input(14, title="ATR Period") 17 | atrMultiplier = input(1.5, title="ATR Multiplier") 18 | takeProfitMultiplier = input(2, title="Take Profit ATR Multiplier") 19 | 20 | // EMA Filter 21 | fastEMA = ta.ema(close, fastLength) 22 | slowEMA = ta.ema(close, slowLength) 23 | uptrend = fastEMA > slowEMA 24 | downtrend = fastEMA < slowEMA 25 | 26 | // ATR for stop loss and take profit 27 | atrValue = ta.atr(atrPeriod) 28 | stopDistance = atrValue * atrMultiplier 29 | tpDistance = atrValue * takeProfitMultiplier 30 | 31 | // Swing High/Low Detection 32 | swingLength = 5 33 | highestHigh = ta.highest(high, swingLength) 34 | lowestLow = ta.lowest(low, swingLength) 35 | 36 | // Detect Break of Structure (BOS) 37 | bosUp = false 38 | bosDown = false 39 | var float prevLow = na 40 | var float prevHigh = na 41 | 42 | if low[swingLength] == lowestLow[1] and not na(lowestLow[1]) 43 | prevLow := low[swingLength] 44 | if close > prevHigh and uptrend 45 | bosUp := true 46 | 47 | if high[swingLength] == highestHigh[1] and not na(highestHigh[1]) 48 | prevHigh := high[swingLength] 49 | if close < prevLow and downtrend 50 | bosDown := true 51 | 52 | // Detect Change of Character (CHoCH) 53 | var float lastHighPoint = na 54 | var float lastLowPoint = na 55 | chochUp = false 56 | chochDown = false 57 | 58 | if bosUp 59 | lastLowPoint := prevLow 60 | if not na(lastLowPoint) and close > lastLowPoint 61 | chochUp := true 62 | 63 | if bosDown 64 | lastHighPoint := prevHigh 65 | if not na(lastHighPoint) and close < lastHighPoint 66 | chochDown := true 67 | 68 | // Trading Conditions 69 | longCondition = bosUp and chochUp and uptrend 70 | shortCondition = bosDown and chochDown and downtrend 71 | 72 | // Strategy Execution 73 | if longCondition 74 | strategy.entry("Long", strategy.long) 75 | strategy.exit("TP/SL", "Long", profit = tpDistance, loss = stopDistance) 76 | 77 | if shortCondition 78 | strategy.entry("Short", strategy.short) 79 | strategy.exit("TP/SL", "Short", profit = tpDistance, loss = stopDistance) 80 | 81 | // Plotting 82 | plot(fastEMA, "Fast EMA", color=color.blue) 83 | plot(slowEMA, "Slow EMA", color=color.red) 84 | plotshape(bosUp and uptrend, "BOS Up", style=shape.triangleup, location=location.belowbar, color=color.green, size=size.small) 85 | plotshape(bosDown and downtrend, "BOS Down", style=shape.triangledown, location=location.abovebar, color=color.red, size=size.small) 86 | plotshape(chochUp and uptrend, "CHoCH Up", style=shape.circle, location=location.belowbar, color=color.green, size=size.small) 87 | plotshape(chochDown and downtrend, "CHoCH Down", style=shape.circle, location=location.abovebar, color=color.red, size=size.small) 88 | 89 | // Draw support and resistance levels 90 | plot(prevLow, "Support", color = uptrend ? color.green : color.gray, style = plot.style_circles) 91 | plot(prevHigh, "Resistance", color = downtrend ? color.red : color.gray, style = plot.style_circles)`; 92 | 93 | // Create a progress reporting context 94 | const progressContext = { 95 | reportProgress: (progress) => { 96 | console.log(`Validation progress: ${progress.progress}/${progress.total}`); 97 | } 98 | }; 99 | 100 | // Print script size info 101 | console.log(`Testing complex script (${complexScript.length} characters, ${complexScript.split('\n').length} lines)`); 102 | 103 | // Start timer 104 | console.time('Validation time'); 105 | 106 | // Validate it with our progress context 107 | console.log('\nValidating PineScript...'); 108 | try { 109 | const result = validatePineScript(complexScript, '5', progressContext); 110 | console.log('Validation result:', JSON.stringify(result, null, 2)); 111 | 112 | if (result.valid) { 113 | console.log('\nValidation successful - script is syntactically correct!'); 114 | } else { 115 | console.log('\nValidation failed with errors:', result.errors); 116 | if (result.warnings.length > 0) { 117 | console.log('Warnings:', result.warnings); 118 | } 119 | } 120 | } catch (error) { 121 | console.error('Validation error:', error); 122 | } 123 | 124 | // End timer 125 | console.timeEnd('Validation time'); -------------------------------------------------------------------------------- /tests/manual/test-fix-errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test script to test the PineScript error fixer 3 | */ 4 | 5 | // Import the fixer and validator 6 | import { fixPineScriptErrors } from '../../src/fixers/errorFixer.js'; 7 | import { validatePineScript } from '../../src/validators/syntaxValidator.js'; 8 | 9 | // Script with common issues 10 | const scriptWithIssues = `//@version=5 11 | indicator("Example With Issues") 12 | 13 | //Missing semicolons 14 | var1 = 10 15 | var2 = 20 16 | result = var1 + var2 17 | 18 | //Unbalanced brackets 19 | if (close > open { 20 | var3 = high 21 | } 22 | 23 | //Unclosed string 24 | message = "This string is not closed 25 | 26 | //Using := without var declaration 27 | counter = 0 28 | counter := counter + 1 29 | 30 | //Missing closing parenthesis in function call 31 | plot(close, "Price", color=color.blue`; 32 | 33 | // Try to fix the script 34 | console.log('Original script:'); 35 | console.log('================'); 36 | console.log(scriptWithIssues); 37 | console.log('================\n'); 38 | 39 | // Validate the original script 40 | console.log('Validating original script...'); 41 | const originalValidation = validatePineScript(scriptWithIssues); 42 | console.log('Validation result:', JSON.stringify(originalValidation, null, 2)); 43 | 44 | // Fix errors 45 | console.log('\nFixing errors...'); 46 | const fixedResult = fixPineScriptErrors(scriptWithIssues); 47 | console.log('Fix result:', JSON.stringify(fixedResult, null, 2)); 48 | 49 | // Validate the fixed script 50 | console.log('\nValidating fixed script...'); 51 | const fixedValidation = validatePineScript(fixedResult.script); 52 | console.log('Validation result:', JSON.stringify(fixedValidation, null, 2)); 53 | 54 | // Show the improvement 55 | console.log('\nImprovement: ' + 56 | (fixedValidation.errors.length < originalValidation.errors.length ? 57 | 'Fixed ' + (originalValidation.errors.length - fixedValidation.errors.length) + ' errors' : 58 | 'No improvement')); 59 | 60 | console.log('\nFixed script:'); 61 | console.log('============'); 62 | console.log(fixedResult.script); 63 | console.log('============'); -------------------------------------------------------------------------------- /tests/manual/test-single-error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test script to fix a single error - unclosed string 3 | */ 4 | 5 | // Import the fixer and validator 6 | import { fixPineScriptErrors } from '../../src/fixers/errorFixer.js'; 7 | import { validatePineScript } from '../../src/validators/syntaxValidator.js'; 8 | 9 | // Script with unclosed string 10 | const scriptWithIssue = `//@version=5 11 | indicator("Example With Unclosed String") 12 | 13 | message = "This string is not closed 14 | 15 | plot(close, "Price", color=color.blue)`; 16 | 17 | // Try to fix the script 18 | console.log('Original script:'); 19 | console.log('================'); 20 | console.log(scriptWithIssue); 21 | console.log('================\n'); 22 | 23 | // Validate the original script 24 | console.log('Validating original script...'); 25 | const originalValidation = validatePineScript(scriptWithIssue); 26 | console.log('Validation result:', JSON.stringify(originalValidation, null, 2)); 27 | 28 | // Fix errors 29 | console.log('\nFixing errors...'); 30 | const fixedResult = fixPineScriptErrors(scriptWithIssue); 31 | console.log('Fix result:', JSON.stringify(fixedResult, null, 2)); 32 | 33 | // Validate the fixed script 34 | console.log('\nValidating fixed script...'); 35 | const fixedValidation = validatePineScript(fixedResult.script); 36 | console.log('Validation result:', JSON.stringify(fixedValidation, null, 2)); 37 | 38 | // Show the improvement 39 | console.log('\nImprovement: ' + 40 | (fixedValidation.errors.length < originalValidation.errors.length ? 41 | 'Fixed ' + (originalValidation.errors.length - fixedValidation.errors.length) + ' errors' : 42 | 'No improvement')); 43 | 44 | console.log('\nFixed script:'); 45 | console.log('============'); 46 | console.log(fixedResult.script); 47 | console.log('============'); -------------------------------------------------------------------------------- /tests/manual/test-strategy-update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test script to validate our updated PineScript strategy with volume filter 3 | */ 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | 9 | // Get the current directory 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | // Path to our modified strategy files 14 | const originalStrategyPath = path.join(__dirname, '..', '..', 'examples', 'Modified_GoldScalping_Strategy.txt'); 15 | const updatedStrategyPath = path.join(__dirname, '..', '..', 'examples', 'Modified_GoldScalping_Strategy_With_Volume.txt'); 16 | 17 | // Function to validate the updated strategy 18 | async function validateUpdatedStrategy() { 19 | console.log('Validating updated Gold Scalping strategy with volume filter...'); 20 | 21 | try { 22 | // Read the updated strategy file 23 | const strategyScript = fs.readFileSync(updatedStrategyPath, 'utf8'); 24 | console.log(`Updated strategy loaded (${strategyScript.length} chars)`); 25 | 26 | // Prepare the request to validate 27 | const req = { 28 | jsonrpc: "2.0", 29 | id: 1, 30 | method: "validate_pinescript", 31 | params: { 32 | script: strategyScript, 33 | version: "v6" 34 | } 35 | }; 36 | 37 | // Send the request to stdin 38 | process.stdout.write(JSON.stringify(req) + '\n'); 39 | 40 | console.log('Updated strategy validation request sent. Waiting for response...'); 41 | } catch (error) { 42 | console.error('Error validating updated strategy:', error); 43 | } 44 | } 45 | 46 | // Function to compare original and updated strategies 47 | async function compareStrategies() { 48 | console.log('Comparing original and updated strategies...'); 49 | 50 | try { 51 | // Read both strategy files 52 | const originalScript = fs.readFileSync(originalStrategyPath, 'utf8'); 53 | const updatedScript = fs.readFileSync(updatedStrategyPath, 'utf8'); 54 | 55 | // Prepare the request to compare 56 | const req = { 57 | jsonrpc: "2.0", 58 | id: 2, 59 | method: "compare_pinescript_versions", 60 | params: { 61 | old_script: originalScript, 62 | new_script: updatedScript 63 | } 64 | }; 65 | 66 | // Send the request to stdin 67 | process.stdout.write(JSON.stringify(req) + '\n'); 68 | 69 | console.log('Strategy comparison request sent. Waiting for response...'); 70 | } catch (error) { 71 | console.error('Error comparing strategies:', error); 72 | } 73 | } 74 | 75 | // Function to format the updated strategy 76 | async function formatUpdatedStrategy() { 77 | console.log('Formatting updated Gold Scalping strategy...'); 78 | 79 | try { 80 | // Read the updated strategy file 81 | const strategyScript = fs.readFileSync(updatedStrategyPath, 'utf8'); 82 | 83 | // Prepare the request to format 84 | const req = { 85 | jsonrpc: "2.0", 86 | id: 3, 87 | method: "format_pinescript", 88 | params: { 89 | script: strategyScript, 90 | indent_size: 4, 91 | spaces_around_operators: true, 92 | max_line_length: 100 93 | } 94 | }; 95 | 96 | // Send the request to stdin 97 | process.stdout.write(JSON.stringify(req) + '\n'); 98 | 99 | console.log('Strategy formatting request sent. Waiting for response...'); 100 | } catch (error) { 101 | console.error('Error formatting updated strategy:', error); 102 | } 103 | } 104 | 105 | // Run the tests in a sequence with delays between them 106 | async function runTests() { 107 | // First validate the updated strategy 108 | await validateUpdatedStrategy(); 109 | 110 | // Wait 2 seconds before comparing strategies 111 | setTimeout(async () => { 112 | await compareStrategies(); 113 | 114 | // Wait 2 seconds before formatting 115 | setTimeout(async () => { 116 | await formatUpdatedStrategy(); 117 | }, 2000); 118 | }, 2000); 119 | } 120 | 121 | runTests(); -------------------------------------------------------------------------------- /tests/manual/test-strategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test script to validate our modified PineScript strategy 3 | */ 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | import { miniGoldStrategy, simplifiedGoldStrategy } from '../../src/templates/simplified-gold-strategy.js'; 9 | 10 | // Get the current directory 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | // Path to our modified strategy file 15 | const strategyFilePath = path.join(__dirname, '..', '..', 'examples', 'Modified_GoldScalping_Strategy.txt'); 16 | 17 | // First test the connection with a minimal script 18 | async function testConnection() { 19 | console.log('Testing MCP server connection with minimal script...'); 20 | 21 | try { 22 | // Create a minimal test script 23 | const testScript = `//@version=5 24 | indicator("Simple Test", overlay=true) 25 | plot(close, "Price", color=color.blue)`; 26 | 27 | // Prepare the request to validate 28 | const req = { 29 | jsonrpc: "2.0", 30 | id: 1, 31 | method: "test_connection", 32 | params: { 33 | version: "5" 34 | } 35 | }; 36 | 37 | // Send the request to stdin 38 | process.stdout.write(JSON.stringify(req) + '\n'); 39 | 40 | console.log('Test connection request sent. Waiting for response...'); 41 | 42 | // Note: In a real scenario, we would need to handle the response 43 | // but for this test we'll just let the MCP server output to console 44 | } catch (error) { 45 | console.error('Error testing connection:', error); 46 | } 47 | } 48 | 49 | // Function to validate mini strategy 50 | async function validateMiniStrategy() { 51 | console.log('Validating mini Gold strategy...'); 52 | 53 | try { 54 | // Prepare the request to validate 55 | const req = { 56 | jsonrpc: "2.0", 57 | id: 2, 58 | method: "validate_pinescript", 59 | params: { 60 | script: miniGoldStrategy, 61 | version: "v6" 62 | } 63 | }; 64 | 65 | // Send the request to stdin 66 | process.stdout.write(JSON.stringify(req) + '\n'); 67 | 68 | console.log('Mini strategy validation request sent. Waiting for response...'); 69 | } catch (error) { 70 | console.error('Error validating mini strategy:', error); 71 | } 72 | } 73 | 74 | // Function to validate simplified strategy 75 | async function validateSimplifiedStrategy() { 76 | console.log('Validating simplified Gold Scalping strategy...'); 77 | 78 | try { 79 | // Prepare the request to validate 80 | const req = { 81 | jsonrpc: "2.0", 82 | id: 3, 83 | method: "validate_pinescript", 84 | params: { 85 | script: simplifiedGoldStrategy, 86 | version: "v6" 87 | } 88 | }; 89 | 90 | // Send the request to stdin 91 | process.stdout.write(JSON.stringify(req) + '\n'); 92 | 93 | console.log('Simplified strategy validation request sent. Waiting for response...'); 94 | } catch (error) { 95 | console.error('Error validating simplified strategy:', error); 96 | } 97 | } 98 | 99 | // Function to validate our full strategy 100 | async function validateFullStrategy() { 101 | console.log('Validating full Gold Scalping strategy...'); 102 | 103 | try { 104 | // Read the strategy file 105 | const strategyScript = fs.readFileSync(strategyFilePath, 'utf8'); 106 | console.log(`Strategy loaded (${strategyScript.length} chars)`); 107 | 108 | // Prepare the request to validate 109 | const req = { 110 | jsonrpc: "2.0", 111 | id: 4, 112 | method: "validate_pinescript", 113 | params: { 114 | script: strategyScript, 115 | version: "v6" 116 | } 117 | }; 118 | 119 | // Send the request to stdin 120 | process.stdout.write(JSON.stringify(req) + '\n'); 121 | 122 | console.log('Full strategy validation request sent. Waiting for response...'); 123 | } catch (error) { 124 | console.error('Error validating full strategy:', error); 125 | } 126 | } 127 | 128 | // Run the tests in a sequence with delays between them 129 | async function runTests() { 130 | // First test connection with minimal script 131 | await testConnection(); 132 | 133 | // Wait 2 seconds before trying the mini strategy 134 | setTimeout(async () => { 135 | await validateMiniStrategy(); 136 | 137 | // Wait 2 seconds before trying the simplified strategy 138 | setTimeout(async () => { 139 | await validateSimplifiedStrategy(); 140 | 141 | // Wait 2 seconds before trying the full strategy 142 | setTimeout(async () => { 143 | await validateFullStrategy(); 144 | }, 2000); 145 | }, 2000); 146 | }, 2000); 147 | } 148 | 149 | runTests(); -------------------------------------------------------------------------------- /tests/manual/test-validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple test script to validate PineScript directly 3 | */ 4 | 5 | // Import the validator directly 6 | import { validatePineScript } from '../../src/validators/syntaxValidator.js'; 7 | import { getTestScript } from '../../src/templates/testScript.js'; 8 | 9 | // Get the minimal test script 10 | const testScript = getTestScript(); 11 | console.log('Testing with minimal script:'); 12 | console.log('============================'); 13 | console.log(testScript); 14 | console.log('============================'); 15 | 16 | // Validate it 17 | console.log('\nValidating PineScript...'); 18 | try { 19 | const result = validatePineScript(testScript); 20 | console.log('Validation result:', JSON.stringify(result, null, 2)); 21 | 22 | if (result.valid) { 23 | console.log('\nValidation successful - script is syntactically correct!'); 24 | } else { 25 | console.log('\nValidation failed with errors:', result.errors); 26 | if (result.warnings.length > 0) { 27 | console.log('Warnings:', result.warnings); 28 | } 29 | } 30 | } catch (error) { 31 | console.error('Validation error:', error); 32 | } -------------------------------------------------------------------------------- /tests/mcp/errorFixer.tool.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MCP Error Fixer Tool Tests 3 | * 4 | * These tests verify that the error fixer tool in the MCP server works correctly. 5 | */ 6 | 7 | import { describe, expect, it, jest } from '@jest/globals'; 8 | import { FixResult } from '../../src/fixers/errorFixer'; 9 | 10 | // Mock the fixPineScriptErrors function 11 | const mockedFixPineScriptErrors = jest.fn(); 12 | 13 | // Mock the module 14 | jest.mock('../../src/fixers/errorFixer', () => ({ 15 | fixPineScriptErrors: (...args: any[]) => mockedFixPineScriptErrors(...args) 16 | })); 17 | 18 | // Import the mocked module 19 | import { fixPineScriptErrors } from '../../src/fixers/errorFixer'; 20 | 21 | describe('MCP Error Fixer Tool', () => { 22 | beforeEach(() => { 23 | // Clear mocks before each test 24 | mockedFixPineScriptErrors.mockClear(); 25 | }); 26 | 27 | it('should fix syntax errors in PineScript code', () => { 28 | // Setup 29 | mockedFixPineScriptErrors.mockImplementation(() => ({ 30 | fixed: true, 31 | script: 'indicator("My Indicator", overlay=true)', 32 | changes: ['Added missing closing parenthesis on line 1'] 33 | })); 34 | 35 | // Execute 36 | const script = 'indicator("My Indicator", overlay=true'; 37 | const result = fixPineScriptErrors(script); 38 | 39 | // Assert 40 | expect(result.fixed).toBe(true); 41 | expect(result.changes.some(change => change.includes('missing closing parenthesis'))).toBe(true); 42 | }); 43 | 44 | it('should fix multiple errors in PineScript code', async () => { 45 | // Setup 46 | mockedFixPineScriptErrors.mockImplementation(() => ({ 47 | fixed: true, 48 | script: 'fixed script', 49 | changes: [ 50 | 'Added missing version annotation (@version=5)', 51 | 'Added missing closing parenthesis on line 2' 52 | ] 53 | })); 54 | 55 | // Execute 56 | const script = 'indicator("My Indicator", overlay=true\nvar x = 10'; 57 | const result = fixPineScriptErrors(script); 58 | 59 | // Assert 60 | expect(result.fixed).toBe(true); 61 | expect(result.changes.length).toBeGreaterThan(0); 62 | }); 63 | 64 | it('should return original script if no errors are detected', () => { 65 | // Setup 66 | mockedFixPineScriptErrors.mockImplementation(() => ({ 67 | fixed: false, 68 | script: 'original script', 69 | changes: [] 70 | })); 71 | 72 | // Execute 73 | const script = 'original script'; 74 | const result = fixPineScriptErrors(script); 75 | 76 | // Assert 77 | expect(result.fixed).toBe(false); 78 | expect(result.changes).toEqual([]); 79 | }); 80 | }); -------------------------------------------------------------------------------- /tests/mcp/formatter.tool.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for PineScript formatter MCP tool 3 | */ 4 | 5 | import { jest } from '@jest/globals'; 6 | import { formatPineScript } from '../../src/utils/formatter.js'; 7 | import { validatePineScript } from '../../src/validators/syntaxValidator.js'; 8 | 9 | // Mock the formatter and validator 10 | jest.mock('../../src/utils/formatter.js'); 11 | jest.mock('../../src/validators/syntaxValidator.js'); 12 | 13 | describe('Format PineScript Tool', () => { 14 | let formatTool: any; 15 | 16 | beforeEach(async () => { 17 | jest.clearAllMocks(); 18 | 19 | // Mock the formatter implementation 20 | (formatPineScript as jest.Mock).mockImplementation((script, options) => { 21 | return { 22 | formatted: script + '\n// Formatted', 23 | changes: ['Standardized indentation', 'Added spaces around operators'], 24 | warnings: options.maxLineLength === 60 ? ['Line 1 exceeds maximum line length'] : [] 25 | }; 26 | }); 27 | 28 | // Mock the validator implementation 29 | (validatePineScript as jest.Mock).mockImplementation((script) => { 30 | return { 31 | valid: true, 32 | warnings: [], 33 | errors: [] 34 | }; 35 | }); 36 | 37 | // Import modules that use the mocked functions 38 | const { default: registerMCPTools } = await import('../../src/index.js'); 39 | 40 | // Create a mock MCP instance 41 | const mcp = { 42 | addTool: jest.fn((config) => { 43 | if (config.name === 'format_pinescript') { 44 | formatTool = config; 45 | } 46 | }) 47 | }; 48 | 49 | // Register the tools 50 | await registerMCPTools(mcp as any); 51 | }); 52 | 53 | it('should register the format_pinescript tool', () => { 54 | expect(formatTool).toBeDefined(); 55 | expect(formatTool.name).toBe('format_pinescript'); 56 | expect(formatTool.description).toContain('Format PineScript'); 57 | }); 58 | 59 | it('should format script with default options', async () => { 60 | const script = `//@version=5 61 | indicator("My Indicator") 62 | a=1+2 63 | plot(a)`; 64 | 65 | const result = await formatTool.execute({ script }); 66 | const parsedResult = JSON.parse(result); 67 | 68 | expect(formatPineScript).toHaveBeenCalledWith(script, expect.any(Object)); 69 | expect(parsedResult.formatted).toContain('// Formatted'); 70 | expect(parsedResult.changes).toContain('Standardized indentation'); 71 | expect(parsedResult.valid).toBe(true); 72 | }); 73 | 74 | it('should pass custom formatting options', async () => { 75 | const script = `//@version=5 76 | indicator("My Indicator") 77 | a=1+2 78 | plot(a)`; 79 | 80 | const options = { 81 | script, 82 | indent_size: 2, 83 | use_spaces: false, 84 | spaces_around_operators: true, 85 | max_line_length: 60 86 | }; 87 | 88 | const result = await formatTool.execute(options); 89 | const parsedResult = JSON.parse(result); 90 | 91 | expect(formatPineScript).toHaveBeenCalledWith(script, expect.objectContaining({ 92 | indentSize: 2, 93 | useSpaces: false, 94 | maxLineLength: 60 95 | })); 96 | 97 | expect(parsedResult.warnings).toContain('Line 1 exceeds maximum line length'); 98 | }); 99 | 100 | it('should validate the formatted script', async () => { 101 | const script = `//@version=5 102 | indicator("My Indicator") 103 | a=1+2 104 | plot(a)`; 105 | 106 | // Mock validator to return warnings 107 | (validatePineScript as jest.Mock).mockReturnValueOnce({ 108 | valid: true, 109 | warnings: ['Consider using let instead of var for initialization'], 110 | errors: [] 111 | }); 112 | 113 | const result = await formatTool.execute({ script }); 114 | const parsedResult = JSON.parse(result); 115 | 116 | expect(validatePineScript).toHaveBeenCalled(); 117 | expect(parsedResult.validation_warnings).toHaveLength(1); 118 | expect(parsedResult.validation_warnings[0]).toContain('let instead of var'); 119 | }); 120 | 121 | it('should handle validation errors in formatted script', async () => { 122 | const script = `//@version=5 123 | indicator("My Indicator") 124 | a=1+2 125 | plot(a)`; 126 | 127 | // Mock validator to return errors 128 | (validatePineScript as jest.Mock).mockReturnValueOnce({ 129 | valid: false, 130 | warnings: [], 131 | errors: ['Unexpected syntax error'] 132 | }); 133 | 134 | const result = await formatTool.execute({ script }); 135 | const parsedResult = JSON.parse(result); 136 | 137 | expect(validatePineScript).toHaveBeenCalled(); 138 | expect(parsedResult.valid).toBe(false); 139 | expect(parsedResult.validation_errors).toHaveLength(1); 140 | expect(parsedResult.validation_errors[0]).toBe('Unexpected syntax error'); 141 | }); 142 | }); -------------------------------------------------------------------------------- /tests/mcp/server.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MCP Server Tests 3 | * 4 | * These tests verify that the MCP server is configured correctly. 5 | * Note: These tests don't actually start the server but verify its configuration. 6 | */ 7 | 8 | // In a real test environment, we'd mock fastmcp or create a test client 9 | // For now, we'll just test the basic setup 10 | describe('PineScript MCP Server Configuration', () => { 11 | it('should have correct name and version', async () => { 12 | // This is a placeholder test that will be expanded 13 | // when we have the ability to test the configuration 14 | expect(true).toBe(true); 15 | }); 16 | 17 | it('should register tools correctly', async () => { 18 | // This is a placeholder test that will be expanded 19 | // when we have the ability to test the tool registration 20 | const toolNames = ['validate_pinescript', 'fix_pinescript_errors', 'get_pinescript_template']; 21 | expect(toolNames.length).toBe(3); 22 | }); 23 | }); -------------------------------------------------------------------------------- /tests/mcp/versionTool.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, jest, test } from '@jest/globals'; 2 | import { FastMCP } from 'fastmcp'; 3 | import { VersionManager } from '../../src/utils/versionManager.js'; 4 | import { PineScriptVersion } from '../../src/utils/versionDetector.js'; 5 | 6 | // Mock the FastMCP class and VersionManager class 7 | jest.mock('fastmcp'); 8 | jest.mock('../../src/utils/versionManager.js'); 9 | 10 | // Define types for our mocks 11 | interface ToolConfig { 12 | name: string; 13 | description?: string; 14 | parameters?: any; 15 | execute: (args: any) => Promise; 16 | } 17 | 18 | describe('PineScript Version Management MCP Tools', () => { 19 | let mcp: jest.Mocked; 20 | let versionManager: jest.Mocked; 21 | let executeCallback: ((args: any) => Promise) | null = null; 22 | let mockTools: Record = {}; 23 | 24 | beforeEach(() => { 25 | // Reset mocks 26 | jest.clearAllMocks(); 27 | mockTools = {}; 28 | 29 | // Create mock for FastMCP with addTool method 30 | mcp = new FastMCP({ 31 | name: 'Test MCP', 32 | version: '1.0.0' 33 | }) as jest.Mocked; 34 | 35 | // Mock the addTool method 36 | mcp.addTool = jest.fn().mockImplementation((config: ToolConfig) => { 37 | mockTools[config.name] = config; 38 | if (config.name === 'convert_pinescript_version') { 39 | executeCallback = config.execute; 40 | } 41 | }); 42 | 43 | // Create mock for VersionManager 44 | versionManager = new VersionManager() as jest.Mocked; 45 | (versionManager.upgradeVersion as jest.Mock) = jest.fn().mockImplementation((script, targetVersion) => { 46 | return `// Converted to ${targetVersion}\n${script}`; 47 | }); 48 | 49 | (versionManager.saveVersion as jest.Mock) = jest.fn().mockReturnValue('test-id-123'); 50 | 51 | (versionManager.getVersionHistory as jest.Mock) = jest.fn().mockReturnValue([{ 52 | id: 'test-id-123', 53 | timestamp: '2024-03-21T12:00:00.000Z', 54 | content: '//@version=5\nindicator("Test")', 55 | version: PineScriptVersion.V5, 56 | valid: true, 57 | notes: 'Test version' 58 | }]); 59 | 60 | (versionManager.compareVersions as jest.Mock) = jest.fn().mockReturnValue([ 61 | '- oldLine', 62 | '+ newLine' 63 | ]); 64 | }); 65 | 66 | test('should register version tools with MCP server', async () => { 67 | // Import the main module (this will execute the main function that registers the tools) 68 | await import('../../src/index.js'); 69 | 70 | // Check that addTool was called the right number of times 71 | expect(mcp.addTool).toHaveBeenCalledTimes(7); 72 | 73 | // Check that tools were registered by looking at mock call arguments 74 | const toolNames = (mcp.addTool as jest.Mock).mock.calls.map(call => call[0].name); 75 | 76 | expect(toolNames).toContain('convert_pinescript_version'); 77 | expect(toolNames).toContain('save_pinescript_version'); 78 | expect(toolNames).toContain('get_pinescript_history'); 79 | expect(toolNames).toContain('compare_pinescript_versions'); 80 | }); 81 | 82 | test('convert_pinescript_version should upgrade script to target version', async () => { 83 | // Import the main module 84 | await import('../../src/index.js'); 85 | 86 | // Get the tool directly from our mock tools map 87 | const convertTool = mockTools['convert_pinescript_version']; 88 | expect(convertTool).toBeDefined(); 89 | 90 | // Execute the tool 91 | const params = { 92 | script: '//@version=5\nindicator("Test")', 93 | target_version: PineScriptVersion.V6 94 | }; 95 | 96 | const result = await convertTool.execute(params); 97 | const parsedResult = JSON.parse(result); 98 | 99 | // Check that the result contains the expected properties 100 | expect(parsedResult).toHaveProperty('convertedScript'); 101 | expect(parsedResult).toHaveProperty('valid'); 102 | expect(parsedResult).toHaveProperty('warnings'); 103 | 104 | // Check that upgradeVersion was called with correct params 105 | expect(versionManager.upgradeVersion).toHaveBeenCalledWith( 106 | params.script, 107 | params.target_version 108 | ); 109 | }); 110 | 111 | test('save_pinescript_version should save a script version', async () => { 112 | // Import the main module 113 | await import('../../src/index.js'); 114 | 115 | // Get the tool directly from our mock tools map 116 | const saveTool = mockTools['save_pinescript_version']; 117 | expect(saveTool).toBeDefined(); 118 | 119 | // Execute the tool 120 | const params = { 121 | script: '//@version=5\nindicator("Test")', 122 | notes: 'Test version' 123 | }; 124 | 125 | const result = await saveTool.execute(params); 126 | const parsedResult = JSON.parse(result); 127 | 128 | // Check that the result contains the expected properties 129 | expect(parsedResult).toHaveProperty('scriptId', 'test-id-123'); 130 | expect(parsedResult).toHaveProperty('versionCount', 1); 131 | expect(parsedResult).toHaveProperty('latestVersion'); 132 | expect(parsedResult.latestVersion).toHaveProperty('id', 'test-id-123'); 133 | expect(parsedResult.latestVersion).toHaveProperty('notes', 'Test version'); 134 | 135 | // Check that saveVersion was called with correct params 136 | expect(versionManager.saveVersion).toHaveBeenCalledWith( 137 | '//@version=5\nindicator("Test")', 138 | 'Test version' 139 | ); 140 | }); 141 | 142 | test('compare_pinescript_versions should compare two scripts', async () => { 143 | // Import the main module 144 | await import('../../src/index.js'); 145 | 146 | // Get the tool directly from our mock tools map 147 | const compareTool = mockTools['compare_pinescript_versions']; 148 | expect(compareTool).toBeDefined(); 149 | 150 | // Execute the tool 151 | const params = { 152 | old_script: '//@version=5\nindicator("Old")', 153 | new_script: '//@version=5\nindicator("New")' 154 | }; 155 | 156 | const result = await compareTool.execute(params); 157 | const parsedResult = JSON.parse(result); 158 | 159 | // Check that the result contains the expected properties 160 | expect(parsedResult).toHaveProperty('changeCount', 2); 161 | expect(parsedResult).toHaveProperty('changes'); 162 | expect(parsedResult.changes).toEqual([ 163 | '- oldLine', 164 | '+ newLine' 165 | ]); 166 | 167 | // Check that compareVersions was called with correct params 168 | expect(versionManager.compareVersions).toHaveBeenCalledWith( 169 | '//@version=5\nindicator("Old")', 170 | '//@version=5\nindicator("New")' 171 | ); 172 | }); 173 | }); -------------------------------------------------------------------------------- /tests/templates/templateManager.test.ts: -------------------------------------------------------------------------------- 1 | import { getTemplate, TemplateType } from '../../src/templates/templateManager.js'; 2 | 3 | describe('PineScript Template Manager', () => { 4 | it('should return a strategy template', () => { 5 | // Given 6 | const name = 'Test Strategy'; 7 | 8 | // When 9 | const template = getTemplate(TemplateType.STRATEGY, name); 10 | 11 | // Then 12 | expect(template).toContain(`strategy("${name}", overlay=true)`); 13 | // Just check for common elements that should be in all strategy templates 14 | expect(template).toContain('input('); 15 | expect(template).toContain('plot('); 16 | }); 17 | 18 | it('should return an indicator template', () => { 19 | // Given 20 | const name = 'Test Indicator'; 21 | 22 | // When 23 | const template = getTemplate(TemplateType.INDICATOR, name); 24 | 25 | // Then 26 | expect(template).toContain(`indicator("${name}"`); 27 | expect(template).toContain('length = input('); 28 | expect(template).toContain('plot('); 29 | }); 30 | 31 | it('should throw an error for unknown template type', () => { 32 | // Given 33 | const invalidType = 'invalid' as TemplateType; 34 | const name = 'Test'; 35 | 36 | // When & Then 37 | expect(() => { 38 | getTemplate(invalidType, name); 39 | }).toThrow('Unknown template type'); 40 | }); 41 | 42 | it('should return specific strategy templates by name', () => { 43 | // Test MA Cross template 44 | const maCrossTemplate = getTemplate(TemplateType.STRATEGY, 'ma cross'); 45 | expect(maCrossTemplate).toContain('Moving Average Crossover Strategy'); 46 | expect(maCrossTemplate).toContain('fastLength = input(9, "Fast MA Length")'); 47 | 48 | // Test RSI strategy template 49 | const rsiTemplate = getTemplate(TemplateType.STRATEGY, 'rsi'); 50 | expect(rsiTemplate).toContain('RSI Strategy'); 51 | expect(rsiTemplate).toContain('rsiLength = input(14, "RSI Length")'); 52 | }); 53 | 54 | it('should return specific indicator templates by name', () => { 55 | // Test Bollinger Bands template 56 | const bollingerTemplate = getTemplate(TemplateType.INDICATOR, 'bollinger bands'); 57 | expect(bollingerTemplate).toContain('Bollinger Bands'); 58 | expect(bollingerTemplate).toContain('mult = input.float(2.0, "Std Dev Multiplier"'); 59 | 60 | // Test MACD template 61 | const macdTemplate = getTemplate(TemplateType.INDICATOR, 'macd'); 62 | expect(macdTemplate).toContain('MACD - Moving Average Convergence/Divergence'); 63 | expect(macdTemplate).toContain('fastLength = input(12, "Fast Length")'); 64 | }); 65 | }); -------------------------------------------------------------------------------- /tests/utils/formatter.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for PineScript formatter 3 | */ 4 | 5 | import { expect, test, describe, it } from '@jest/globals'; 6 | import { formatPineScript, defaultFormatOptions, FormatOptions } from '../../src/utils/formatter'; 7 | 8 | describe('PineScript Formatter', () => { 9 | it('should add version comment if missing', () => { 10 | const script = `indicator("My Indicator", overlay=true) 11 | var counter = 0 12 | counter := counter + 1 13 | plot(counter)`; 14 | 15 | const result = formatPineScript(script); 16 | 17 | expect(result.formatted).toContain('//@version=5'); 18 | expect(result.changes).toContain('Added version comment'); 19 | }); 20 | 21 | it('should update existing version comment if needed', () => { 22 | const script = `//@version=4 23 | indicator("My Indicator", overlay=true) 24 | var counter = 0 25 | counter := counter + 1 26 | plot(counter)`; 27 | 28 | const result = formatPineScript(script); 29 | 30 | expect(result.formatted).toContain('//@version=5'); 31 | expect(result.changes).toContain('Updated version comment'); 32 | }); 33 | 34 | it('should format indentation correctly', () => { 35 | const script = `//@version=5 36 | indicator("Indentation Test") 37 | if (close > open) { 38 | plot(close) 39 | }`; 40 | 41 | const result = formatPineScript(script); 42 | 43 | // The if block should be indented properly 44 | expect(result.formatted).toContain('if (close > open) {'); 45 | expect(result.formatted).toContain(' plot(close)'); 46 | expect(result.changes).toContain('Standardized indentation'); 47 | }); 48 | 49 | it('should add spaces around operators', () => { 50 | const script = `//@version=5 51 | indicator("Operator Spacing") 52 | a=b+c*2 53 | d=e-f/3`; 54 | 55 | const result = formatPineScript(script); 56 | 57 | expect(result.formatted).toContain('a = b + c * 2'); 58 | expect(result.formatted).toContain('d = e - f / 3'); 59 | expect(result.changes).toContain('Standardized spacing around operators and commas'); 60 | }); 61 | 62 | it('should add spaces after commas', () => { 63 | const script = `//@version=5 64 | indicator("Comma Spacing") 65 | ma = ta.sma(close,14) 66 | plot(ma,color=color.red)`; 67 | 68 | const result = formatPineScript(script); 69 | 70 | expect(result.formatted).toContain('ma = ta.sma(close, 14)'); 71 | expect(result.formatted).toContain('plot(ma, color=color.red)'); 72 | expect(result.changes).toContain('Standardized spacing around operators and commas'); 73 | }); 74 | 75 | it('should normalize blank lines', () => { 76 | const script = `//@version=5 77 | indicator("Blank Lines") 78 | 79 | 80 | var a = 1 81 | 82 | 83 | 84 | var b = 2 85 | plot(a+b)`; 86 | 87 | const options: Partial = { 88 | keepBlankLines: false 89 | }; 90 | 91 | const result = formatPineScript(script, options); 92 | 93 | // There should be only one blank line between statements 94 | const lines = result.formatted.split('\n'); 95 | expect(lines.filter(line => line.trim() === '').length).toBeLessThan(5); 96 | expect(result.changes).toContain('Normalized blank lines'); 97 | }); 98 | 99 | it('should align comment blocks', () => { 100 | const script = `//@version=5 101 | indicator("Comment Alignment") 102 | // This is a comment block 103 | // With varying indentation 104 | // That should be aligned 105 | var a = 1 106 | plot(a)`; 107 | 108 | const result = formatPineScript(script); 109 | 110 | // All comments should have the same indentation 111 | const commentLines = result.formatted.split('\n').filter(line => line.trim().startsWith('//')); 112 | const indentations = commentLines.map(line => line.indexOf('//')); 113 | 114 | expect(new Set(indentations).size).toBe(1); 115 | expect(result.changes).toContain('Aligned comment blocks'); 116 | }); 117 | 118 | it('should warn about long lines', () => { 119 | const longLine = 'var reallyLongVariableName = "This is an extremely long string that will definitely exceed the maximum line length set in the formatter options"'; 120 | const script = `//@version=5 121 | indicator("Long Lines") 122 | ${longLine} 123 | plot(1)`; 124 | 125 | const options: Partial = { 126 | maxLineLength: 80 127 | }; 128 | 129 | const result = formatPineScript(script, options); 130 | 131 | expect(result.warnings.length).toBeGreaterThan(0); 132 | expect(result.warnings[0]).toContain('exceeds maximum line length'); 133 | }); 134 | 135 | it('should respect custom formatting options', () => { 136 | const script = `//@version=5 137 | indicator("Custom Options") 138 | if (condition) { 139 | code 140 | }`; 141 | 142 | const customOptions: Partial = { 143 | indentSize: 2, 144 | useSpaces: true, 145 | bracesOnNewLine: true 146 | }; 147 | 148 | const result = formatPineScript(script, customOptions); 149 | 150 | // Should use 2 spaces for indentation 151 | expect(result.formatted).toContain(' code'); 152 | }); 153 | 154 | it('should handle PineScript v6 syntax elements', () => { 155 | const script = ` 156 | indicator("V6 Elements") 157 | import ta 158 | method calculateRSI(source, length) { 159 | return ta.rsi(source, length) 160 | } 161 | let rsiValue = calculateRSI(close, 14) 162 | plot(rsiValue)`; 163 | 164 | const result = formatPineScript(script); 165 | 166 | expect(result.formatted).toContain('//@version=6'); 167 | expect(result.formatted).toContain('import ta'); 168 | expect(result.formatted).toContain('method calculateRSI(source, length) {'); 169 | expect(result.formatted).toContain(' return ta.rsi(source, length)'); 170 | expect(result.formatted).toContain('let rsiValue = calculateRSI(close, 14)'); 171 | }); 172 | 173 | it('should not modify code semantics', () => { 174 | const script = `//@version=5 175 | indicator("Preserve Semantics") 176 | a = 1+2 177 | b = "string,with,commas" 178 | c = [1,2,3,4] 179 | plot(a+b+c)`; 180 | 181 | const result = formatPineScript(script); 182 | 183 | // Should preserve string contents and important semantics 184 | expect(result.formatted).toContain('a = 1 + 2'); 185 | expect(result.formatted).toContain('b = "string,with,commas"'); 186 | expect(result.formatted).toContain('c = [1, 2, 3, 4]'); 187 | }); 188 | }); -------------------------------------------------------------------------------- /tests/utils/versionDetector.test.ts: -------------------------------------------------------------------------------- 1 | import { detectVersion, PineScriptVersion } from '../../src/utils/versionDetector.js'; 2 | 3 | describe('PineScript Version Detector', () => { 4 | it('should detect version 4', () => { 5 | // Given 6 | const script = `//@version=4 7 | study("My Script") 8 | `; 9 | 10 | // When 11 | const version = detectVersion(script); 12 | 13 | // Then 14 | expect(version).toBe(PineScriptVersion.V4); 15 | }); 16 | 17 | it('should detect version 5', () => { 18 | // Given 19 | const script = `//@version=5 20 | indicator("My Indicator") 21 | `; 22 | 23 | // When 24 | const version = detectVersion(script); 25 | 26 | // Then 27 | expect(version).toBe(PineScriptVersion.V5); 28 | }); 29 | 30 | it('should detect version 6', () => { 31 | // Given 32 | const script = `//@version=6 33 | indicator("My Indicator") 34 | `; 35 | 36 | // When 37 | const version = detectVersion(script); 38 | 39 | // Then 40 | expect(version).toBe(PineScriptVersion.V6); 41 | }); 42 | 43 | it('should default to version 5 when no version is specified', () => { 44 | // Given 45 | const script = `indicator("My Indicator")`; 46 | 47 | // When 48 | const version = detectVersion(script); 49 | 50 | // Then 51 | expect(version).toBe(PineScriptVersion.V5); 52 | }); 53 | }); -------------------------------------------------------------------------------- /tests/utils/versionManager.test.ts: -------------------------------------------------------------------------------- 1 | import { VersionManager, ScriptVersion } from '../../src/utils/versionManager.js'; 2 | import { PineScriptVersion } from '../../src/utils/versionDetector.js'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | 6 | // Create a temp directory for testing 7 | const TEST_HISTORY_DIR = '.test_pinescript_history'; 8 | 9 | describe('PineScript Version Manager', () => { 10 | let versionManager: VersionManager; 11 | 12 | beforeEach(() => { 13 | // Create a new version manager instance for each test 14 | versionManager = new VersionManager(TEST_HISTORY_DIR); 15 | }); 16 | 17 | afterEach(() => { 18 | // Clean up test directory after each test 19 | if (fs.existsSync(TEST_HISTORY_DIR)) { 20 | fs.rmSync(TEST_HISTORY_DIR, { recursive: true, force: true }); 21 | } 22 | }); 23 | 24 | it('should upgrade from v4 to v5', () => { 25 | // Given 26 | const v4Script = `//@version=4 27 | study("My Script") 28 | myRsi = rsi(close, 14) 29 | myEma = ema(close, 20) 30 | securityData = security(syminfo.tickerid, "D", close) 31 | plot(myRsi, "RSI") 32 | `; 33 | 34 | // When 35 | const result = versionManager.upgradeVersion(v4Script, PineScriptVersion.V5); 36 | 37 | // Then 38 | expect(result).toContain('//@version=5'); 39 | expect(result).toContain('indicator("My Script")'); 40 | expect(result).toContain('myRsi = ta.rsi(close, 14)'); 41 | expect(result).toContain('myEma = ta.ema(close, 20)'); 42 | expect(result).toContain('securityData = request.security(syminfo.tickerid, "D", close)'); 43 | }); 44 | 45 | it('should upgrade from v5 to v6', () => { 46 | // Given 47 | const v5Script = `//@version=5 48 | indicator("My Indicator") 49 | var counter = 0 50 | var price = close 51 | incrementer(x) => x + 1 52 | myRsi = ta.rsi(close, 14) 53 | plot(myRsi, "RSI") 54 | `; 55 | 56 | // When 57 | const result = versionManager.upgradeVersion(v5Script, PineScriptVersion.V6); 58 | 59 | // Then 60 | expect(result).toContain('//@version=6'); 61 | expect(result).toContain('import ta'); 62 | expect(result).toContain('let price = close'); 63 | expect(result).toContain('method incrementer'); 64 | }); 65 | 66 | it('should downgrade from v5 to v4', () => { 67 | // Given 68 | const v5Script = `//@version=5 69 | indicator("My Indicator") 70 | myRsi = ta.rsi(close, 14) 71 | myEma = ta.ema(close, 20) 72 | securityData = request.security(syminfo.tickerid, "D", close) 73 | plot(myRsi, "RSI") 74 | `; 75 | 76 | // When 77 | const result = versionManager.upgradeVersion(v5Script, PineScriptVersion.V4); 78 | 79 | // Then 80 | expect(result).toContain('//@version=4'); 81 | expect(result).toContain('study("My Indicator")'); 82 | expect(result).toContain('myRsi = rsi(close, 14)'); 83 | expect(result).toContain('myEma = ema(close, 20)'); 84 | expect(result).toContain('securityData = security(syminfo.tickerid, "D", close)'); 85 | }); 86 | 87 | it('should downgrade from v6 to v5', () => { 88 | // Given 89 | const v6Script = `//@version=6 90 | indicator("My Indicator") 91 | import ta 92 | let price = close 93 | var counter = 0 94 | method incrementer(x) { 95 | x + 1 96 | } 97 | myRsi = ta.rsi(close, 14) 98 | plot(series=myRsi, title="RSI", color=color.purple) 99 | `; 100 | 101 | // When 102 | const result = versionManager.upgradeVersion(v6Script, PineScriptVersion.V5); 103 | 104 | // Then 105 | expect(result).toContain('//@version=5'); 106 | expect(result).not.toContain('import ta'); 107 | expect(result).toContain('var price = close'); 108 | expect(result).toContain('incrementer(x) => {'); 109 | }); 110 | 111 | it('should save a version and generate an ID', () => { 112 | // Given 113 | const script = `//@version=5 114 | indicator("Test Indicator") 115 | myRsi = ta.rsi(close, 14) 116 | plot(myRsi, "RSI") 117 | `; 118 | 119 | // When 120 | const id = versionManager.saveVersion(script, 'Initial version'); 121 | 122 | // Then 123 | expect(id).toBeDefined(); 124 | expect(id.length).toBe(10); 125 | 126 | // Check that the file was saved 127 | const scriptDir = path.join(TEST_HISTORY_DIR, id); 128 | expect(fs.existsSync(scriptDir)).toBe(true); 129 | 130 | // Should have at least one .pine file and one .meta.json file 131 | const files = fs.readdirSync(scriptDir); 132 | expect(files.some(file => file.endsWith('.pine'))).toBe(true); 133 | expect(files.some(file => file.endsWith('.meta.json'))).toBe(true); 134 | }); 135 | 136 | it('should retrieve version history', () => { 137 | // Given 138 | const script1 = `//@version=5 139 | indicator("Version 1") 140 | myRsi = ta.rsi(close, 14) 141 | plot(myRsi, "RSI") 142 | `; 143 | 144 | const script2 = `//@version=5 145 | indicator("Version 2") 146 | myRsi = ta.rsi(close, 14) 147 | myEma = ta.ema(close, 20) 148 | plot(myRsi, "RSI") 149 | plot(myEma, "EMA") 150 | `; 151 | 152 | // When 153 | const id = versionManager.saveVersion(script1, 'Version 1'); 154 | versionManager.saveVersion(script2, 'Version 2'); 155 | 156 | // Then 157 | const history = versionManager.getVersionHistory(id); 158 | expect(history.length).toBe(1); 159 | expect(history[0].notes).toBe('Version 1'); 160 | expect(history[0].content).toBe(script1); 161 | }); 162 | 163 | it('should compare two script versions', () => { 164 | // Given 165 | const oldScript = `//@version=5 166 | indicator("Old Version") 167 | length = input(14, "Length") 168 | myRsi = ta.rsi(close, length) 169 | plot(myRsi, "RSI") 170 | `; 171 | 172 | const newScript = `//@version=5 173 | indicator("New Version") 174 | length = input(14, "Length") 175 | oversold = input(30, "Oversold") 176 | myRsi = ta.rsi(close, length) 177 | plot(myRsi, "RSI") 178 | hline(oversold, "Oversold") 179 | `; 180 | 181 | // When 182 | const changes = versionManager.compareVersions(oldScript, newScript); 183 | 184 | // Then 185 | expect(changes.length).toBeGreaterThan(0); 186 | 187 | // Check for the specific changes 188 | const addedLines = changes.filter(change => change.startsWith('+ ')); 189 | const removedLines = changes.filter(change => change.startsWith('- ')); 190 | 191 | expect(addedLines.some(line => line.includes('New Version'))).toBe(true); 192 | expect(addedLines.some(line => line.includes('oversold = input'))).toBe(true); 193 | expect(addedLines.some(line => line.includes('hline'))).toBe(true); 194 | expect(removedLines.some(line => line.includes('Old Version'))).toBe(true); 195 | }); 196 | }); -------------------------------------------------------------------------------- /tests/validators/syntaxValidator.test.ts: -------------------------------------------------------------------------------- 1 | import { validatePineScript, ValidationResult } from '../../src/validators/syntaxValidator.js'; 2 | 3 | describe('PineScript Syntax Validator', () => { 4 | it('should return a valid result for correct script', () => { 5 | // Given 6 | const script = `//@version=5 7 | indicator("Test Indicator", overlay=true) 8 | 9 | // Input parameters 10 | length = input(14, "RSI Length") 11 | 12 | // Calculate indicator 13 | rsiValue = ta.rsi(close, length) 14 | 15 | // Execute logic 16 | if (rsiValue > 70) 17 | label.new(bar_index, high, "Overbought") 18 | else if (rsiValue < 30) 19 | label.new(bar_index, low, "Oversold") 20 | 21 | // Plot indicator 22 | plot(rsiValue, "RSI", color.purple) 23 | hline(70, "Overbought", color.red) 24 | hline(30, "Oversold", color.green) 25 | `; 26 | 27 | // When 28 | const result: ValidationResult = validatePineScript(script); 29 | 30 | // Then 31 | expect(result.valid).toBe(true); 32 | expect(result.errors).toEqual([]); 33 | expect(result.warnings).toEqual([]); 34 | }); 35 | 36 | it('should detect empty script', () => { 37 | // Given 38 | const script = ''; 39 | 40 | // When 41 | const result: ValidationResult = validatePineScript(script); 42 | 43 | // Then 44 | expect(result.valid).toBe(false); 45 | expect(result.errors).toContain('Script cannot be empty'); 46 | }); 47 | 48 | it('should detect version mismatch', () => { 49 | // Given 50 | const script = '//@version=5\nindicator("Test")\n'; 51 | 52 | // When 53 | const result: ValidationResult = validatePineScript(script, 'v4'); 54 | 55 | // Then 56 | expect(result.warnings.length).toBeGreaterThan(0); 57 | expect(result.warnings[0]).toContain('Script uses version'); 58 | }); 59 | 60 | it('should detect unbalanced parentheses', () => { 61 | // Given 62 | const script = `//@version=5 63 | indicator("Test Indicator", overlay=true 64 | 65 | length = input(14, "RSI Length") 66 | `; 67 | 68 | // When 69 | const result: ValidationResult = validatePineScript(script); 70 | 71 | // Then 72 | expect(result.valid).toBe(false); 73 | expect(result.errors.some(error => error.includes('Unbalanced parentheses'))).toBe(true); 74 | }); 75 | 76 | it('should detect unclosed string literal', () => { 77 | // Given 78 | const script = `//@version=5 79 | indicator("Test Indicator, overlay=true) 80 | `; 81 | 82 | // When 83 | const result: ValidationResult = validatePineScript(script); 84 | 85 | // Then 86 | expect(result.valid).toBe(false); 87 | expect(result.errors.some(error => error.includes('Unclosed string literal'))).toBe(true); 88 | }); 89 | 90 | it('should detect deprecated study() function in v5', () => { 91 | // Given 92 | const script = `//@version=5 93 | study("Test Study", overlay=true) 94 | `; 95 | 96 | // When 97 | const result: ValidationResult = validatePineScript(script); 98 | 99 | // Then 100 | expect(result.warnings.some(warning => warning.includes("'study()' is deprecated"))).toBe(true); 101 | }); 102 | 103 | it('should detect incorrect variable export syntax', () => { 104 | // Given 105 | const script = `//@version=5 106 | indicator("Test Indicator") 107 | export var myVar = 10 108 | `; 109 | 110 | // When 111 | const result: ValidationResult = validatePineScript(script); 112 | 113 | // Then 114 | expect(result.valid).toBe(false); 115 | expect(result.errors.some(error => error.includes('Incorrect variable export syntax'))).toBe(true); 116 | }); 117 | 118 | it('should detect missing comma in function call', () => { 119 | // Given 120 | const script = `//@version=5 121 | indicator("Test Indicator") 122 | length = input(14 "RSI Length") 123 | `; 124 | 125 | // When 126 | const result: ValidationResult = validatePineScript(script); 127 | 128 | // Then 129 | expect(result.warnings.some(warning => warning.includes('Possible missing comma'))).toBe(true); 130 | }); 131 | }); -------------------------------------------------------------------------------- /tests/validators/v6Validator.test.ts: -------------------------------------------------------------------------------- 1 | import { validatePineScript, ValidationResult } from '../../src/validators/syntaxValidator.js'; 2 | 3 | describe('PineScript v6 Syntax Validator', () => { 4 | it('should detect v6 method syntax errors', () => { 5 | // Given 6 | const script = `//@version=6 7 | indicator("V6 Test") 8 | 9 | // Incorrect method syntax 10 | myMethod(x) => x * 2 11 | 12 | // Usage 13 | result = myMethod(5) 14 | `; 15 | 16 | // When 17 | const result: ValidationResult = validatePineScript(script, 'v6'); 18 | 19 | // Then 20 | expect(result.valid).toBe(false); 21 | expect(result.errors.some(error => error.includes("use 'method' keyword"))).toBe(true); 22 | }); 23 | 24 | it('should detect deprecated varip usage', () => { 25 | // Given 26 | const script = `//@version=6 27 | indicator("V6 Test") 28 | 29 | // Using deprecated varip 30 | varip counter = 0 31 | counter := counter + 1 32 | `; 33 | 34 | // When 35 | const result: ValidationResult = validatePineScript(script, 'v6'); 36 | 37 | // Then 38 | expect(result.valid).toBe(false); 39 | expect(result.errors.some(error => error.includes("'varip' keyword is deprecated"))).toBe(true); 40 | }); 41 | 42 | it('should warn about missing function return types', () => { 43 | // Given 44 | const script = `//@version=6 45 | indicator("V6 Test") 46 | 47 | // Missing return type 48 | fn calculateValue(price) 49 | value = price * 1.5 50 | value 51 | `; 52 | 53 | // When 54 | const result: ValidationResult = validatePineScript(script, 'v6'); 55 | 56 | // Then 57 | expect(result.warnings.some(warning => warning.includes("function declarations should include return type"))).toBe(true); 58 | }); 59 | 60 | it('should warn about var usage without mutable assignment', () => { 61 | // Given 62 | const script = `//@version=6 63 | indicator("V6 Test") 64 | 65 | // Should use let instead of var for immutable variable 66 | var price = close 67 | `; 68 | 69 | // When 70 | const result: ValidationResult = validatePineScript(script, 'v6'); 71 | 72 | // Then 73 | expect(result.warnings.some(warning => warning.includes("consider using 'let' for non-mutable variables"))).toBe(true); 74 | }); 75 | 76 | it('should warn about non-named parameters', () => { 77 | // Given 78 | const script = `//@version=6 79 | indicator("V6 Test") 80 | 81 | // Should use named parameters 82 | plot(close, "Price", color.blue) 83 | `; 84 | 85 | // When 86 | const result: ValidationResult = validatePineScript(script, 'v6'); 87 | 88 | // Then 89 | expect(result.warnings.some(warning => warning.includes("consider using named parameters"))).toBe(true); 90 | }); 91 | 92 | it('should warn about missing imports', () => { 93 | // Given 94 | const script = `//@version=6 95 | indicator("V6 Test") 96 | 97 | // Missing imports 98 | rsi = ta.rsi(close, 14) 99 | plot(rsi, "RSI") 100 | `; 101 | 102 | // When 103 | const result: ValidationResult = validatePineScript(script, 'v6'); 104 | 105 | // Then 106 | expect(result.warnings.some(warning => warning.includes("using imports for standard libraries"))).toBe(true); 107 | }); 108 | 109 | it('should validate correct v6 script', () => { 110 | // Given 111 | const script = `//@version=6 112 | indicator("V6 Test") 113 | 114 | import ta 115 | 116 | // Correct function with return type 117 | fn calculateRSI(price, length) -> float 118 | rsiValue = ta.rsi(price, length) 119 | rsiValue 120 | 121 | // Correct variable declarations 122 | let length = 14 123 | var mutableValue = 0 124 | mutableValue := mutableValue + 1 125 | 126 | // Method declaration 127 | method calculateAverage(length) { 128 | avg = ta.sma(close, length) 129 | avg 130 | } 131 | 132 | // Named parameters 133 | plot(series=calculateRSI(close, length), title="RSI", color=color.purple) 134 | `; 135 | 136 | // When 137 | const result: ValidationResult = validatePineScript(script, 'v6'); 138 | 139 | // Then 140 | expect(result.errors).toEqual([]); 141 | // There might still be some warnings, but there should be no errors 142 | expect(result.valid).toBe(true); 143 | }); 144 | }); --------------------------------------------------------------------------------