diff --git a/.github/badges/api-math.svg b/.github/badges/api-math.svg index 0669fe2..1c11e5b 100644 --- a/.github/badges/api-math.svg +++ b/.github/badges/api-math.svg @@ -1,14 +1,14 @@ - - math: 26/28 (93%) + + math: 28/28 (100%) - + \ No newline at end of file diff --git a/.github/badges/coverage.svg b/.github/badges/coverage.svg index 0f93663..f56f019 100644 --- a/.github/badges/coverage.svg +++ b/.github/badges/coverage.svg @@ -1,20 +1,20 @@ - - test coverage: 84.9% - + + test coverage: 85% + - - + + - - + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c350b75..530855b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## [0.8.4] - 2026-01-24 - Math Namespace Enhancements & Critical Fixes + +### Added +- **Math Namespace**: Added `math.todegrees` and `math.toradians` functions. (contribution) + +### Fixed +- **Math Namespace**: Fixed `math.precision` implementation and `math.round` precision parameter handling. +- **Variable Scope Collision**: Fixed critical issue where local variables (`var`, `let`, `const`) in user-defined functions were sharing state across different function calls. Implemented dynamic scoping using unique call IDs to ensure each function instance maintains isolated state and history. +- **SMA NaN Handling**: Improved `ta.sma` to correctly propagate `NaN` values and handle `NaN` contamination in the rolling window by falling back to full recalculation when necessary. +- **Transpiler Optimization**: Major optimization of user-defined function transpilation. Introduced local context (`$$`) for scoping variables, reducing transpiled code complexity and improving readability by removing redundant `_callId` argument passing. +- **Array Access in Expressions**: Fixed a bug in the transpiler where array access inside expressions (e.g. ternary operators) could use incorrect static scope keys. + + + ## [0.8.3] - 2026-01-13 - Transpiler Critical Fixes ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..51a53e2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,347 @@ +# Contributing to PineTS + +First off, thank you for considering contributing to PineTS! 🎉 + +PineTS aims to bring Pine Script compatibility to JavaScript environments, and we welcome contributions that help us achieve this goal. Whether you're fixing bugs, adding missing functions, improving documentation, or suggesting features, your help is appreciated. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [How Can I Contribute?](#how-can-i-contribute) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Development Workflow](#development-workflow) +- [Project Architecture](#project-architecture) +- [Testing Requirements](#testing-requirements) +- [Code Style](#code-style) +- [Getting Help](#getting-help) + +--- + +## Code of Conduct + +This project follows a simple principle: **Be respectful and constructive**. We're all here to build something useful together. + +--- + +## How Can I Contribute? + +### 🐛 Reporting Bugs + +- **Check existing issues** first to avoid duplicates +- Use the issue template if available +- Include: + - Clear description of the bug + - Steps to reproduce + - Expected vs actual behavior + - PineTS version and environment (Node.js version, browser, etc.) + - Sample code that reproduces the issue + +### 💡 Suggesting Features + +- **Open a Discussion first** for significant features or architectural changes +- Explain the use case and why it would benefit the project +- Consider whether it aligns with PineTS's goal of Pine Script compatibility + +### 📝 Improving Documentation + +- Documentation improvements are always welcome! +- Fix typos, clarify confusing sections, or add examples +- Documentation PRs can be merged quickly + +--- + +## Pull Request Guidelines + +### 🎯 Keep PRs Small and Focused + +**This is critical for maintainability and review efficiency.** + +Large PRs (1000+ lines) are very difficult to review properly and can delay merging significantly. Instead: + +#### ✅ Good PR Examples: + +- **Bug Fix PR**: Fixes a specific bug without introducing new features + - Example: "Fix ta.sma NaN handling" + +- **Single Namespace Methods**: Add methods to one namespace at a time + - Example: "Add math.todegrees and math.toradians functions" + - Example: "Implement ta.supertrend indicator" + +- **Documentation Update**: Improve docs or add examples + - Example: "Add streaming examples to README" + +#### ❌ Avoid These: + +- **Mixed PRs**: Bug fixes + new features + refactoring in one PR +- **Multiple Namespaces**: Adding methods across `ta`, `math`, `strategy`, etc. simultaneously +- **Large Feature Dumps**: 10,000+ lines of code with missing tests + +### 📋 PR Checklist + +Before submitting, ensure: + +- [ ] **Tests are included** for all new functionality +- [ ] **All tests pass**: Run `npm test -- --run` +- [ ] **Code follows project style** (see [Code Style](#code-style)) +- [ ] **Documentation is updated** if you changed public APIs +- [ ] **Commit messages are clear** and describe what changed +- [ ] **PR description explains** what problem you're solving and how + +### 🔄 PR Process + +1. **Fork the repository** and create a feature branch + ```bash + git checkout -b fix/issue-description + # or + git checkout -b feature/new-functionality + ``` + +2. **Make your changes** following our guidelines + +3. **Test thoroughly** + ```bash + npm test -- --run + ``` + +4. **Commit with clear messages** + ```bash + git commit -m "Fix: math.round precision parameter handling" + ``` + +5. **Push and create PR** + ```bash + git push origin your-branch-name + ``` + +6. **Wait for review** - We'll review as soon as possible! + +### 🏗️ Architectural Changes + +**Please open a Discussion before working on:** + +- Changes to the transpiler +- New runtime features +- Modifications to the Series/Context classes +- Strategy backtesting implementation +- Drawing API implementation +- New data providers + +These require upfront discussion to ensure alignment with project direction and architecture. + +--- + +## Development Workflow + +### Initial Setup + +```bash +# Clone the repository +git clone https://github.com/QuantForgeOrg/PineTS.git +cd PineTS + +# Install dependencies +npm install + +# Run tests to verify setup +npm test -- --run +``` + +### Adding New Functions + +#### Example: Adding a `ta` namespace function + +1. **Create the implementation** + ```bash + # Create file: src/namespaces/ta/methods/yourfunction.ts + ``` + +2. **Follow the factory pattern** + ```typescript + export function yourfunction(context: any) { + return (source: any, period: any, _callId?: string) => { + const stateKey = _callId || `yourfunc_${period}`; + // Implementation with incremental calculation + // Return NaN during initialization + // Use context.precision() for output + }; + } + ``` + +3. **Add tests** + ```bash + # Create: tests/compatibility/namespace/ta/methods/indicators/yourfunction.test.ts + ``` + +4. **Regenerate barrel file** (only needed for some namespaces check package.json scripts) + ```bash + npm run generate:ta-index + ``` + +5. **Run tests** + ```bash + npm test -- yourfunction.test.ts --run + ``` + +#### For other namespaces: + +- **Math**: `npm run generate:math-index` +- **Array**: `npm run generate:array-index` +- **Input**: `npm run generate:input-index` +- **Request**: `npm run generate:request-index` + +### Running Tests + +```bash +# Run all tests +npm test -- --run + +# Run specific test file +npm test -- ta-sma.test.ts --run + +# Run with coverage +npm run test:coverage + +# Watch mode (during development) +npm test +``` + +**Important**: PineTS uses **Vitest**, not Jest. Don't use Jest-specific flags. + +### Building + +```bash +# Development build +npm run build:dev:all + +# Production build +npm run build:prod:all +``` +--- + +## Testing Requirements + +**All code changes MUST include tests.** No exceptions. + +### What to Test: + +1. **Basic Functionality**: Correct calculation with known inputs +2. **Edge Cases**: + - NaN inputs + - Single bar scenarios + - Empty/insufficient data +3. **Multiple Calls**: Same function with different parameters +4. **State Isolation**: Verify independent state with different `_callId`s +5. **Initialization Period**: Ensure NaN is returned when appropriate + +### Test Template: + +```typescript +import { describe, it, expect } from 'vitest'; +import { PineTS } from '../../../src/PineTS.class'; +import { Provider } from '@pinets/marketData/Provider.class'; + +describe('ta.yourfunction', () => { + it('should calculate correctly', async () => { + const pineTS = new PineTS( + Provider.Mock, + 'BTCUSDC', + '60', + null, + new Date('2024-01-01').getTime(), + new Date('2024-01-10').getTime() + ); + + const { plots } = await pineTS.run(($) => { + const { close } = $.data; + const { ta, plotchar } = $.pine; + + const result = ta.yourfunction(close, 14); + plotchar(result, 'result'); + }); + + expect(plots['result']).toBeDefined(); + expect(plots['result'].data.length).toBeGreaterThan(0); + + // Verify specific values + const lastValue = plots['result'].data[plots['result'].data.length - 1].value; + expect(lastValue).toBeCloseTo(expectedValue, 8); + }); + + it('should handle NaN inputs gracefully', async () => { + // Test with NaN inputs + }); + + it('should maintain independent state for multiple calls', async () => { + // Test state isolation + }); +}); +``` + +--- + +## Code Style + +### TypeScript + +- Use TypeScript for new code +- Properly type function signatures +- Document complex logic with comments + +### Naming Conventions + +- **TA/Math functions**: lowercase (e.g., `ema`, `sma`, `rsi`) +- **Classes**: PascalCase (e.g., `Series`, `Context`, `PineTS`) +- **Private methods**: prefix with `_` (e.g., `_initializeState`) +- **Constants**: UPPER_CASE (e.g., `MAX_ITERATIONS`) + +### Comments + +- Explain **WHY**, not **WHAT** +- Document non-obvious behavior +- Add warnings for critical sections +- Reference Pine Script documentation when relevant + +### Formatting + +We use Prettier for code formatting: + +```bash +# Format is automatic on save, or run: +npx prettier --write . +``` + +--- + +## Getting Help + +### Where to Ask: + +- **General Questions**: [GitHub Discussions](https://github.com/QuantForgeOrg/PineTS/discussions) +- **Bug Reports**: [GitHub Issues](https://github.com/QuantForgeOrg/PineTS/issues) +- **Feature Proposals**: [GitHub Discussions](https://github.com/QuantForgeOrg/PineTS/discussions) first +- **Private Contact**: [QuantForge Contact Form](https://quantforge.org/contact/) + +### Resources: + +- [Documentation](https://quantforgeorg.github.io/PineTS/) +- [Architecture Guide](./docs/architecture/index.md) +- [API Coverage](https://quantforgeorg.github.io/PineTS/api-coverage/) + +--- + +## License and CLA + +By contributing to PineTS, you agree to the terms of the [CLA](https://github.com/QuantForgeOrg/cla-signatures/blob/main/CLA.md). + +--- + +## Recognition + +Contributors are recognized in: +- GitHub Contributors page +- Release notes (for significant contributions) +- Our hearts ❤️ + +--- + +Thank you for contributing to PineTS! Together, we're building the future of open algorithmic trading. 🚀 diff --git a/docs/api-coverage/pinescript-v6/math.json b/docs/api-coverage/pinescript-v6/math.json index 5864bc4..6cbb9f4 100644 --- a/docs/api-coverage/pinescript-v6/math.json +++ b/docs/api-coverage/pinescript-v6/math.json @@ -36,7 +36,7 @@ }, "Utilities": { "math.random()": true, - "math.todegrees()": false, - "math.toradians()": false + "math.todegrees()": true, + "math.toradians()": true } } diff --git a/package.json b/package.json index d145150..cec3eb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pinets", - "version": "0.8.3", + "version": "0.8.4", "description": "Run Pine Script anywhere. PineTS is an open-source transpiler and runtime that brings Pine Script logic to Node.js and the browser with 1:1 syntax compatibility. Reliably write, port, and run indicators or strategies on your own infrastructure.", "keywords": [ "Pine Script", diff --git a/src/Context.class.ts b/src/Context.class.ts index 9103342..8002d4d 100644 --- a/src/Context.class.ts +++ b/src/Context.class.ts @@ -71,6 +71,7 @@ export class Context { public const: any = {}; public var: any = {}; public let: any = {}; + public lctx: Map = new Map(); public result: any = undefined; public plots: any = {}; @@ -416,6 +417,27 @@ export class Context { return this._callStack.length > 0 ? this._callStack[this._callStack.length - 1] : ''; } + /** + * Returns the local context object for the current call ID. + * Creates it if it doesn't exist. + */ + public peekCtx() { + const id = this.peekId(); + if (!id) return this; // Fallback to global context if not in a function call + + let ctx = this.lctx.get(id); + if (!ctx) { + ctx = { + id: id, + let: {}, + const: {}, + var: {}, + }; + this.lctx.set(id, ctx); + } + return ctx; + } + /** * Calls a function with a specific call ID context * @param fn - The function to call diff --git a/src/PineTS.class.ts b/src/PineTS.class.ts index e60ade8..367ec6c 100644 --- a/src/PineTS.class.ts +++ b/src/PineTS.class.ts @@ -535,15 +535,23 @@ export class PineTS { // Fix: Rollback context variables (let, var, const, params) const contextVarNames = ['const', 'var', 'let', 'params']; - for (let ctxVarName of contextVarNames) { - for (let key in context[ctxVarName]) { - const item = context[ctxVarName][key]; - if (item instanceof Series) { - item.data.pop(); - } else if (Array.isArray(item)) { - item.pop(); + const rollbackVariables = (container: any) => { + for (let ctxVarName of contextVarNames) { + if (!container[ctxVarName]) continue; + for (let key in container[ctxVarName]) { + const item = container[ctxVarName][key]; + if (item instanceof Series) { + item.data.pop(); + } else if (Array.isArray(item)) { + item.pop(); + } } } + }; + + rollbackVariables(context); + if (context.lctx) { + context.lctx.forEach((lctx: any) => rollbackVariables(lctx)); } } @@ -645,19 +653,27 @@ export class PineTS { } //shift context - for (let ctxVarName of contextVarNames) { - for (let key in context[ctxVarName]) { - const item = context[ctxVarName][key]; - - if (item instanceof Series) { - const val = item.get(0); - item.data.push(val); - } else if (Array.isArray(item)) { - // Legacy array support during transition - const val = item[item.length - 1]; - item.push(val); + const shiftVariables = (container: any) => { + for (let ctxVarName of contextVarNames) { + if (!container[ctxVarName]) continue; + for (let key in container[ctxVarName]) { + const item = container[ctxVarName][key]; + + if (item instanceof Series) { + const val = item.get(0); + item.data.push(val); + } else if (Array.isArray(item)) { + // Legacy array support during transition + const val = item[item.length - 1]; + item.push(val); + } } } + }; + + shiftVariables(context); + if (context.lctx) { + context.lctx.forEach((lctx: any) => shiftVariables(lctx)); } } } diff --git a/src/namespaces/math/math.index.ts b/src/namespaces/math/math.index.ts index 79e1374..9df8a76 100644 --- a/src/namespaces/math/math.index.ts +++ b/src/namespaces/math/math.index.ts @@ -30,6 +30,8 @@ import { sin } from './methods/sin'; import { sqrt } from './methods/sqrt'; import { sum } from './methods/sum'; import { tan } from './methods/tan'; +import { todegrees } from './methods/todegrees'; +import { toradians } from './methods/toradians'; import { __eq } from './methods/__eq'; const methods = { @@ -61,6 +63,8 @@ const methods = { sqrt, sum, tan, + todegrees, + toradians, __eq }; @@ -94,6 +98,8 @@ export class PineMath { sqrt: ReturnType; sum: ReturnType; tan: ReturnType; + todegrees: ReturnType; + toradians: ReturnType; __eq: ReturnType; constructor(private context: any) { diff --git a/src/namespaces/math/methods/round.ts b/src/namespaces/math/methods/round.ts index afcef6b..f2040c3 100644 --- a/src/namespaces/math/methods/round.ts +++ b/src/namespaces/math/methods/round.ts @@ -3,8 +3,23 @@ import { Series } from '../../../Series'; export function round(context: any) { - return (source: any) => { - return Math.round(Series.from(source).get(0)); + return (source: any, precision?: any) => { + const value = Series.from(source).get(0); + + if (precision === undefined || precision === null) { + // No precision specified - round to nearest integer + return Math.round(value); + } + + const precisionValue = Series.from(precision).get(0); + + if (precisionValue === 0) { + return Math.round(value); + } + + // Round to specified decimal places + const multiplier = Math.pow(10, precisionValue); + return Math.round(value * multiplier) / multiplier; }; } diff --git a/src/namespaces/math/methods/todegrees.ts b/src/namespaces/math/methods/todegrees.ts new file mode 100644 index 0000000..7d27e53 --- /dev/null +++ b/src/namespaces/math/methods/todegrees.ts @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +import { Series } from '../../../Series'; + +/** + * Converts an angle in radians to degrees. + * @param radians - The angle in radians to convert + * @returns The angle converted to degrees + * + * @example + * math.todegrees(math.pi()) // Returns 180 + * math.todegrees(math.pi() / 2) // Returns 90 + */ +export function todegrees(context: any) { + return (radians: any) => { + const value = Series.from(radians).get(0); + if (value === null || value === undefined || Number.isNaN(value)) { + return NaN; + } + return value * (180 / Math.PI); + }; +} diff --git a/src/namespaces/math/methods/toradians.ts b/src/namespaces/math/methods/toradians.ts new file mode 100644 index 0000000..56ca795 --- /dev/null +++ b/src/namespaces/math/methods/toradians.ts @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +import { Series } from '../../../Series'; + +/** + * Converts an angle in degrees to radians. + * @param degrees - The angle in degrees to convert + * @returns The angle converted to radians + * + * @example + * math.toradians(180) // Returns math.pi() + * math.toradians(90) // Returns math.pi() / 2 + */ +export function toradians(context: any) { + return (degrees: any) => { + const value = Series.from(degrees).get(0); + if (value === null || value === undefined || Number.isNaN(value)) { + return NaN; + } + return value * (Math.PI / 180); + }; +} diff --git a/src/namespaces/ta/methods/sma.ts b/src/namespaces/ta/methods/sma.ts index fef9182..841f42b 100644 --- a/src/namespaces/ta/methods/sma.ts +++ b/src/namespaces/ta/methods/sma.ts @@ -33,32 +33,101 @@ export function sma(context: any) { state.lastIdx = context.idx; } - const currentValue = Series.from(source).get(0) || 0; + const currentValue = Series.from(source).get(0); // Use committed state const window = [...state.prevWindow]; - let sum = state.prevSum; - + // Add current value to window window.unshift(currentValue); - sum += currentValue; - if (window.length < period) { - // Update tentative state - state.currentWindow = window; - state.currentSum = sum; - return NaN; + // Manage window size + if (window.length > period) { + window.pop(); } - if (window.length > period) { - // Remove oldest value from sum - const oldValue = window.pop(); - sum -= oldValue; + let sum; + + // Check for NaN contamination to decide on calculation strategy + // If any value in the window is NaN (or previous sum was NaN), we must verify the sum by full recalculation + // to correctly handle NaN propagation and recovery. + // We also check for undefined/null which treated as NaN for calculation purposes. + const isCurrentInvalid = currentValue === undefined || currentValue === null || Number.isNaN(currentValue); + const isPrevSumInvalid = Number.isNaN(state.prevSum); + + // We assume fast path is possible if everything looks valid. + // But if we are popping a value, we can't easily know if it was NaN without checking. + // Given SMA window sizes are usually manageable, O(N) sum when state is suspect is safer. + // Optimization: If prevSum is valid and current is valid, we still need to know if the popped value was valid. + // Since we've already popped it (or not), we can't easily check it unless we checked before pop. + + // To avoid complexity, we use the "NaN check" strategy: + // 1. If sum needs repair (NaN involved), calculate from scratch. + // 2. Otherwise try incremental. + + // But checking "popped value was NaN" implies we need access to it. + // Let's simplify: Always calculate sum O(N) if any NaN is present in window? + // Or just implement the O(N) loop which is robust. + + // Let's try the robust O(N) fallback only when necessary. + + let useFastPath = !isPrevSumInvalid && !isCurrentInvalid; + + // If fast path seems possible, we still need to be sure we didn't just pop a NaN (which would make result NaN -> Number, requiring recalc of prevSum didn't allow recovery) + // Actually, if prevSum was Number, then the window *should* have contained only Numbers. + // So popping a value should be popping a Number. + // So fast path IS safe if prevSum is valid and current is valid. + + if (useFastPath) { + // Reconstruct incremental step + // We need the value that was popped. + // Logic: sum = prevSum + current - popped + + // But we already modified 'window'. + // Let's use the state logic again carefully. + + // Re-derive from state vars for calculation + let tempSum = state.prevSum + currentValue; + + // If we shrank the window, we need to subtract the element that was in prevWindow but not in current window. + // That element is prevWindow[prevWindow.length - 1] IF prevWindow.length == period. + + if (state.prevWindow.length >= period) { + const popped = state.prevWindow[state.prevWindow.length - 1]; + // Double check popped validity just in case + if (popped === undefined || popped === null || Number.isNaN(popped)) { + useFastPath = false; + } else { + tempSum -= popped; + } + } + + if (useFastPath) { + sum = tempSum; + } + } + + if (!useFastPath) { + // Fallback to full recalculation + sum = 0; + let hasNaN = false; + for (const v of window) { + if (v === undefined || v === null || Number.isNaN(v)) { + hasNaN = true; + break; + } + sum += v; + } + if (hasNaN) sum = NaN; } // Update tentative state state.currentWindow = window; state.currentSum = sum; + + if (window.length < period) { + return NaN; + } const sma = sum / period; return context.precision(sma); diff --git a/src/transpiler/transformers/ExpressionTransformer.ts b/src/transpiler/transformers/ExpressionTransformer.ts index 909c649..0b7ceaa 100644 --- a/src/transpiler/transformers/ExpressionTransformer.ts +++ b/src/transpiler/transformers/ExpressionTransformer.ts @@ -11,6 +11,25 @@ const UNDEFINED_ARG = { name: 'undefined', }; +export function createScopedVariableReference(name: string, scopeManager: ScopeManager): any { + const [scopedName, kind] = scopeManager.getVariable(name); + + // Check if function scoped and not $$ itself + if (scopedName.match(/^fn\d+_/) && name !== '$$') { + const [localCtxName] = scopeManager.getVariable('$$'); + // Only if $$ is actually found (it should be in function scope) + if (localCtxName) { + return ASTFactory.createLocalContextVariableReference(kind, scopedName); + } + } + return ASTFactory.createContextVariableReference(kind, scopedName); +} + +export function createScopedVariableAccess(name: string, scopeManager: ScopeManager): any { + const varRef = createScopedVariableReference(name, scopeManager); + return ASTFactory.createGetCall(varRef, 0); +} + export function transformArrayIndex(node: any, scopeManager: ScopeManager): void { if (node.computed && node.property.type === 'Identifier') { // If index is a loop variable, we still need to transform the object to use $.get() @@ -18,9 +37,8 @@ export function transformArrayIndex(node: any, scopeManager: ScopeManager): void // Transform the object if it's a context-bound variable if (node.object.type === 'Identifier' && !scopeManager.isLoopVariable(node.object.name)) { if (!scopeManager.isContextBound(node.object.name)) { - const [scopedName, kind] = scopeManager.getVariable(node.object.name); // Transform to $.get($.kind.scopedName, loopVar) - const contextVarRef = ASTFactory.createContextVariableReference(kind, scopedName); + const contextVarRef = createScopedVariableReference(node.object.name, scopeManager); const getCall = ASTFactory.createGetCall(contextVarRef, node.property); Object.assign(node, getCall); node._indexTransformed = true; @@ -31,10 +49,8 @@ export function transformArrayIndex(node: any, scopeManager: ScopeManager): void // Only transform if it's not a context-bound variable if (!scopeManager.isContextBound(node.property.name)) { - const [scopedName, kind] = scopeManager.getVariable(node.property.name); - // Transform property to $.kind.scopedName - node.property = ASTFactory.createContextVariableReference(kind, scopedName); + node.property = createScopedVariableReference(node.property.name, scopeManager); // Add [0] to the index: $.get($.kind.scopedName, 0) node.property = ASTFactory.createGetCall(node.property, 0); @@ -47,10 +63,8 @@ export function transformArrayIndex(node: any, scopeManager: ScopeManager): void } if (!scopeManager.isContextBound(node.object.name)) { - const [scopedName, kind] = scopeManager.getVariable(node.object.name); - // Transform the object to scoped variable: $.kind.scopedName - node.object = ASTFactory.createContextVariableReference(kind, scopedName); + node.object = createScopedVariableReference(node.object.name, scopeManager); } if (node.property.type === 'MemberExpression') { @@ -184,6 +198,20 @@ export function transformIdentifier(node: any, scopeManager: ScopeManager): void return; } + // FIX: Don't transform function identifier if it's the first argument to $.call(fn, id, ...) + if ( + node.parent && + node.parent.type === 'CallExpression' && + node.parent.callee && + node.parent.callee.type === 'MemberExpression' && + node.parent.callee.object && + node.parent.callee.object.name === CONTEXT_NAME && + node.parent.callee.property.name === 'call' && + node.parent.arguments[0] === node + ) { + return; + } + // For local series variables (hoisted params), don't rename or wrap if they are args to a namespace function if (scopeManager.isLocalSeriesVar(node.name)) { return; @@ -196,8 +224,7 @@ export function transformIdentifier(node: any, scopeManager: ScopeManager): void } // Don't add [0] for namespace function arguments or array indices - const [scopedName, kind] = scopeManager.getVariable(node.name); - const memberExpr = ASTFactory.createContextVariableReference(kind, scopedName); + const memberExpr = createScopedVariableReference(node.name, scopeManager); Object.assign(node, memberExpr); return; } @@ -234,7 +261,7 @@ export function transformIdentifier(node: any, scopeManager: ScopeManager): void if (scopedName === node.name && !scopeManager.isContextBound(node.name)) { return; // Global/unknown var, return as is } - memberExpr = ASTFactory.createContextVariableReference(kind, scopedName); + memberExpr = createScopedVariableReference(node.name, scopeManager); } if (!hasArrayAccess) { @@ -328,7 +355,7 @@ export function transformMemberExpression(memberNode: any, originalParamName: st memberNode.object.object && memberNode.object.object.type === 'MemberExpression' && memberNode.object.object.object && - memberNode.object.object.object.name === CONTEXT_NAME; + (memberNode.object.object.object.name === CONTEXT_NAME || memberNode.object.object.object.name === '$$'); const isContextBoundIdentifier = memberNode.object && memberNode.object.type === 'Identifier' && scopeManager.isContextBound(memberNode.object.name); @@ -391,7 +418,7 @@ function transformIdentifierForParam(node: any, scopeManager: ScopeManager): any // If it's a user variable, transform it if (isUserVariable) { - return ASTFactory.createContextVariableReference(kind, scopedName); + return createScopedVariableReference(node.name, scopeManager); } // JavaScript global literals should never be transformed @@ -401,7 +428,7 @@ function transformIdentifierForParam(node: any, scopeManager: ScopeManager): any } // Otherwise transform with context variable reference (shouldn't reach here in normal cases) - return ASTFactory.createContextVariableReference(kind, scopedName); + return createScopedVariableReference(node.name, scopeManager); } return node; } @@ -681,8 +708,7 @@ export function transformFunctionArgument(arg: any, namespace: string, scopeMana return element; } // It's a user variable - transform to context reference - const [scopedName, kind] = scopeManager.getVariable(element.name); - return ASTFactory.createContextVariableAccess0(kind, scopedName); + return createScopedVariableAccess(element.name, scopeManager); } return element; }); @@ -787,8 +813,6 @@ export function transformFunctionArgument(arg: any, namespace: string, scopeMana }; } - const [scopedName, kind] = scopeManager.getVariable(prop.value.name); - // Convert shorthand to full property definition return { type: 'Property', @@ -796,7 +820,7 @@ export function transformFunctionArgument(arg: any, namespace: string, scopeMana type: 'Identifier', name: prop.key.name, }, - value: ASTFactory.createContextVariableReference(kind, scopedName), + value: createScopedVariableReference(prop.value.name, scopeManager), kind: 'init', method: false, shorthand: false, @@ -917,21 +941,22 @@ export function transformCallExpression(node: any, scopeManager: ScopeManager, n // Inject unique call ID for TA functions to enable proper state management if (namespace === 'ta') { if (scopeManager.getCurrentScopeType() === 'fn') { - // If inside a function, combine _callId param with the static ID + // If inside a function, combine $$.id with the static ID const staticId = scopeManager.getNextTACallId(); - // Manually resolve _callId from scope to ensure it uses the scoped variable name - const [scopedName, kind] = scopeManager.getVariable('_callId'); + // Manually resolve $$ from scope to ensure it uses the scoped variable name + const [localCtxName] = scopeManager.getVariable('$$'); let leftOperand; - if (scopedName !== '_callId') { - // It was found in scope (e.g. fn1__callId), create context variable reference - // _callId is a Series (from $.init/VariableDeclaration), so we must get current value - const contextVar = ASTFactory.createContextVariableReference(kind, scopedName); - leftOperand = ASTFactory.createGetCall(contextVar, 0); + if (localCtxName) { + // $$.id + leftOperand = ASTFactory.createMemberExpression( + ASTFactory.createLocalContextIdentifier(), + ASTFactory.createIdentifier('id') + ); } else { - // Fallback to identifier if not found (should not happen in valid PineTS) - leftOperand = ASTFactory.createIdentifier('_callId'); + // Fallback to empty string if not found (should not happen in valid PineTS) + leftOperand = ASTFactory.createLiteral(''); } const callIdArg = { diff --git a/src/transpiler/transformers/StatementTransformer.ts b/src/transpiler/transformers/StatementTransformer.ts index e1d122f..6d67782 100644 --- a/src/transpiler/transformers/StatementTransformer.ts +++ b/src/transpiler/transformers/StatementTransformer.ts @@ -10,14 +10,15 @@ import { transformMemberExpression, transformArrayIndex, addArrayAccess, + createScopedVariableReference, + createScopedVariableAccess, } from './ExpressionTransformer'; export function transformAssignmentExpression(node: any, scopeManager: ScopeManager): void { let targetVarRef = null; // Transform assignment expressions to use the context object if (node.left.type === 'Identifier') { - const [varName, kind] = scopeManager.getVariable(node.left.name); - targetVarRef = ASTFactory.createContextVariableReference(kind, varName); + targetVarRef = createScopedVariableReference(node.left.name, scopeManager); } else if (node.left.type === 'MemberExpression' && node.left.computed) { // Assignment to array element: series[0] = val if (node.left.object.type === 'Identifier') { @@ -29,7 +30,7 @@ export function transformAssignmentExpression(node: any, scopeManager: ScopeMana if ((isRenamed || isContextBound) && !scopeManager.isLoopVariable(name)) { // If index is 0 (literal), transform to $.set(target, value) if (node.left.property.type === 'Literal' && node.left.property.value === 0) { - targetVarRef = ASTFactory.createContextVariableReference(kind, varName); + targetVarRef = createScopedVariableReference(name, scopeManager); } } } @@ -46,7 +47,7 @@ export function transformAssignmentExpression(node: any, scopeManager: ScopeMana if (isRenamed && !scopeManager.isLoopVariable(name)) { // Transform object to scoped variable reference with [0] access // trade2.active = false -> $.get($.let.glb1_trade2, 0).active = false - const contextVarRef = ASTFactory.createContextVariableReference(kind, varName); + const contextVarRef = createScopedVariableReference(name, scopeManager); const getCall = ASTFactory.createGetCall(contextVarRef, 0); node.left.object = getCall; } @@ -150,6 +151,8 @@ export function transformAssignmentExpression(node: any, scopeManager: ScopeMana } export function transformVariableDeclaration(varNode: any, scopeManager: ScopeManager): void { + if (varNode._skipTransformation) return; + varNode.declarations.forEach((decl: any) => { //special case for na if (decl.init.name == 'na') { @@ -332,7 +335,7 @@ export function transformVariableDeclaration(varNode: any, scopeManager: ScopeMa } // Create the target variable reference using ASTFactory - const targetVarRef = ASTFactory.createContextVariableReference(kind, newName); + const targetVarRef = createScopedVariableReference(decl.id.name, scopeManager); // Check if initialization is from array access const isArrayInit = @@ -378,8 +381,7 @@ export function transformVariableDeclaration(varNode: any, scopeManager: ScopeMa // We skipped transformation for decl.init, so it's still a MemberExpression (temp[index]) const tempVarName = decl.init.object.name; - const [scopedTempName, tempKind] = scopeManager.getVariable(tempVarName); - const tempVarRef = ASTFactory.createContextVariableReference(tempKind, scopedTempName); + const tempVarRef = createScopedVariableReference(tempVarName, scopeManager); const arrayIndex = decl.init.property.value; // Create $.get(tempVar, 0)[index] @@ -598,8 +600,7 @@ export function transformReturnStatement(node: any, scopeManager: ScopeManager): } // Transform non-context-bound variables - const [scopedName, kind] = scopeManager.getVariable(element.name); - return ASTFactory.createContextVariableAccess0(kind, scopedName); + return createScopedVariableAccess(element.name, scopeManager); } else if (element.type === 'MemberExpression') { // Check if this is a context variable reference ($.const.xxx, $.let.xxx, etc.) const isContextVarRef = @@ -653,7 +654,7 @@ export function transformReturnStatement(node: any, scopeManager: ScopeManager): return { type: 'Property', key: ASTFactory.createIdentifier(prop.key.name), - value: ASTFactory.createContextVariableReference(kind, scopedName), + value: createScopedVariableReference(prop.value.name, scopeManager), kind: 'init', method: false, shorthand: false, @@ -670,8 +671,7 @@ export function transformReturnStatement(node: any, scopeManager: ScopeManager): // FIXED: Keep native data as Series (don't dereference to value) } else if (!scopeManager.isContextBound(prop.value.name)) { // It's a user variable - transform to context reference - const [scopedName, kind] = scopeManager.getVariable(prop.value.name); - prop.value = ASTFactory.createContextVariableReference(kind, scopedName); + prop.value = createScopedVariableReference(prop.value.name, scopeManager); } } @@ -758,12 +758,15 @@ export function transformFunctionDeclaration(node: any, scopeManager: ScopeManag // node.params.push(ASTFactory.createIdentifier('_callId')); const callIdDecl = ASTFactory.createVariableDeclaration( - '_callId', + '$$', ASTFactory.createCallExpression( - ASTFactory.createMemberExpression(ASTFactory.createContextIdentifier(), ASTFactory.createIdentifier('peekId')), + ASTFactory.createMemberExpression(ASTFactory.createContextIdentifier(), ASTFactory.createIdentifier('peekCtx')), [] ) ); + // Mark as special to skip transformation and be treated as raw variable + callIdDecl._skipTransformation = true; + scopeManager.addLoopVariable('$$', '$$'); // Transform the function body if (node.body && node.body.type === 'BlockStatement') { diff --git a/src/transpiler/utils/ASTFactory.ts b/src/transpiler/utils/ASTFactory.ts index 3842be1..13b7308 100644 --- a/src/transpiler/utils/ASTFactory.ts +++ b/src/transpiler/utils/ASTFactory.ts @@ -31,6 +31,10 @@ export const ASTFactory = { return this.createIdentifier(CONTEXT_NAME); }, + createLocalContextIdentifier(): any { + return this.createIdentifier('$$'); + }, + // Create $.kind.name createContextVariableReference(kind: string, name: string): any { const context = this.createContextIdentifier(); @@ -40,12 +44,35 @@ export const ASTFactory = { return this.createMemberExpression(this.createMemberExpression(context, kindId, false), nameId, false); }, + // Create $$.kind.name + createLocalContextVariableReference(kind: string, name: string): any { + const context = this.createLocalContextIdentifier(); + const kindId = this.createIdentifier(kind); + const nameId = this.createIdentifier(name); + + return this.createMemberExpression(this.createMemberExpression(context, kindId, false), nameId, false); + }, + + // Create $.kind[dynamicKey] + createDynamicContextVariableReference(kind: string, dynamicKey: any): any { + const context = this.createContextIdentifier(); + const kindId = this.createIdentifier(kind); + + return this.createMemberExpression(this.createMemberExpression(context, kindId, false), dynamicKey, true); + }, + // Create $.get($.kind.name, 0) createContextVariableAccess0(kind: string, name: string): any { const varRef = this.createContextVariableReference(kind, name); return this.createGetCall(varRef, 0); }, + // Create $.get($.kind[dynamicKey], 0) + createDynamicContextVariableAccess0(kind: string, dynamicKey: any): any { + const varRef = this.createDynamicContextVariableReference(kind, dynamicKey); + return this.createGetCall(varRef, 0); + }, + createArrayAccess(object: any, index: any): any { const indexNode = typeof index === 'number' ? this.createLiteral(index) : index; return this.createMemberExpression(object, indexNode, true); diff --git a/tests/indicators/ml.test.ts b/tests/indicators/ml.test.ts new file mode 100644 index 0000000..0040a9e --- /dev/null +++ b/tests/indicators/ml.test.ts @@ -0,0 +1,470 @@ +import { describe, expect, it } from 'vitest'; + +import { PineTS, Provider } from 'index'; + +describe('Indicators', () => { + it.skip('COMP_TEST', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2018-12-10').getTime(), new Date('2020-01-27').getTime()); + const pineTSSourceCode = (context) => { + const { close, high, low, volume } = context.data; + const { ta, plotchar, math } = context.pine; + function normalize(src, min, max) { + var historicMin = 10e10; + var historicMax = -10e10; + historicMin = math.min(nz(src, historicMin), historicMin); + historicMax = math.max(nz(src, historicMax), historicMax); + return min + (max - min) * (src - historicMin) / math.max(historicMax - historicMin, 10e-10); + } + + function n_wt(src, n1, n2) { + let ema1 = ta.ema(src, n1); + let ema2 = ta.ema(math.abs(src - ema1), n1); + let ci = (src - ema1) / (0.015 * ema2); + let wt1 = ta.ema(ci, n2); + let wt2 = ta.sma(wt1, 4); + return normalize(wt1 - wt2, 0, 1); + } + + function n_cci(src, n1, n2) { + let ema = ta.ema(ta.cci(src, n1), n2); + return normalize(ema, 0, 1); + } + + let src = close + let n1 = 3 + let n2 = 6 + let ema1 = ta.ema(src, n1) + plotchar(ema1, '_ema1'); + let ema2 = ta.ema(math.abs(src - ema1), n1) + plotchar(ema2, '_ema2'); + let ci = (src - ema1) / (0.015 * ema2) + plotchar(ci, '_ci'); + let wt1 = ta.ema(ci, n2) + plotchar(wt1, '_wt1'); + let wt2 = ta.sma(wt1, 4) + plotchar(wt2, '_wt2'); + //let res = normalize(wt1 - wt2, 0, 1); + + let _src = wt1 - wt2 + plotchar(_src, '_src'); + + let min = 0 + let max = 1 + var historicMin = 10e10 + var historicMax = -10e10 + historicMin = math.min(nz(_src, historicMin), historicMin) + plotchar(historicMin, '_historicMin'); + historicMax = math.max(nz(_src, historicMax), historicMax) + plotchar(historicMax, '_historicMax'); + //let wt = min + (max - min) * (_src - historicMin) / math.max(historicMax - historicMin, 10e-10) + const wt = n_wt(close, 3, 6) + plotchar(wt, '_wt'); + + + + const res = _src; + plotchar(res, '_res'); + } + + const { result, plots } = await pineTS.run(pineTSSourceCode); + + let _plotdata_res = plots['_res']?.data; + let _plotdata_src = plots['_src']?.data; + let _plotdata_ema1 = plots['_ema1']?.data; + let _plotdata_ema2 = plots['_ema2']?.data; + let _plotdata_ci = plots['_ci']?.data; + let _plotdata_wt1 = plots['_wt1']?.data; + let _plotdata_wt2 = plots['_wt2']?.data; + let _plotdata_historicMin = plots['_historicMin']?.data; + let _plotdata_historicMax = plots['_historicMax']?.data; + let _plotdata_wt = plots['_wt']?.data; + const startDate = new Date('2018-12-10').getTime(); + const endDate = new Date('2019-04-16').getTime(); + + let plotdata_str = ''; + for (let i = 0; i < _plotdata_res.length; i++) { + const time = _plotdata_res[i].time; + if (time < startDate || time > endDate) { + continue; + } + + const str_time = new Date(time).toISOString().slice(0, -1) + '-00:00'; + //const res_value = _plotdata_res[i].value.toFixed(3); + const src_value = _plotdata_src[i].value.toFixed(3); + const ema1_value = _plotdata_ema1[i].value.toFixed(3); + const ema2_value = _plotdata_ema2[i].value.toFixed(3); + const ci_value = _plotdata_ci[i].value.toFixed(3); + const wt1_value = _plotdata_wt1[i].value.toFixed(3); + const wt2_value = _plotdata_wt2[i].value.toFixed(3); + const historicMin_value = _plotdata_historicMin[i].value.toFixed(3); + const historicMax_value = _plotdata_historicMax[i].value.toFixed(3); + const wt_value = _plotdata_wt[i].value.toFixed(3); + plotdata_str += `[${str_time}]: ${wt1_value} ${wt2_value} ${src_value} ${historicMin_value} ${historicMax_value} ${wt_value}\n`; + } + + const expected_plot = `[2018-12-10T00:00:00.000-00:00]: NaN NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2018-12-17T00:00:00.000-00:00]: NaN NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2018-12-24T00:00:00.000-00:00]: NaN NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2018-12-31T00:00:00.000-00:00]: NaN NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2019-01-07T00:00:00.000-00:00]: NaN NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2019-01-14T00:00:00.000-00:00]: NaN NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2019-01-21T00:00:00.000-00:00]: NaN NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2019-01-28T00:00:00.000-00:00]: NaN NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2019-02-04T00:00:00.000-00:00]: NaN NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2019-02-11T00:00:00.000-00:00]: -16.406 NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2019-02-18T00:00:00.000-00:00]: 8.443 NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2019-02-25T00:00:00.000-00:00]: 25.788 NaN NaN 100,000,000,000 -100,000,000,000 NaN +[2019-03-04T00:00:00.000-00:00]: 41.059 14.721 26.338 26.338 26.338 0 +[2019-03-11T00:00:00.000-00:00]: 48.989 31.07 17.919 17.919 26.338 0 +[2019-03-18T00:00:00.000-00:00]: 48.603 41.11 7.493 7.493 26.338 0 +[2019-03-25T00:00:00.000-00:00]: 56.756 48.852 7.904 7.493 26.338 0.022 +[2019-04-01T00:00:00.000-00:00]: 74.589 57.234 17.355 7.493 26.338 0.523 +[2019-04-08T00:00:00.000-00:00]: 70.75 62.675 8.076 7.493 26.338 0.031 +[2019-04-15T00:00:00.000-00:00]: 65.965 67.015 -1.05 -1.05 26.338 0`; + + console.log('expected_plot', expected_plot); + console.log('plotdata_str', plotdata_str); + expect(plotdata_str.trim()).toEqual(expected_plot.trim()); + }); + + + + it('COMP_TEST VAR OK', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2018-12-10').getTime(), new Date('2020-01-27').getTime()); + const pineTSSourceCode = (context) => { + const { close, high, low, volume } = context.data; + const { ta, plotchar, math } = context.pine; + function normalize(src, min, max) { + var historicMin = 10e10; + var historicMax = -10e10; + historicMin = math.min(nz(src, historicMin), historicMin); + historicMax = math.max(nz(src, historicMax), historicMax); + return min + (max - min) * (src - historicMin) / math.max(historicMax - historicMin, 10e-10); + } + + function n_wt(src, n1, n2) { + let ema1 = ta.ema(src, n1); + let ema2 = ta.ema(math.abs(src - ema1), n1); + let ci = (src - ema1) / (0.015 * ema2); + let wt1 = ta.ema(ci, n2); + let wt2 = ta.sma(wt1, 4); + return normalize(wt1 - wt2, 0, 1); + } + + function n_cci(src, n1, n2) { + let ema = ta.ema(ta.cci(src, n1), n2); + return normalize(ema, 0, 1); + } + + + + const wt = n_wt(close, 3, 6) + plotchar(wt, '_wt'); + + const cci = 0;//n_cci(close, 3, 6) + plotchar(cci, '_cci'); + + + + } + + const { result, plots } = await pineTS.run(pineTSSourceCode); + + + + let _plotdata_wt = plots['_wt']?.data; + let _plotdata_cci = plots['_cci']?.data; + const startDate = new Date('2018-12-10').getTime(); + const endDate = new Date('2019-04-16').getTime(); + + let plotdata_str = ''; + for (let i = 0; i < _plotdata_wt.length; i++) { + const time = _plotdata_wt[i].time; + if (time < startDate || time > endDate) { + continue; + } + + const str_time = new Date(time).toISOString().slice(0, -1) + '-00:00'; + + const wt_value = _plotdata_wt[i].value.toFixed(3); + const cci_value = _plotdata_cci[i].value.toFixed(3); + plotdata_str += `[${str_time}]: ${wt_value}\n`; + } + + const expected_plot = `[2018-12-10T00:00:00.000-00:00]: NaN +[2018-12-17T00:00:00.000-00:00]: NaN +[2018-12-24T00:00:00.000-00:00]: NaN +[2018-12-31T00:00:00.000-00:00]: NaN +[2019-01-07T00:00:00.000-00:00]: NaN +[2019-01-14T00:00:00.000-00:00]: NaN +[2019-01-21T00:00:00.000-00:00]: NaN +[2019-01-28T00:00:00.000-00:00]: NaN +[2019-02-04T00:00:00.000-00:00]: NaN +[2019-02-11T00:00:00.000-00:00]: NaN +[2019-02-18T00:00:00.000-00:00]: NaN +[2019-02-25T00:00:00.000-00:00]: NaN +[2019-03-04T00:00:00.000-00:00]: 0.000 +[2019-03-11T00:00:00.000-00:00]: 0.000 +[2019-03-18T00:00:00.000-00:00]: 0.000 +[2019-03-25T00:00:00.000-00:00]: 0.022 +[2019-04-01T00:00:00.000-00:00]: 0.523 +[2019-04-08T00:00:00.000-00:00]: 0.031 +[2019-04-15T00:00:00.000-00:00]: 0.000`; + + console.log('expected_plot', expected_plot); + console.log('plotdata_str', plotdata_str); + expect(plotdata_str.trim()).toEqual(expected_plot.trim()); + }); + + it('COMP_TEST VAR Collide', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2018-12-10').getTime(), new Date('2020-01-27').getTime()); + const pineTSSourceCode = (context) => { + const { close, high, low, volume } = context.data; + const { ta, plotchar, math } = context.pine; + function normalize(src, min, max) { + var historicMin = 10e10; + var historicMax = -10e10; + historicMin = math.min(nz(src, historicMin), historicMin); + historicMax = math.max(nz(src, historicMax), historicMax); + return min + (max - min) * (src - historicMin) / math.max(historicMax - historicMin, 10e-10); + } + + function n_wt(src, n1, n2) { + let ema1 = ta.ema(src, n1); + let ema2 = ta.ema(math.abs(src - ema1), n1); + let ci = (src - ema1) / (0.015 * ema2); + let wt1 = ta.ema(ci, n2); + let wt2 = ta.sma(wt1, 4); + return normalize(wt1 - wt2, 0, 1); + } + + function n_cci(src, n1, n2) { + let ema = ta.ema(ta.cci(src, n1), n2); + return normalize(ema, 0, 1); + } + + + + const wt = n_wt(close, 3, 6) + plotchar(wt, '_wt'); + + const cci = n_cci(close, 3, 6) + plotchar(cci, '_cci'); + + + + } + + const { result, plots } = await pineTS.run(pineTSSourceCode); + + + + let _plotdata_wt = plots['_wt']?.data; + let _plotdata_cci = plots['_cci']?.data; + const startDate = new Date('2018-12-10').getTime(); + const endDate = new Date('2019-04-16').getTime(); + + let plotdata_str = ''; + for (let i = 0; i < _plotdata_wt.length; i++) { + const time = _plotdata_wt[i].time; + if (time < startDate || time > endDate) { + continue; + } + + const str_time = new Date(time).toISOString().slice(0, -1) + '-00:00'; + + const wt_value = _plotdata_wt[i].value.toFixed(3); + const cci_value = _plotdata_cci[i].value.toFixed(3); + plotdata_str += `[${str_time}]: ${wt_value}\n`; + } + + const expected_plot = `[2018-12-10T00:00:00.000-00:00]: NaN +[2018-12-17T00:00:00.000-00:00]: NaN +[2018-12-24T00:00:00.000-00:00]: NaN +[2018-12-31T00:00:00.000-00:00]: NaN +[2019-01-07T00:00:00.000-00:00]: NaN +[2019-01-14T00:00:00.000-00:00]: NaN +[2019-01-21T00:00:00.000-00:00]: NaN +[2019-01-28T00:00:00.000-00:00]: NaN +[2019-02-04T00:00:00.000-00:00]: NaN +[2019-02-11T00:00:00.000-00:00]: NaN +[2019-02-18T00:00:00.000-00:00]: NaN +[2019-02-25T00:00:00.000-00:00]: NaN +[2019-03-04T00:00:00.000-00:00]: 0.000 +[2019-03-11T00:00:00.000-00:00]: 0.000 +[2019-03-18T00:00:00.000-00:00]: 0.000 +[2019-03-25T00:00:00.000-00:00]: 0.022 +[2019-04-01T00:00:00.000-00:00]: 0.523 +[2019-04-08T00:00:00.000-00:00]: 0.031 +[2019-04-15T00:00:00.000-00:00]: 0.000`; + + console.log('expected_plot', expected_plot); + console.log('plotdata_str', plotdata_str); + expect(plotdata_str.trim()).toEqual(expected_plot.trim()); + }); + it('MLExtensions - n_wt n_cci', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2018-12-10').getTime(), new Date('2020-01-27').getTime()); + const pineTSSourceCode = (context) => { + const { close, high, low, volume } = context.data; + const { ta, plotchar, math } = context.pine; + + function normalize(src, min, max) { + var historicMin = 10e10; + var historicMax = -10e10; + historicMin = math.min(nz(src, historicMin), historicMin); + historicMax = math.max(nz(src, historicMax), historicMax); + return min + (max - min) * (src - historicMin) / math.max(historicMax - historicMin, 10e-10); + } + + function n_wt(src, n1, n2) { + let ema1 = ta.ema(src, n1); + let ema2 = ta.ema(math.abs(src - ema1), n1); + let ci = (src - ema1) / (0.015 * ema2); + let wt1 = ta.ema(ci, n2); + let wt2 = ta.sma(wt1, 4); + return normalize(wt1 - wt2, 0, 1); + } + + function n_cci(src, n1, n2) { + let ema = ta.ema(ta.cci(src, n1), n2); + return normalize(ema, 0, 1); + } + + const wt = n_wt(close, 3, 6); + const cci = n_cci(close, 3, 6); + plotchar(wt, '_wt'); + plotchar(cci, '_cci'); + } + + const { result, plots } = await pineTS.run(pineTSSourceCode); + + let _plotdata_wt = plots['_wt']?.data; + let _plotdata_cci = plots['_cci']?.data; + const startDate = new Date('2018-12-10').getTime(); + const endDate = new Date('2019-04-16').getTime(); + + let plotdata_str = ''; + for (let i = 0; i < _plotdata_wt.length; i++) { + const time = _plotdata_wt[i].time; + if (time < startDate || time > endDate) { + continue; + } + + const str_time = new Date(time).toISOString().slice(0, -1) + '-00:00'; + const wt_value = _plotdata_wt[i].value.toFixed(3); + const cci_value = _plotdata_cci[i].value.toFixed(3); + plotdata_str += `[${str_time}]: ${wt_value} ${cci_value}\n`; + } + + const expected_plot = `[2018-12-10T00:00:00.000-00:00]: NaN NaN +[2018-12-17T00:00:00.000-00:00]: NaN NaN +[2018-12-24T00:00:00.000-00:00]: NaN NaN +[2018-12-31T00:00:00.000-00:00]: NaN NaN +[2019-01-07T00:00:00.000-00:00]: NaN NaN +[2019-01-14T00:00:00.000-00:00]: NaN NaN +[2019-01-21T00:00:00.000-00:00]: NaN NaN +[2019-01-28T00:00:00.000-00:00]: NaN 0.000 +[2019-02-04T00:00:00.000-00:00]: NaN 1.000 +[2019-02-11T00:00:00.000-00:00]: NaN 1.000 +[2019-02-18T00:00:00.000-00:00]: NaN 1.000 +[2019-02-25T00:00:00.000-00:00]: NaN 1.000 +[2019-03-04T00:00:00.000-00:00]: 0.000 1.000 +[2019-03-11T00:00:00.000-00:00]: 0.000 1.000 +[2019-03-18T00:00:00.000-00:00]: 0.000 0.941 +[2019-03-25T00:00:00.000-00:00]: 0.022 1.000 +[2019-04-01T00:00:00.000-00:00]: 0.523 1.000 +[2019-04-08T00:00:00.000-00:00]: 0.031 0.895 +[2019-04-15T00:00:00.000-00:00]: 0.000 0.970`; + + console.log('expected_plot', expected_plot); + console.log('plotdata_str', plotdata_str); + expect(plotdata_str.trim()).toEqual(expected_plot.trim()); + }); + + + it('scope test', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2018-12-10').getTime(), new Date('2020-01-27').getTime()); + const pineTSSourceCode = (context) => { + const { close, high, low, volume } = context.data; + const { ta, plotchar, math } = context.pine; + + function foo(val) { + let x = val + let prev = x[1] + return [x, prev] + } + + let [c1_curr, c1_prev] = foo(100) + let [c2_curr, c2_prev] = foo(200) + plotchar(c1_curr, '_c1_curr') + plotchar(c1_prev, '_c1_prev') + plotchar(c2_curr, '_c2_curr') + plotchar(c2_prev, '_c2_prev') + } +// ` +// //@version=5 +// foo(val) => +// x = val +// prev = x[1] +// [x, prev] + +// [c1_curr, c1_prev] = foo(100) + +// [c2_curr, c2_prev] = foo(200) +// plotchar(c1_curr, '_c1_curr') +// plotchar(c1_prev, '_c1_prev') +// plotchar(c2_curr, '_c2_curr') +// plotchar(c2_prev, '_c2_prev') +// ` + + const { result, plots } = await pineTS.run(pineTSSourceCode); + + let _plotdata_c1_curr = plots['_c1_curr']?.data; + let _plotdata_c1_prev = plots['_c1_prev']?.data; + let _plotdata_c2_curr = plots['_c2_curr']?.data; + let _plotdata_c2_prev = plots['_c2_prev']?.data; + + + const startDate = new Date('2018-12-10').getTime(); + const endDate = new Date('2019-04-16').getTime(); + + let plotdata_str = ''; + for (let i = 0; i < _plotdata_c1_curr.length; i++) { + const time = _plotdata_c1_curr[i].time; + if (time < startDate || time > endDate) { + continue; + } + + const str_time = new Date(time).toISOString().slice(0, -1) + '-00:00'; + const c1_curr_value = _plotdata_c1_curr[i].value; + const c1_prev_value = _plotdata_c1_prev[i].value; + const c2_curr_value = _plotdata_c2_curr[i].value; + const c2_prev_value = _plotdata_c2_prev[i].value; + plotdata_str += `[${str_time}]: ${c1_prev_value} ${c2_prev_value}\n`; + } + + const expected_plot = `[2018-12-10T00:00:00.000-00:00]: NaN NaN +[2018-12-17T00:00:00.000-00:00]: 100 200 +[2018-12-24T00:00:00.000-00:00]: 100 200 +[2018-12-31T00:00:00.000-00:00]: 100 200 +[2019-01-07T00:00:00.000-00:00]: 100 200 +[2019-01-14T00:00:00.000-00:00]: 100 200 +[2019-01-21T00:00:00.000-00:00]: 100 200 +[2019-01-28T00:00:00.000-00:00]: 100 200 +[2019-02-04T00:00:00.000-00:00]: 100 200 +[2019-02-11T00:00:00.000-00:00]: 100 200 +[2019-02-18T00:00:00.000-00:00]: 100 200 +[2019-02-25T00:00:00.000-00:00]: 100 200 +[2019-03-04T00:00:00.000-00:00]: 100 200 +[2019-03-11T00:00:00.000-00:00]: 100 200 +[2019-03-18T00:00:00.000-00:00]: 100 200 +[2019-03-25T00:00:00.000-00:00]: 100 200 +[2019-04-01T00:00:00.000-00:00]: 100 200 +[2019-04-08T00:00:00.000-00:00]: 100 200 +[2019-04-15T00:00:00.000-00:00]: 100 200`; + + console.log('expected_plot', expected_plot); + console.log('plotdata_str', plotdata_str); + expect(plotdata_str.trim()).toEqual(expected_plot.trim()); + }); +}); \ No newline at end of file diff --git a/tests/namespaces/math-edge-cases.test.ts b/tests/namespaces/math-edge-cases.test.ts index e13796c..e8dff7a 100644 --- a/tests/namespaces/math-edge-cases.test.ts +++ b/tests/namespaces/math-edge-cases.test.ts @@ -248,11 +248,11 @@ describe('Math Edge Cases', () => { const code = ` const { math, plotchar } = context.pine; - + const round_up = math.round(3.6); const round_down = math.round(3.4); const round_neg = math.round(-3.6); - + plotchar(round_up, 'up'); plotchar(round_down, 'down'); plotchar(round_neg, 'neg'); @@ -263,6 +263,34 @@ describe('Math Edge Cases', () => { expect(plots['down'].data[0].value).toBe(3); expect(plots['neg'].data[0].value).toBe(-4); }); + + it('math.round with precision parameter should work correctly', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '1h', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` + const { math, plotchar } = context.pine; + + // Test various precision levels + const round_2dec = math.round(2.01234567890123456789, 2); + const round_5dec = math.round(2.01234567890123456789, 5); + const round_10dec = math.round(2.01234567890123456789, 10); + const round_0dec = math.round(2.56789, 0); + const round_neg_2dec = math.round(-3.14159, 2); + + plotchar(round_2dec, 'dec2'); + plotchar(round_5dec, 'dec5'); + plotchar(round_10dec, 'dec10'); + plotchar(round_0dec, 'dec0'); + plotchar(round_neg_2dec, 'neg_dec2'); + `; + + const { plots } = await pineTS.run(code); + expect(plots['dec2'].data[0].value).toBe(2.01); + expect(plots['dec5'].data[0].value).toBe(2.01235); + expect(plots['dec10'].data[0].value).toBe(2.0123456789); + expect(plots['dec0'].data[0].value).toBe(3); + expect(plots['neg_dec2'].data[0].value).toBe(-3.14); + }); }); describe('Trigonometric Functions with Edge Cases', () => { @@ -369,11 +397,11 @@ describe('Math Edge Cases', () => { const code = ` const { math, plotchar } = context.pine; - + const very_small = 1e-200; const underflow = very_small * 1e-200; const is_zero = underflow === 0; - + plotchar(is_zero ? 1 : 0, 'zero'); `; @@ -381,5 +409,137 @@ describe('Math Edge Cases', () => { expect(plots['zero'].data[0].value).toBe(1); }); }); + + describe('Degree/Radian Conversion Functions', () => { + it('math.todegrees should convert radians to degrees', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '1h', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` + const { math, plotchar } = context.pine; + + const deg_from_pi = math.todegrees(math.pi()); + const deg_from_half_pi = math.todegrees(math.pi() / 2); + const deg_from_quarter_pi = math.todegrees(math.pi() / 4); + const deg_from_zero = math.todegrees(0); + + plotchar(deg_from_pi, 'pi'); + plotchar(deg_from_half_pi, 'half_pi'); + plotchar(deg_from_quarter_pi, 'quarter_pi'); + plotchar(deg_from_zero, 'zero'); + `; + + const { plots } = await pineTS.run(code); + expect(plots['pi'].data[0].value).toBeCloseTo(180, 10); + expect(plots['half_pi'].data[0].value).toBeCloseTo(90, 10); + expect(plots['quarter_pi'].data[0].value).toBeCloseTo(45, 10); + expect(plots['zero'].data[0].value).toBe(0); + }); + + it('math.toradians should convert degrees to radians', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '1h', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` + const { math, plotchar } = context.pine; + + const rad_from_180 = math.toradians(180); + const rad_from_90 = math.toradians(90); + const rad_from_45 = math.toradians(45); + const rad_from_zero = math.toradians(0); + + plotchar(rad_from_180, 'rad_180'); + plotchar(rad_from_90, 'rad_90'); + plotchar(rad_from_45, 'rad_45'); + plotchar(rad_from_zero, 'rad_zero'); + `; + + const { plots } = await pineTS.run(code); + expect(plots['rad_180'].data[0].value).toBeCloseTo(Math.PI, 10); + expect(plots['rad_90'].data[0].value).toBeCloseTo(Math.PI / 2, 10); + expect(plots['rad_45'].data[0].value).toBeCloseTo(Math.PI / 4, 10); + expect(plots['rad_zero'].data[0].value).toBe(0); + }); + + it('math.todegrees and math.toradians should be inverse operations', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '1h', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` + const { math, plotchar } = context.pine; + + const original_deg = 123.456; + const original_rad = 1.234; + + const roundtrip_deg = math.todegrees(math.toradians(original_deg)); + const roundtrip_rad = math.toradians(math.todegrees(original_rad)); + + plotchar(roundtrip_deg, 'roundtrip_deg'); + plotchar(roundtrip_rad, 'roundtrip_rad'); + `; + + const { plots } = await pineTS.run(code); + expect(plots['roundtrip_deg'].data[0].value).toBeCloseTo(123.456, 10); + expect(plots['roundtrip_rad'].data[0].value).toBeCloseTo(1.234, 10); + }); + + it('math.todegrees should handle negative values', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '1h', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` + const { math, plotchar } = context.pine; + + const neg_deg = math.todegrees(-math.pi()); + const neg_rad = math.toradians(-180); + + plotchar(neg_deg, 'neg_deg'); + plotchar(neg_rad, 'neg_rad'); + `; + + const { plots } = await pineTS.run(code); + expect(plots['neg_deg'].data[0].value).toBeCloseTo(-180, 10); + expect(plots['neg_rad'].data[0].value).toBeCloseTo(-Math.PI, 10); + }); + + it('math.todegrees and math.toradians should handle NaN', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '1h', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` + const { math, plotchar, na } = context.pine; + + const nan_val = 0 / 0; + const deg_nan = math.todegrees(nan_val); + const rad_nan = math.toradians(nan_val); + + plotchar(na(deg_nan) ? 1 : 0, 'deg_is_nan'); + plotchar(na(rad_nan) ? 1 : 0, 'rad_is_nan'); + `; + + const { plots } = await pineTS.run(code); + expect(plots['deg_is_nan'].data[0].value).toBe(1); + expect(plots['rad_is_nan'].data[0].value).toBe(1); + }); + + it('math.todegrees and math.toradians should handle Infinity', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '1h', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` + const { math, plotchar } = context.pine; + + const deg_inf = math.todegrees(Infinity); + const rad_inf = math.toradians(Infinity); + const deg_neg_inf = math.todegrees(-Infinity); + const rad_neg_inf = math.toradians(-Infinity); + + plotchar(deg_inf, 'deg_inf'); + plotchar(rad_inf, 'rad_inf'); + plotchar(deg_neg_inf, 'deg_neg_inf'); + plotchar(rad_neg_inf, 'rad_neg_inf'); + `; + + const { plots } = await pineTS.run(code); + expect(plots['deg_inf'].data[0].value).toBe(Infinity); + expect(plots['rad_inf'].data[0].value).toBe(Infinity); + expect(plots['deg_neg_inf'].data[0].value).toBe(-Infinity); + expect(plots['rad_neg_inf'].data[0].value).toBe(-Infinity); + }); + }); }); diff --git a/tests/namespaces/ta/oscillators-momentum.test.ts b/tests/namespaces/ta/oscillators-momentum.test.ts index d441269..bedb161 100644 --- a/tests/namespaces/ta/oscillators-momentum.test.ts +++ b/tests/namespaces/ta/oscillators-momentum.test.ts @@ -607,4 +607,7 @@ describe('Technical Analysis - Oscillators & Momentum', () => { console.log('plotdata_str', plotdata_str); expect(plotdata_str.trim()).toEqual(expected_plot.trim()); }); + + + }); diff --git a/tests/transpiler/pinescript-to-js.test.ts b/tests/transpiler/pinescript-to-js.test.ts index b4d9319..29aa22b 100644 --- a/tests/transpiler/pinescript-to-js.test.ts +++ b/tests/transpiler/pinescript-to-js.test.ts @@ -252,7 +252,7 @@ plot(result) expect(jsCode).toContain('function calculate(x, y)'); // Expect _callId to be retrieved from context stack, not passed as argument - expect(jsCode).toContain('$.peekId()'); + expect(jsCode).toContain('$.peekCtx()'); expect(jsCode).toContain('$.let.fn'); }); diff --git a/tests/transpiler/pinets-source-to-js.test.ts b/tests/transpiler/pinets-source-to-js.test.ts index 20e25dc..95bc559 100644 --- a/tests/transpiler/pinets-source-to-js.test.ts +++ b/tests/transpiler/pinets-source-to-js.test.ts @@ -902,39 +902,37 @@ let src_open = input.any({ title: 'Open Source', defval: open }); $.const.glb1__cc = $.init($.const.glb1__cc, close); $.const.glb1_aa = $.init($.const.glb1_aa, 1); function angle(src) { - const temp_1 = $.peekId(); - $.const.fn1__callId = $.init($.const.fn1__callId, temp_1); - $.const.fn1_rad2degree = $.init($.const.fn1_rad2degree, 180 / Math.PI); + const $$ = $.peekCtx(); + $$.const.fn1_rad2degree = $.init($$.const.fn1_rad2degree, 180 / Math.PI); const p0 = ta.param(14, undefined, 'p0'); - const temp_2 = ta.atr(p0, $.get($.const.fn1__callId, 0) + "_ta0"); - const p1 = math.param(($.get(src, 0) - $.get(src, 1)) / temp_2, undefined, 'p1'); - const temp_3 = math.atan(p1); - $.const.fn1_ang = $.init($.const.fn1_ang, $.get($.const.fn1_rad2degree, 0) * temp_3); - return $.precision($.get($.const.fn1_ang, 0)); + const temp_1 = ta.atr(p0, $$.id + "_ta0"); + const p1 = math.param(($.get(src, 0) - $.get(src, 1)) / temp_1, undefined, 'p1'); + const temp_2 = math.atan(p1); + $$.const.fn1_ang = $.init($$.const.fn1_ang, $.get($$.const.fn1_rad2degree, 0) * temp_2); + return $.precision($.get($$.const.fn1_ang, 0)); } function get_average(avg_src, avg_len) { - const temp_4 = $.peekId(); - $.const.fn2__callId = $.init($.const.fn2__callId, temp_4); - $.let.fn2_bb = $.init($.let.fn2_bb, 1); - $.let.fn2_cc = $.init($.let.fn2_cc, close); - $.set($.let.fn2_cc, $.get(close, 1)); - $.set($.let.fn2_cc, $.get($.let.fn2_bb, 2)); - $.set($.let.fn2_cc, $.get($.const.glb1_aa, $.get($.let.fn2_bb, 0))); - $.let.fn2_dd = $.init($.let.fn2_dd, $.get(close, 1)); - $.let.fn2_ee = $.init($.let.fn2_ee, $.get(close, $.get($.const.glb1_aa, 0))); - $.let.fn2_ff = $.init($.let.fn2_ff, $.get(close, $.get($.const.glb1_aa, 99))); - $.let.fn2_cc0 = $.init($.let.fn2_cc0, $.get($.const.glb1__cc, 0)); - $.let.fn2_cc1 = $.init($.let.fn2_cc1, $.get($.const.glb1__cc, 1)); - $.let.fn2_cc2 = $.init($.let.fn2_cc2, $.get($.const.glb1__cc, $.get($.const.glb1_aa, 0))); - $.let.fn2_cc3 = $.init($.let.fn2_cc3, $.get($.const.glb1__cc, $.get($.const.glb1_aa, 99))); - $.let.fn2_ret_val = $.init($.let.fn2_ret_val, 0); + const $$ = $.peekCtx(); + $$.let.fn2_bb = $.init($$.let.fn2_bb, 1); + $$.let.fn2_cc = $.init($$.let.fn2_cc, close); + $.set($$.let.fn2_cc, $.get(close, 1)); + $.set($$.let.fn2_cc, $.get($$.let.fn2_bb, 2)); + $.set($$.let.fn2_cc, $.get($.const.glb1_aa, $.get($$.let.fn2_bb, 0))); + $$.let.fn2_dd = $.init($$.let.fn2_dd, $.get(close, 1)); + $$.let.fn2_ee = $.init($$.let.fn2_ee, $.get(close, $.get($.const.glb1_aa, 0))); + $$.let.fn2_ff = $.init($$.let.fn2_ff, $.get(close, $.get($.const.glb1_aa, 99))); + $$.let.fn2_cc0 = $.init($$.let.fn2_cc0, $.get($.const.glb1__cc, 0)); + $$.let.fn2_cc1 = $.init($$.let.fn2_cc1, $.get($.const.glb1__cc, 1)); + $$.let.fn2_cc2 = $.init($$.let.fn2_cc2, $.get($.const.glb1__cc, $.get($.const.glb1_aa, 0))); + $$.let.fn2_cc3 = $.init($$.let.fn2_cc3, $.get($.const.glb1__cc, $.get($.const.glb1_aa, 99))); + $$.let.fn2_ret_val = $.init($$.let.fn2_ret_val, 0); for (let i = 1; i <= $.get(avg_len, 0); i++) { - $.set($.let.fn2_ret_val, $.get($.let.fn2_ret_val, 0) + $.get(avg_src, i)); + $.set($$.let.fn2_ret_val, $.get($$.let.fn2_ret_val, 0) + $.get(avg_src, i)); } if ($.math.__eq($.get(avg_len, 0), 0)) { - $.set($.let.fn2_ret_val, $.get($.let.fn2_cc, 1)); + $.set($$.let.fn2_ret_val, $.get($$.let.fn2_cc, 1)); } - return $.precision($.get($.let.fn2_ret_val, 0) / $.get(avg_len, 0)); + return $.precision($.get($$.let.fn2_ret_val, 0) / $.get(avg_len, 0)); } const p2 = $.param(close, undefined, 'p2'); const p3 = $.param(14, undefined, 'p3'); @@ -1026,11 +1024,10 @@ let src_open = input.any({ title: 'Open Source', defval: open }); const {close, open} = $.data; const {plot, plotchar, request, ta} = $.pine; function foo() { - const temp_2 = $.peekId(); - $.const.fn1__callId = $.init($.const.fn1__callId, temp_2); - $.const.fn1_oo = $.init($.const.fn1_oo, open); - $.const.fn1_cc = $.init($.const.fn1_cc, close); - return $.precision([[$.get($.const.fn1_oo, 0), $.get($.const.fn1_cc, 0)]]); + const $$ = $.peekCtx(); + $$.const.fn1_oo = $.init($$.const.fn1_oo, open); + $$.const.fn1_cc = $.init($$.const.fn1_cc, close); + return $.precision([[$.get($$.const.fn1_oo, 0), $.get($$.const.fn1_cc, 0)]]); } { $.const.glb1_temp_1 = $.init($.const.glb1_temp_1, $.call(foo, "_fn0")); @@ -1106,28 +1103,27 @@ let src_open = input.any({ title: 'Open Source', defval: open }); $.const.glb1__cc = $.init($.const.glb1__cc, close); $.const.glb1_aa = $.init($.const.glb1_aa, 1); function get_average(avg_src, avg_len) { - const temp_1 = $.peekId(); - $.const.fn1__callId = $.init($.const.fn1__callId, temp_1); - $.let.fn1_bb = $.init($.let.fn1_bb, 1); - $.let.fn1_cc = $.init($.let.fn1_cc, close); - $.set($.let.fn1_cc, $.get(close, 1)); - $.set($.let.fn1_cc, $.get($.let.fn1_bb, 2)); - $.set($.let.fn1_cc, $.get($.const.glb1_aa, $.get($.let.fn1_bb, 0))); - $.let.fn1_dd = $.init($.let.fn1_dd, $.get(close, 1)); - $.let.fn1_ee = $.init($.let.fn1_ee, $.get(close, $.get($.const.glb1_aa, 0))); - $.let.fn1_ff = $.init($.let.fn1_ff, $.get(close, $.get($.const.glb1_aa, 99))); - $.let.fn1_cc0 = $.init($.let.fn1_cc0, $.get($.const.glb1__cc, 0)); - $.let.fn1_cc1 = $.init($.let.fn1_cc1, $.get($.const.glb1__cc, 1)); - $.let.fn1_cc2 = $.init($.let.fn1_cc2, $.get($.const.glb1__cc, $.get($.const.glb1_aa, 0))); - $.let.fn1_cc3 = $.init($.let.fn1_cc3, $.get($.const.glb1__cc, $.get($.const.glb1_aa, 99))); - $.let.fn1_ret_val = $.init($.let.fn1_ret_val, 0); + const $$ = $.peekCtx(); + $$.let.fn1_bb = $.init($$.let.fn1_bb, 1); + $$.let.fn1_cc = $.init($$.let.fn1_cc, close); + $.set($$.let.fn1_cc, $.get(close, 1)); + $.set($$.let.fn1_cc, $.get($$.let.fn1_bb, 2)); + $.set($$.let.fn1_cc, $.get($.const.glb1_aa, $.get($$.let.fn1_bb, 0))); + $$.let.fn1_dd = $.init($$.let.fn1_dd, $.get(close, 1)); + $$.let.fn1_ee = $.init($$.let.fn1_ee, $.get(close, $.get($.const.glb1_aa, 0))); + $$.let.fn1_ff = $.init($$.let.fn1_ff, $.get(close, $.get($.const.glb1_aa, 99))); + $$.let.fn1_cc0 = $.init($$.let.fn1_cc0, $.get($.const.glb1__cc, 0)); + $$.let.fn1_cc1 = $.init($$.let.fn1_cc1, $.get($.const.glb1__cc, 1)); + $$.let.fn1_cc2 = $.init($$.let.fn1_cc2, $.get($.const.glb1__cc, $.get($.const.glb1_aa, 0))); + $$.let.fn1_cc3 = $.init($$.let.fn1_cc3, $.get($.const.glb1__cc, $.get($.const.glb1_aa, 99))); + $$.let.fn1_ret_val = $.init($$.let.fn1_ret_val, 0); for (let i = 1; i <= $.get(avg_len, 0); i++) { - $.set($.let.fn1_ret_val, $.get($.let.fn1_ret_val, 0) + $.get(avg_src, i)); + $.set($$.let.fn1_ret_val, $.get($$.let.fn1_ret_val, 0) + $.get(avg_src, i)); } if ($.math.__eq($.get(avg_len, 0), 0)) { - $.set($.let.fn1_ret_val, $.get($.let.fn1_cc, 1)); + $.set($$.let.fn1_ret_val, $.get($$.let.fn1_cc, 1)); } - return $.precision($.get($.let.fn1_ret_val, 0) / $.get(avg_len, 0)); + return $.precision($.get($$.let.fn1_ret_val, 0) / $.get(avg_len, 0)); } const p0 = $.param(close, undefined, 'p0'); const p1 = $.param(14, undefined, 'p1');