UNPKG

ta-pattern-lib

Version:

Technical Analysis and Backtesting Framework for Node.js

127 lines 5.82 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DSLParser = void 0; /** * DSLParser evaluates expressions like "ema(9, close(2)) > 100" * by recursively resolving each indicator or function call. */ class DSLParser { /** * Creates a new DSLParser instance with candle data sliced up to the current evaluation point. * This avoids any lookahead bias and ensures real-time safe evaluation. * * @param sliced_candles - Only the candles up to the current tick (e.g., candles.slice(0, index + 1)) * @param function_registry - Set of known function implementations (ema, close, etc.) */ constructor(sliced_candles, function_registry) { this.last_resolved_expression = null; // Store the last fully resolved expression this.candles = sliced_candles; // Store the candles for calculations this.function_registry = function_registry; // Store the function registry } /** * Evaluates a DSL expression like "ema(9, 2) > close(0)" * against the most recent candle in the slice. * * Example: * - ema(9, 2) → compute EMA(9) using candles[0..(len-1-2)] * - close(0) → get close price of latest candle * * @param expression - DSL rule to evaluate * @param context - External variables (e.g., entry_price, capital) * @returns boolean or number result */ evaluate(expression, context = {}) { const resolved = this.parse_and_evaluate_expression(expression, context); // Parse and resolve all function calls this.last_resolved_expression = resolved?.toString().replaceAll('\n', '').trim(); // Store the resolved expression try { // Evaluate using JS eval() after all functions are resolved to values return eval(resolved); } catch (err) { // Provide detailed error information if evaluation fails throw new Error(`DSL evaluation error: ${err instanceof Error ? err.message : String(err)}\nResolved: ${resolved}`); } } /** * Returns the last fully resolved expression after all function calls were evaluated. * Useful for debugging and understanding how expressions are processed. * * @returns The last resolved expression or null if no expression has been evaluated */ get_last_resolved_expression() { return this.last_resolved_expression; } /** * Parses and resolves all function calls within a DSL expression * into a final flattened string like "104.2 > 100", ready to be passed to eval(). * * Supports deeply nested calls like: * - ema(3, close(0)) * - avg(ema(5, 1), close(0), 10) */ parse_and_evaluate_expression(expr, context) { const fn_pattern = /\b(\w+)\(([^()]*)\)/; // Regex to match function calls like "func(args)" while (true) { const match = fn_pattern.exec(expr); // Find the next function call if (!match) break; // Exit loop if no more function calls found const [full_match, fn_name, raw_args] = match; // Extract function name and raw arguments // Split args and recursively evaluate each one to a number const args = this.split_arguments(raw_args).map((arg) => Number(this.evaluate(arg, context))); const fn_entry = this.function_registry[fn_name]; // Look up function in registry if (!fn_entry) throw new Error(`Unknown function: ${fn_name}`); // Error if function not found if (fn_entry.arity !== null && args.length !== fn_entry.arity) { // Validate argument count if function has fixed arity throw new Error(`Function "${fn_name}" expects ${fn_entry.arity} arguments, got ${args.length}`); } const result = fn_entry.fn(this.candles, context, ...args); // Execute the function with arguments // Replace the entire "func(...)" with its computed result expr = expr.replace(full_match, String(result)); } return expr; // Return the fully resolved expression } /** * Splits a function argument list string into its individual arguments, * handling nested function calls and parentheses properly. * * Example: * "ema(3, close(0)), 5, myfunc(1,2)" * → ["ema(3, close(0))", "5", "myfunc(1,2)"] */ split_arguments(arg_expr) { const args = []; // Array to store split arguments let current = ""; // Current argument being built let depth = 0; // Tracks nesting level of parentheses for (let i = 0; i < arg_expr.length; i++) { const char = arg_expr[i]; // Get current character if (char === "," && depth === 0) { // Found argument separator at top level args.push(current.trim()); // Add completed argument to array current = ""; // Reset current argument } else { if (char === "(") depth++; // Increase depth when opening parenthesis found if (char === ")") depth--; // Decrease depth when closing parenthesis found current += char; // Add character to current argument } } if (current.trim()) { // Add final argument if not empty args.push(current.trim()); } return args; // Return array of split arguments } } exports.DSLParser = DSLParser; /** * Sample Expressions * ema(2, 3) + ema(7, close(0)) > 100 * ema(2, 3) + ema(7, close(0)) > ema(9, close(0)) * rsi(14, 1) > 50 + current_price() * my_function(1, 2, 3) * avg(1, 2, 3) */ //# sourceMappingURL=feature_dsl_parser.js.map