From 7cdb9ad4c23fc7c1b009a1f96336193eb2676b67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:28:40 +0000 Subject: [PATCH 1/2] Initial plan From aa31666e12b39c6665c1241c08c6a90e4270cc9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:32:59 +0000 Subject: [PATCH 2/2] Refactor indicator generation to use docs/official as source - Created indicators/sources.json with 16 indicator mappings - Updated .github/workflows/generate-indicators.yml to read from sources.json - Updated workflow triggers to monitor sources.json and docs/official - Updated example/scripts/generate-indicators.cjs to use sources.json - Deleted duplicate .pine files from example/pinescript/ - Updated example/pinescript/README.md to explain new workflow - Regenerated indicators from official sources Co-authored-by: deepentropy <8287111+deepentropy@users.noreply.github.com> --- .github/workflows/generate-indicators.yml | 54 +++++---- example/pinescript/ADR.pine | 5 - example/pinescript/BOP.pine | 3 - example/pinescript/DEMA.pine | 8 -- example/pinescript/HMA.pine | 6 - example/pinescript/LSMA.pine | 7 -- example/pinescript/Mass Index.pine | 6 - example/pinescript/McGinley Dynamic.pine | 7 -- example/pinescript/Momentum.pine | 6 - example/pinescript/README.md | 38 ++++--- example/pinescript/ROC.pine | 7 -- example/pinescript/SMA.pine | 34 ------ example/pinescript/TEMA.pine | 8 -- example/scripts/generate-indicators.cjs | 37 ++++--- indicators/alma/alma.ts | 93 ++++++++++------ indicators/obv/obv.ts | 110 +++++++++++++++---- indicators/rma/rma.ts | 127 +++++++++++++--------- indicators/sources.json | 84 ++++++++++++++ indicators/vwma/vwma.ts | 121 ++++++++++++--------- indicators/wma/wma.ts | 120 +++++++++++--------- 20 files changed, 523 insertions(+), 358 deletions(-) delete mode 100644 example/pinescript/ADR.pine delete mode 100644 example/pinescript/BOP.pine delete mode 100644 example/pinescript/DEMA.pine delete mode 100644 example/pinescript/HMA.pine delete mode 100644 example/pinescript/LSMA.pine delete mode 100644 example/pinescript/Mass Index.pine delete mode 100644 example/pinescript/McGinley Dynamic.pine delete mode 100644 example/pinescript/Momentum.pine delete mode 100644 example/pinescript/ROC.pine delete mode 100644 example/pinescript/SMA.pine delete mode 100644 example/pinescript/TEMA.pine create mode 100644 indicators/sources.json diff --git a/.github/workflows/generate-indicators.yml b/.github/workflows/generate-indicators.yml index 0f4cabf..d7dc4e0 100644 --- a/.github/workflows/generate-indicators.yml +++ b/.github/workflows/generate-indicators.yml @@ -4,8 +4,10 @@ on: push: branches: [main] paths: - # Trigger when PineScript source files change - - 'example/pinescript/**/*.pine' + # Trigger when indicator sources config changes + - 'indicators/sources.json' + # Or when official PineScript files change + - 'docs/official/indicators_standard/**/*.pine' # Or when transpiler changes - 'packages/pine2ts/**' workflow_dispatch: # Allow manual trigger @@ -38,25 +40,35 @@ jobs: - name: Generate indicators run: | - # Find all .pine files and transpile them - for pine_file in example/pinescript/*.pine; do - if [ -f "$pine_file" ]; then - # Extract base name and convert to kebab-case - base_name=$(basename "$pine_file" .pine) - # Replace spaces first, then convert camelCase to kebab-case - indicator_name=$(echo "$base_name" | tr ' ' '-' | sed 's/\([a-z0-9]\)\([A-Z]\)/\1-\2/g' | tr '[:upper:]' '[:lower:]') - output_dir="indicators/$indicator_name" - output_file="$output_dir/$indicator_name.ts" - - echo "Transpiling $pine_file -> $output_file" - - # Create output directory if it doesn't exist - mkdir -p "$output_dir" - - # Run the transpiler - node ./packages/pine2ts/bin/pine2ts.js "$pine_file" "$output_file" - fi - done + # Read the sources.json and transpile each indicator + node << 'EOF' + const fs = require('fs'); + const { execSync } = require('child_process'); + const path = require('path'); + + const sources = JSON.parse(fs.readFileSync('indicators/sources.json', 'utf-8')); + + for (const indicator of sources.indicators) { + const sourcePath = indicator.sourcePath; + const outputDir = `indicators/${indicator.id}`; + const outputFile = `${outputDir}/${indicator.id}.ts`; + + if (!fs.existsSync(sourcePath)) { + console.error(`Source file not found: ${sourcePath}`); + continue; + } + + console.log(`Transpiling ${indicator.name} -> ${outputFile}`); + + // Create output directory + fs.mkdirSync(outputDir, { recursive: true }); + + // Run transpiler + execSync(`node ./packages/pine2ts/bin/pine2ts.js "${sourcePath}" "${outputFile}"`, { + stdio: 'inherit' + }); + } + EOF - name: Compile indicators to JavaScript run: | diff --git a/example/pinescript/ADR.pine b/example/pinescript/ADR.pine deleted file mode 100644 index d5a9431..0000000 --- a/example/pinescript/ADR.pine +++ /dev/null @@ -1,5 +0,0 @@ -//@version=6 -indicator("Average Day Range", shorttitle="ADR", timeframe="", timeframe_gaps=true) -lengthInput = input.int(14, title="Length") -adr = ta.sma(high - low, lengthInput) -plot(adr, title="ADR") \ No newline at end of file diff --git a/example/pinescript/BOP.pine b/example/pinescript/BOP.pine deleted file mode 100644 index a0a0415..0000000 --- a/example/pinescript/BOP.pine +++ /dev/null @@ -1,3 +0,0 @@ -//@version=6 -indicator(title="Balance of Power", format=format.price, precision=2, timeframe="", timeframe_gaps=true) -plot((close - open) / (high - low), color=color.red) \ No newline at end of file diff --git a/example/pinescript/DEMA.pine b/example/pinescript/DEMA.pine deleted file mode 100644 index 7297d63..0000000 --- a/example/pinescript/DEMA.pine +++ /dev/null @@ -1,8 +0,0 @@ -//@version=6 -indicator(title="Double EMA", shorttitle="DEMA", overlay=true, timeframe="", timeframe_gaps=true) -length = input.int(9, minval=1) -src = input(close, title="Source") -e1 = ta.ema(src, length) -e2 = ta.ema(e1, length) -dema = 2 * e1 - e2 -plot(dema, "DEMA", color=#43A047) \ No newline at end of file diff --git a/example/pinescript/HMA.pine b/example/pinescript/HMA.pine deleted file mode 100644 index 98130a5..0000000 --- a/example/pinescript/HMA.pine +++ /dev/null @@ -1,6 +0,0 @@ -//@version=6 -indicator(title="Hull Moving Average", shorttitle="HMA", overlay=true, timeframe="", timeframe_gaps=true) -length = input.int(9, "Length", minval = 2) -src = input(close, "Source") -hullma = ta.wma(2*ta.wma(src, length/2)-ta.wma(src, length), math.floor(math.sqrt(length))) -plot(hullma, "HMA") \ No newline at end of file diff --git a/example/pinescript/LSMA.pine b/example/pinescript/LSMA.pine deleted file mode 100644 index 2d4a127..0000000 --- a/example/pinescript/LSMA.pine +++ /dev/null @@ -1,7 +0,0 @@ -//@version=6 -indicator(title = "Least Squares Moving Average", shorttitle="LSMA", overlay=true, timeframe="", timeframe_gaps=true) -length = input(title="Length", defval=25) -offset = input(title="Offset", defval=0) -src = input(close, title="Source") -lsma = ta.linreg(src, length, offset) -plot(lsma, "LSMA") \ No newline at end of file diff --git a/example/pinescript/Mass Index.pine b/example/pinescript/Mass Index.pine deleted file mode 100644 index 84bc210..0000000 --- a/example/pinescript/Mass Index.pine +++ /dev/null @@ -1,6 +0,0 @@ -//@version=6 -indicator(title="Mass Index", format=format.price, precision=2, timeframe="", timeframe_gaps=true) -length = input.int(10, minval=1) -span = high - low -mi = math.sum(ta.ema(span, 9) / ta.ema(ta.ema(span, 9), 9), length) -plot(mi, "Mass Index") \ No newline at end of file diff --git a/example/pinescript/McGinley Dynamic.pine b/example/pinescript/McGinley Dynamic.pine deleted file mode 100644 index 6d7f265..0000000 --- a/example/pinescript/McGinley Dynamic.pine +++ /dev/null @@ -1,7 +0,0 @@ -//@version=6 -indicator(title="McGinley Dynamic", overlay=true, timeframe="", timeframe_gaps=true) -length = input.int(14, minval=1) -source = close -mg = 0.0 -mg := na(mg[1]) ? ta.ema(source, length) : mg[1] + (source - mg[1]) / (length * math.pow(source/mg[1], 4)) -plot(mg, "McGinley Dynamic") \ No newline at end of file diff --git a/example/pinescript/Momentum.pine b/example/pinescript/Momentum.pine deleted file mode 100644 index beac304..0000000 --- a/example/pinescript/Momentum.pine +++ /dev/null @@ -1,6 +0,0 @@ -//@version=6 -indicator(title="Momentum", shorttitle="Mom", timeframe="", timeframe_gaps=true) -len = input.int(10, minval=1, title="Length") -src = input(close, title="Source") -mom = src - src[len] -plot(mom, color=#2962FF, title="MOM") \ No newline at end of file diff --git a/example/pinescript/README.md b/example/pinescript/README.md index 38f0e3a..97f0766 100644 --- a/example/pinescript/README.md +++ b/example/pinescript/README.md @@ -1,26 +1,28 @@ # PineScript Sources -This directory contains the source PineScript files for indicators. +Indicator source files are now located in `docs/official/indicators_standard/`. -When you commit changes to files in this directory, a GitHub Action will automatically: -1. Run the `pine2ts` transpiler -2. Generate TypeScript indicators in `indicators/` -3. Commit the generated files +To add or modify which indicators are generated: +1. Edit `indicators/sources.json` to add/remove indicator entries +2. Each entry should have an `id`, `name`, and `sourcePath` pointing to the official .pine file +3. Commit and push - the GitHub Action will automatically transpile the indicators -## Adding a new indicator +## Example Entry -1. Create a new `.pine` file in this directory -2. Use standard PineScript v6 syntax -3. Commit and push -4. The indicator will be auto-generated in `indicators/` +```json +{ + "id": "sma", + "name": "Simple Moving Average (SMA)", + "sourcePath": "docs/official/indicators_standard/Moving Average Simple.pine" +} +``` + +## Local Development -## Example +To generate indicators locally, run: -```pine -//@version=6 -indicator(title="My Custom Indicator", shorttitle="MCI", overlay=true) -length = input.int(14, minval=1, title="Length") -src = input(close, title="Source") -result = ta.sma(src, length) -plot(result, "MCI", color=color.blue) +```bash +pnpm generate-indicators ``` + +This reads from `indicators/sources.json` and transpiles each indicator to TypeScript. diff --git a/example/pinescript/ROC.pine b/example/pinescript/ROC.pine deleted file mode 100644 index b7686ac..0000000 --- a/example/pinescript/ROC.pine +++ /dev/null @@ -1,7 +0,0 @@ -//@version=6 -indicator(title="Rate Of Change", shorttitle="ROC", format=format.price, precision=2, timeframe="", timeframe_gaps=true) -length = input.int(9, minval=1) -source = input(close, "Source") -roc = 100 * (source - source[length])/source[length] -plot(roc, color=#2962FF, title="ROC") -hline(0, color=#787B86, title="Zero Line") \ No newline at end of file diff --git a/example/pinescript/SMA.pine b/example/pinescript/SMA.pine deleted file mode 100644 index defd4d6..0000000 --- a/example/pinescript/SMA.pine +++ /dev/null @@ -1,34 +0,0 @@ -//@version=6 -indicator(title="Moving Average Simple", shorttitle="SMA", overlay=true, timeframe="", timeframe_gaps=true) -len = input.int(9, minval=1, title="Length") -src = input(close, title="Source") -offset = input.int(title="Offset", defval=0, minval=-500, maxval=500, display = display.data_window) -out = ta.sma(src, len) -plot(out, color=color.blue, title="MA", offset=offset) - -// Smoothing MA inputs -GRP = "Smoothing" -TT_BB = "Only applies when 'SMA + Bollinger Bands' is selected. Determines the distance between the SMA and the bands." -maTypeInput = input.string("None", "Type", options = ["None", "SMA", "SMA + Bollinger Bands", "EMA", "SMMA (RMA)", "WMA", "VWMA"], group = GRP, display = display.data_window) -var isBB = maTypeInput == "SMA + Bollinger Bands" -maLengthInput = input.int(14, "Length", group = GRP, display = display.data_window, active = maTypeInput != "None") -bbMultInput = input.float(2.0, "BB StdDev", minval = 0.001, maxval = 50, step = 0.5, tooltip = TT_BB, group = GRP, display = display.data_window, active = isBB) -var enableMA = maTypeInput != "None" - -// Smoothing MA Calculation -ma(source, length, MAtype) => - switch MAtype - "SMA" => ta.sma(source, length) - "SMA + Bollinger Bands" => ta.sma(source, length) - "EMA" => ta.ema(source, length) - "SMMA (RMA)" => ta.rma(source, length) - "WMA" => ta.wma(source, length) - "VWMA" => ta.vwma(source, length) - -// Smoothing MA plots -smoothingMA = enableMA ? ma(out, maLengthInput, maTypeInput) : na -smoothingStDev = isBB ? ta.stdev(out, maLengthInput) * bbMultInput : na -plot(smoothingMA, "SMA-based MA", color=color.yellow, display = enableMA ? display.all : display.none, editable = enableMA) -bbUpperBand = plot(smoothingMA + smoothingStDev, title = "Upper Bollinger Band", color=color.green, display = isBB ? display.all : display.none, editable = isBB) -bbLowerBand = plot(smoothingMA - smoothingStDev, title = "Lower Bollinger Band", color=color.green, display = isBB ? display.all : display.none, editable = isBB) -fill(bbUpperBand, bbLowerBand, color= isBB ? color.new(color.green, 90) : na, title="Bollinger Bands Background Fill", display = isBB ? display.all : display.none, editable = isBB) diff --git a/example/pinescript/TEMA.pine b/example/pinescript/TEMA.pine deleted file mode 100644 index 71afccd..0000000 --- a/example/pinescript/TEMA.pine +++ /dev/null @@ -1,8 +0,0 @@ -//@version=6 -indicator(title="Triple EMA", shorttitle="TEMA", overlay=true, timeframe="", timeframe_gaps=true) -length = input.int(9, minval=1) -ema1 = ta.ema(close, length) -ema2 = ta.ema(ema1, length) -ema3 = ta.ema(ema2, length) -out = 3 * (ema1 - ema2) + ema3 -plot(out, "TEMA", color=#2962FF) \ No newline at end of file diff --git a/example/scripts/generate-indicators.cjs b/example/scripts/generate-indicators.cjs index 6d4f6b7..8407914 100755 --- a/example/scripts/generate-indicators.cjs +++ b/example/scripts/generate-indicators.cjs @@ -3,24 +3,31 @@ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); -const PINE_DIR = path.join(__dirname, '..', '..', 'example', 'pinescript'); +const SOURCES_JSON = path.join(__dirname, '..', '..', 'indicators', 'sources.json'); const INDICATORS_DIR = path.join(__dirname, '..', '..', 'indicators'); +const PROJECT_ROOT = path.join(__dirname, '..', '..'); -// Find all .pine files -const pineFiles = fs.readdirSync(PINE_DIR).filter(f => f.endsWith('.pine')); +// Read sources.json +if (!fs.existsSync(SOURCES_JSON)) { + console.error(`Error: sources.json not found at ${SOURCES_JSON}`); + process.exit(1); +} + +const sources = JSON.parse(fs.readFileSync(SOURCES_JSON, 'utf-8')); -console.log(`Found ${pineFiles.length} PineScript files to transpile`); +console.log(`Found ${sources.indicators.length} indicators to transpile`); -for (const pineFile of pineFiles) { - const baseName = path.basename(pineFile, '.pine'); - // Convert to kebab-case: replace spaces first, then handle camelCase - const indicatorName = baseName - .replace(/\s+/g, '-') // Replace spaces with hyphens first - .replace(/([a-z0-9])([A-Z])/g, '$1-$2') // Insert hyphen between lowercase/digit and uppercase - .toLowerCase(); - const outputDir = path.join(INDICATORS_DIR, indicatorName); +for (const indicator of sources.indicators) { + const sourcePath = path.join(PROJECT_ROOT, indicator.sourcePath); + const outputDir = path.join(INDICATORS_DIR, indicator.id); + const outputFile = path.join(outputDir, `${indicator.id}.ts`); + + if (!fs.existsSync(sourcePath)) { + console.error(`Source file not found: ${sourcePath}`); + continue; + } - console.log(`Transpiling ${pineFile} -> ${indicatorName}/`); + console.log(`Transpiling ${indicator.name} -> ${indicator.id}/`); try { // Create output directory if it doesn't exist @@ -28,11 +35,11 @@ for (const pineFile of pineFiles) { fs.mkdirSync(outputDir, { recursive: true }); } - execSync(`node ./transpiler/bin/pine2ts.js "${path.join(PINE_DIR, pineFile)}" "${path.join(outputDir, indicatorName + '.ts')}"`, { + execSync(`node ./packages/pine2ts/bin/pine2ts.js "${sourcePath}" "${outputFile}"`, { stdio: 'inherit' }); } catch (error) { - console.error(`Failed to transpile ${pineFile}:`, error.message); + console.error(`Failed to transpile ${indicator.name}:`, error.message); } } diff --git a/indicators/alma/alma.ts b/indicators/alma/alma.ts index 74a2e9a..4a4c6a5 100644 --- a/indicators/alma/alma.ts +++ b/indicators/alma/alma.ts @@ -1,33 +1,12 @@ -import { Series, taCore, type IndicatorResult } from '@deepentropy/oakscriptjs'; +import { Series, ta, taCore, math, array, type IndicatorResult } from '@deepentropy/oakscriptjs'; -export interface IndicatorInputs { - lengthInput: number; - offsetInput: number; - sigmaInput: number; +// Helper functions +function na(value: number | null | undefined): boolean { + return value === null || value === undefined || Number.isNaN(value); } -const defaultInputs: IndicatorInputs = { - lengthInput: 9, - offsetInput: 0.85, - sigmaInput: 6, -}; - -export function Arnaud_Legoux_Moving_Average(bars: any[], inputs: Partial = {}): IndicatorResult { - const { lengthInput, offsetInput, sigmaInput } = { ...defaultInputs, ...inputs }; - - // Close series for ALMA calculation - const close = new Series(bars, (bar) => bar.close); - - // @version=6 - // ALMA calculation using taCore.alma (array-based) - const closeArray = close.toArray(); - const almaArray = taCore.alma(closeArray, lengthInput, offsetInput, sigmaInput); - const almaValues = new Series(bars, (bar, i) => almaArray[i] ?? NaN); - - return { - metadata: { title: "Arnaud Legoux Moving Average", shorttitle: "ALMA", overlay: true }, - plots: { 'plot0': almaValues.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, - }; +function nz(value: number | null | undefined, replacement: number = 0): number { + return na(value) ? replacement : value as number; } // Plot configuration interface @@ -50,15 +29,61 @@ export interface InputConfig { options?: string[]; } +export interface IndicatorInputs { + lengthInput: number; + offsetInput: number; + sigmaInput: number; +} + +const defaultInputs: IndicatorInputs = { + lengthInput: 9, + offsetInput: 0.85, + sigmaInput: 6, +}; + +export function Arnaud_Legoux_Moving_Average(bars: any[], inputs: Partial = {}): IndicatorResult { + const { lengthInput, offsetInput, sigmaInput } = { ...defaultInputs, ...inputs }; + +// OHLCV Series +const open = new Series(bars, (bar) => bar.open); +const high = new Series(bars, (bar) => bar.high); +const low = new Series(bars, (bar) => bar.low); +const close = new Series(bars, (bar) => bar.close); +const volume = new Series(bars, (bar) => bar.volume ?? 0); + +// Calculated price sources +const hl2 = high.add(low).div(2); +const hlc3 = high.add(low).add(close).div(3); +const ohlc4 = open.add(high).add(low).add(close).div(4); +const hlcc4 = high.add(low).add(close).add(close).div(4); + +// Time series +const year = new Series(bars, (bar) => new Date(bar.time).getFullYear()); +const month = new Series(bars, (bar) => new Date(bar.time).getMonth() + 1); +const dayofmonth = new Series(bars, (bar) => new Date(bar.time).getDate()); +const dayofweek = new Series(bars, (bar) => new Date(bar.time).getDay() + 1); +const hour = new Series(bars, (bar) => new Date(bar.time).getHours()); +const minute = new Series(bars, (bar) => new Date(bar.time).getMinutes()); + +// Bar index +const last_bar_index = bars.length - 1; + + // @version=6 + const TT_OFFSET = "Controls tradeoff between smoothness (closer to 1) and responsiveness (closer to 0)."; + const TT_SIGMA = "This element is a standard deviation that is applied to the combo line in order for it to appear more sharp."; + const source = close; + + return { + metadata: { title: "Arnaud Legoux Moving Average", shorttitle: "ALMA", overlay: true }, + plots: { 'plot0': ta.alma(source, lengthInput, offsetInput, sigmaInput).toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, + }; +} + // Additional exports for compatibility export const metadata = { title: "Arnaud Legoux Moving Average", shortTitle: "ALMA", overlay: true }; export { defaultInputs }; -export const inputConfig: InputConfig[] = [ - { id: 'lengthInput', type: 'int', title: 'Length', defval: 9, min: 1 }, - { id: 'offsetInput', type: 'float', title: 'Offset', defval: 0.85, step: 0.01 }, - { id: 'sigmaInput', type: 'float', title: 'Sigma', defval: 6 } -]; -export const plotConfig: PlotConfig[] = [{ id: 'plot0', title: 'ALMA', color: '#2962FF', lineWidth: 2 }]; +export const inputConfig: InputConfig[] = [{ id: 'lengthInput', type: 'int', title: 'Length', defval: 9, min: 1 }, { id: 'offsetInput', type: 'float', title: 'Offset', defval: 0.85, step: 0.01 }, { id: 'sigmaInput', type: 'float', title: 'Sigma', defval: 6 }]; +export const plotConfig: PlotConfig[] = [{ id: 'plot0', title: 'Plot 0', color: '#2962FF', lineWidth: 2 }]; export const calculate = Arnaud_Legoux_Moving_Average; export { Arnaud_Legoux_Moving_Average as Arnaud_Legoux_Moving_AverageIndicator }; -export type Arnaud_Legoux_Moving_AverageInputs = IndicatorInputs; +export type Arnaud_Legoux_Moving_AverageInputs = IndicatorInputs; \ No newline at end of file diff --git a/indicators/obv/obv.ts b/indicators/obv/obv.ts index 93e6c92..a32af6b 100644 --- a/indicators/obv/obv.ts +++ b/indicators/obv/obv.ts @@ -1,27 +1,12 @@ -import { Series, ta, math, type IndicatorResult } from '@deepentropy/oakscriptjs'; +import { Series, ta, taCore, math, array, type IndicatorResult } from '@deepentropy/oakscriptjs'; -export interface IndicatorInputs { - // No inputs for basic OBV - it uses close and volume by default +// Helper functions +function na(value: number | null | undefined): boolean { + return value === null || value === undefined || Number.isNaN(value); } -const defaultInputs: IndicatorInputs = {}; - -export function On_Balance_Volume(bars: any[], inputs: Partial = {}): IndicatorResult { - // OHLCV Series - const close = new Series(bars, (bar) => bar.close); - const volume = new Series(bars, (bar) => bar.volume ?? 0); - - // @version=6 - // obv = ta.cum(math.sign(ta.change(src)) * volume) - const changeClose = ta.change(close, 1); - const signChange = math.sign(changeClose); - const signedVolume = signChange.mul(volume); - const obv = ta.cum(signedVolume); - - return { - metadata: { title: "On Balance Volume", shorttitle: "OBV", overlay: false }, - plots: { 'plot0': obv.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, - }; +function nz(value: number | null | undefined, replacement: number = 0): number { + return na(value) ? replacement : value as number; } // Plot configuration interface @@ -44,11 +29,88 @@ export interface InputConfig { options?: string[]; } +export interface IndicatorInputs { + maTypeInput: "None" | "SMA" | "SMA + Bollinger Bands" | "EMA" | "SMMA (RMA)" | "WMA" | "VWMA"; + maLengthInput: number; + bbMultInput: number; +} + +const defaultInputs: IndicatorInputs = { + maTypeInput: "None", + maLengthInput: 14, + bbMultInput: 2, +}; + +export function On_Balance_Volume(bars: any[], inputs: Partial = {}): IndicatorResult { + const { maTypeInput, maLengthInput, bbMultInput } = { ...defaultInputs, ...inputs }; + +// OHLCV Series +const open = new Series(bars, (bar) => bar.open); +const high = new Series(bars, (bar) => bar.high); +const low = new Series(bars, (bar) => bar.low); +const close = new Series(bars, (bar) => bar.close); +const volume = new Series(bars, (bar) => bar.volume ?? 0); + +// Calculated price sources +const hl2 = high.add(low).div(2); +const hlc3 = high.add(low).add(close).div(3); +const ohlc4 = open.add(high).add(low).add(close).div(4); +const hlcc4 = high.add(low).add(close).add(close).div(4); + +// Time series +const year = new Series(bars, (bar) => new Date(bar.time).getFullYear()); +const month = new Series(bars, (bar) => new Date(bar.time).getMonth() + 1); +const dayofmonth = new Series(bars, (bar) => new Date(bar.time).getDate()); +const dayofweek = new Series(bars, (bar) => new Date(bar.time).getDay() + 1); +const hour = new Series(bars, (bar) => new Date(bar.time).getHours()); +const minute = new Series(bars, (bar) => new Date(bar.time).getMinutes()); + +// Bar index +const last_bar_index = bars.length - 1; + + // @version=6 + const cumVol = 0; + (cumVol += nz(volume)); + if ((i === bars.length - 1)) { + runtime.error("No volume is provided by the data vendor."); + } + const src = close; + const obv = ta.cum(math.sign(ta.change(src)).mul(volume)); + // Smoothing MA inputs + const GRP = "Smoothing"; + const TT_BB = "Only applies when 'SMA + Bollinger Bands' is selected. Determines the distance between the SMA and the bands."; + const isBB = (maTypeInput == "SMA + Bollinger Bands"); + const enableMA = (maTypeInput != "None"); + // Smoothing MA Calculation + function ma(source: any, length: any, MAtype: any): any { + return (() => { + switch (MAtype) { + case "SMA": return ta.sma(source, length); + case "SMA + Bollinger Bands": return ta.sma(source, length); + case "EMA": return ta.ema(source, length); + case "SMMA (RMA)": return ta.rma(source, length); + case "WMA": return ta.wma(source, length); + case "VWMA": return ta.vwma(source, length, volume); + } + })(); + } + // Smoothing MA plots + const smoothingMA = (enableMA ? ma(obv, maLengthInput, maTypeInput) : new Series(bars, () => NaN)); + const smoothingStDev = (isBB ? ta.stdev(obv, maLengthInput).mul(bbMultInput) : new Series(bars, () => NaN)); + // bbUpperBand = ; + // bbLowerBand = ; + + return { + metadata: { title: "On Balance Volume", shorttitle: "OBV", overlay: false }, + plots: { 'plot0': obv.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })), 'plot1': smoothingMA.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })), 'plot2': smoothingMA.add(smoothingStDev).toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })), 'plot3': smoothingMA.sub(smoothingStDev).toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, + }; +} + // Additional exports for compatibility export const metadata = { title: "On Balance Volume", shortTitle: "OBV", overlay: false }; export { defaultInputs }; -export const inputConfig: InputConfig[] = []; -export const plotConfig: PlotConfig[] = [{ id: 'plot0', title: 'OnBalanceVolume', color: '#2962FF', lineWidth: 2 }]; +export const inputConfig: InputConfig[] = [{ id: 'maTypeInput', type: 'string', title: 'Type', defval: "None", options: ['None', 'SMA', 'SMA + Bollinger Bands', 'EMA', 'SMMA (RMA)', 'WMA', 'VWMA'] }, { id: 'maLengthInput', type: 'int', title: 'Length', defval: 14 }, { id: 'bbMultInput', type: 'float', title: 'BB StdDev', defval: 2, min: 0.001, max: 50, step: 0.5 }]; +export const plotConfig: PlotConfig[] = [{ id: 'plot0', title: 'OnBalanceVolume', color: '#2962FF', lineWidth: 2 }, { id: 'plot1', title: 'smoothingMA', color: '#FFFF00', lineWidth: 2 }, { id: 'plot2', title: 'Upper Bollinger Band', color: '#00FF00', lineWidth: 2 }, { id: 'plot3', title: 'Lower Bollinger Band', color: '#00FF00', lineWidth: 2 }]; export const calculate = On_Balance_Volume; export { On_Balance_Volume as On_Balance_VolumeIndicator }; -export type On_Balance_VolumeInputs = IndicatorInputs; +export type On_Balance_VolumeInputs = IndicatorInputs; \ No newline at end of file diff --git a/indicators/rma/rma.ts b/indicators/rma/rma.ts index 87add05..71fc055 100644 --- a/indicators/rma/rma.ts +++ b/indicators/rma/rma.ts @@ -1,53 +1,12 @@ -import { Series, ta, type IndicatorResult } from '@deepentropy/oakscriptjs'; +import { Series, ta, taCore, math, array, type IndicatorResult } from '@deepentropy/oakscriptjs'; -export interface IndicatorInputs { - len: number; - src: "open" | "high" | "low" | "close" | "hl2" | "hlc3" | "ohlc4" | "hlcc4"; +// Helper functions +function na(value: number | null | undefined): boolean { + return value === null || value === undefined || Number.isNaN(value); } -const defaultInputs: IndicatorInputs = { - len: 7, - src: "close", -}; - -export function Smoothed_Moving_Average(bars: any[], inputs: Partial = {}): IndicatorResult { - const { len, src } = { ...defaultInputs, ...inputs }; - - // OHLCV Series - const open = new Series(bars, (bar) => bar.open); - const high = new Series(bars, (bar) => bar.high); - const low = new Series(bars, (bar) => bar.low); - const close = new Series(bars, (bar) => bar.close); - - // Calculated price sources - const hl2 = high.add(low).div(2); - const hlc3 = high.add(low).add(close).div(3); - const ohlc4 = open.add(high).add(low).add(close).div(4); - const hlcc4 = high.add(low).add(close).add(close).div(4); - - // Map source inputs to Series - const srcSeries = (() => { - switch (src) { - case "open": return open; - case "high": return high; - case "low": return low; - case "close": return close; - case "hl2": return hl2; - case "hlc3": return hlc3; - case "ohlc4": return ohlc4; - case "hlcc4": return hlcc4; - default: return close; - } - })(); - - // @version=6 - // RMA calculation using ta.rma - const smma = ta.rma(srcSeries, len); - - return { - metadata: { title: "Smoothed Moving Average", shorttitle: "SMMA", overlay: true }, - plots: { 'plot0': smma.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, - }; +function nz(value: number | null | undefined, replacement: number = 0): number { + return na(value) ? replacement : value as number; } // Plot configuration interface @@ -70,11 +29,79 @@ export interface InputConfig { options?: string[]; } +export interface IndicatorInputs { + len: number; + src: "open" | "high" | "low" | "close" | "hl2" | "hlc3" | "ohlc4" | "hlcc4"; +} + +const defaultInputs: IndicatorInputs = { + len: 7, + src: "close", +}; + +export function Smoothed_Moving_Average(bars: any[], inputs: Partial = {}): IndicatorResult { + const { len, src } = { ...defaultInputs, ...inputs }; + +// OHLCV Series +const open = new Series(bars, (bar) => bar.open); +const high = new Series(bars, (bar) => bar.high); +const low = new Series(bars, (bar) => bar.low); +const close = new Series(bars, (bar) => bar.close); +const volume = new Series(bars, (bar) => bar.volume ?? 0); + +// Calculated price sources +const hl2 = high.add(low).div(2); +const hlc3 = high.add(low).add(close).div(3); +const ohlc4 = open.add(high).add(low).add(close).div(4); +const hlcc4 = high.add(low).add(close).add(close).div(4); + +// Map source inputs to Series +const srcSeries = (() => { + switch (src) { + case "open": return open; + case "high": return high; + case "low": return low; + case "close": return close; + case "hl2": return hl2; + case "hlc3": return hlc3; + case "ohlc4": return ohlc4; + case "hlcc4": return hlcc4; + default: return close; + } +})(); + +// Time series +const year = new Series(bars, (bar) => new Date(bar.time).getFullYear()); +const month = new Series(bars, (bar) => new Date(bar.time).getMonth() + 1); +const dayofmonth = new Series(bars, (bar) => new Date(bar.time).getDate()); +const dayofweek = new Series(bars, (bar) => new Date(bar.time).getDay() + 1); +const hour = new Series(bars, (bar) => new Date(bar.time).getHours()); +const minute = new Series(bars, (bar) => new Date(bar.time).getMinutes()); + +// Bar index +const last_bar_index = bars.length - 1; + + // @version=6 + let smma = new Series(bars, () => 0); + // Recursive formula for smma + const smmaValues: number[] = new Array(bars.length).fill(NaN); + for (let i = 0; i < bars.length; i++) { + const smmaPrev = i > 0 ? smmaValues[i - 1] : NaN; + smmaValues[i] = (na(smmaPrev) ? ta.sma(srcSeries, len).get(i) : (((smmaPrev * (len - 1)) + srcSeries.get(i)) / len)); + } + smma = Series.fromArray(bars, smmaValues); + + return { + metadata: { title: "Smoothed Moving Average", shorttitle: "SMMA", overlay: true }, + plots: { 'plot0': smma.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, + }; +} + // Additional exports for compatibility export const metadata = { title: "Smoothed Moving Average", shortTitle: "SMMA", overlay: true }; export { defaultInputs }; -export const inputConfig: InputConfig[] = [{ id: 'len', type: 'int', title: 'Length', defval: 7, min: 1 }, { id: 'src', type: 'source', title: 'Source', defval: "close", options: ['open', 'high', 'low', 'close', 'hl2', 'hlc3', 'ohlc4', 'hlcc4'] }]; -export const plotConfig: PlotConfig[] = [{ id: 'plot0', title: 'SMMA', color: '#673AB7', lineWidth: 2 }]; +export const inputConfig: InputConfig[] = [{ id: 'len', type: 'int', title: 'Length', defval: 7, min: 1 }, { id: 'src', type: 'source', title: 'Source', defval: "close" }]; +export const plotConfig: PlotConfig[] = [{ id: 'plot0', title: 'smma', color: '#673AB7', lineWidth: 2 }]; export const calculate = Smoothed_Moving_Average; export { Smoothed_Moving_Average as Smoothed_Moving_AverageIndicator }; -export type Smoothed_Moving_AverageInputs = IndicatorInputs; +export type Smoothed_Moving_AverageInputs = IndicatorInputs; \ No newline at end of file diff --git a/indicators/sources.json b/indicators/sources.json new file mode 100644 index 0000000..20596e0 --- /dev/null +++ b/indicators/sources.json @@ -0,0 +1,84 @@ +{ + "indicators": [ + { + "id": "sma", + "name": "Simple Moving Average (SMA)", + "sourcePath": "docs/official/indicators_standard/Moving Average Simple.pine" + }, + { + "id": "momentum", + "name": "Momentum (MOM)", + "sourcePath": "docs/official/indicators_standard/Momentum.pine" + }, + { + "id": "bop", + "name": "Balance of Power (BOP)", + "sourcePath": "docs/official/indicators_standard/Balance of Power.pine" + }, + { + "id": "dema", + "name": "Double EMA (DEMA)", + "sourcePath": "docs/official/indicators_standard/Double EMA.pine" + }, + { + "id": "tema", + "name": "Triple EMA (TEMA)", + "sourcePath": "docs/official/indicators_standard/Triple EMA.pine" + }, + { + "id": "roc", + "name": "Rate of Change (ROC)", + "sourcePath": "docs/official/indicators_standard/Rate Of Change.pine" + }, + { + "id": "adr", + "name": "Average Day Range (ADR)", + "sourcePath": "docs/official/indicators_standard/Average Day Range.pine" + }, + { + "id": "mass-index", + "name": "Mass Index", + "sourcePath": "docs/official/indicators_standard/Mass Index.pine" + }, + { + "id": "mc-ginley-dynamic", + "name": "McGinley Dynamic", + "sourcePath": "docs/official/indicators_standard/McGinley Dynamic.pine" + }, + { + "id": "hma", + "name": "Hull Moving Average (HMA)", + "sourcePath": "docs/official/indicators_standard/Hull Moving Average.pine" + }, + { + "id": "lsma", + "name": "Least Squares Moving Average (LSMA)", + "sourcePath": "docs/official/indicators_standard/Least Squares Moving Average.pine" + }, + { + "id": "rma", + "name": "Smoothed Moving Average (RMA)", + "sourcePath": "docs/official/indicators_standard/Smoothed Moving Average.pine" + }, + { + "id": "wma", + "name": "Weighted Moving Average (WMA)", + "sourcePath": "docs/official/indicators_standard/Moving Average Weighted.pine" + }, + { + "id": "vwma", + "name": "Volume Weighted Moving Average (VWMA)", + "sourcePath": "docs/official/indicators_standard/Volume Weighted Moving Average.pine" + }, + { + "id": "alma", + "name": "Arnaud Legoux Moving Average (ALMA)", + "sourcePath": "docs/official/indicators_standard/Arnaud Legoux Moving Average.pine" + }, + { + "id": "obv", + "name": "On Balance Volume (OBV)", + "sourcePath": "docs/official/indicators_standard/On Balance Volume.pine" + } + ] +} diff --git a/indicators/vwma/vwma.ts b/indicators/vwma/vwma.ts index 6fa4d9e..cb168d9 100644 --- a/indicators/vwma/vwma.ts +++ b/indicators/vwma/vwma.ts @@ -1,54 +1,12 @@ -import { Series, ta, type IndicatorResult } from '@deepentropy/oakscriptjs'; +import { Series, ta, taCore, math, array, type IndicatorResult } from '@deepentropy/oakscriptjs'; -export interface IndicatorInputs { - len: number; - src: "open" | "high" | "low" | "close" | "hl2" | "hlc3" | "ohlc4" | "hlcc4"; +// Helper functions +function na(value: number | null | undefined): boolean { + return value === null || value === undefined || Number.isNaN(value); } -const defaultInputs: IndicatorInputs = { - len: 20, - src: "close", -}; - -export function Volume_Weighted_Moving_Average(bars: any[], inputs: Partial = {}): IndicatorResult { - const { len, src } = { ...defaultInputs, ...inputs }; - - // OHLCV Series - const open = new Series(bars, (bar) => bar.open); - const high = new Series(bars, (bar) => bar.high); - const low = new Series(bars, (bar) => bar.low); - const close = new Series(bars, (bar) => bar.close); - const volume = new Series(bars, (bar) => bar.volume ?? 0); - - // Calculated price sources - const hl2 = high.add(low).div(2); - const hlc3 = high.add(low).add(close).div(3); - const ohlc4 = open.add(high).add(low).add(close).div(4); - const hlcc4 = high.add(low).add(close).add(close).div(4); - - // Map source inputs to Series - const srcSeries = (() => { - switch (src) { - case "open": return open; - case "high": return high; - case "low": return low; - case "close": return close; - case "hl2": return hl2; - case "hlc3": return hlc3; - case "ohlc4": return ohlc4; - case "hlcc4": return hlcc4; - default: return close; - } - })(); - - // @version=6 - // VWMA calculation using ta.vwma - const ma = ta.vwma(srcSeries, len, volume); - - return { - metadata: { title: "Volume Weighted Moving Average", shorttitle: "VWMA", overlay: true }, - plots: { 'plot0': ma.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, - }; +function nz(value: number | null | undefined, replacement: number = 0): number { + return na(value) ? replacement : value as number; } // Plot configuration interface @@ -71,11 +29,74 @@ export interface InputConfig { options?: string[]; } +export interface IndicatorInputs { + len: number; + src: "open" | "high" | "low" | "close" | "hl2" | "hlc3" | "ohlc4" | "hlcc4"; + offset: number; +} + +const defaultInputs: IndicatorInputs = { + len: 20, + src: "close", + offset: 0, +}; + +export function Volume_Weighted_Moving_Average(bars: any[], inputs: Partial = {}): IndicatorResult { + const { len, src, offset } = { ...defaultInputs, ...inputs }; + +// OHLCV Series +const open = new Series(bars, (bar) => bar.open); +const high = new Series(bars, (bar) => bar.high); +const low = new Series(bars, (bar) => bar.low); +const close = new Series(bars, (bar) => bar.close); +const volume = new Series(bars, (bar) => bar.volume ?? 0); + +// Calculated price sources +const hl2 = high.add(low).div(2); +const hlc3 = high.add(low).add(close).div(3); +const ohlc4 = open.add(high).add(low).add(close).div(4); +const hlcc4 = high.add(low).add(close).add(close).div(4); + +// Map source inputs to Series +const srcSeries = (() => { + switch (src) { + case "open": return open; + case "high": return high; + case "low": return low; + case "close": return close; + case "hl2": return hl2; + case "hlc3": return hlc3; + case "ohlc4": return ohlc4; + case "hlcc4": return hlcc4; + default: return close; + } +})(); + +// Time series +const year = new Series(bars, (bar) => new Date(bar.time).getFullYear()); +const month = new Series(bars, (bar) => new Date(bar.time).getMonth() + 1); +const dayofmonth = new Series(bars, (bar) => new Date(bar.time).getDate()); +const dayofweek = new Series(bars, (bar) => new Date(bar.time).getDay() + 1); +const hour = new Series(bars, (bar) => new Date(bar.time).getHours()); +const minute = new Series(bars, (bar) => new Date(bar.time).getMinutes()); + +// Bar index +const last_bar_index = bars.length - 1; + + // @version=6 + const ma = ta.vwma(srcSeries, len, volume); + + return { + metadata: { title: "Volume Weighted Moving Average", shorttitle: "VWMA", overlay: true }, + plots: { 'plot0': ma.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, + }; +} + // Additional exports for compatibility export const metadata = { title: "Volume Weighted Moving Average", shortTitle: "VWMA", overlay: true }; export { defaultInputs }; -export const inputConfig: InputConfig[] = [{ id: 'len', type: 'int', title: 'Length', defval: 20, min: 1 }, { id: 'src', type: 'source', title: 'Source', defval: "close", options: ['open', 'high', 'low', 'close', 'hl2', 'hlc3', 'ohlc4', 'hlcc4'] }]; +export const inputConfig: InputConfig[] = [{ id: 'len', type: 'int', title: 'Length', defval: 20, min: 1 }, { id: 'src', type: 'source', title: 'Source', defval: "close" }, { id: 'offset', type: 'int', title: 'Offset', defval: 0, min: -500, max: 500 }]; export const plotConfig: PlotConfig[] = [{ id: 'plot0', title: 'VWMA', color: '#2962FF', lineWidth: 2 }]; export const calculate = Volume_Weighted_Moving_Average; export { Volume_Weighted_Moving_Average as Volume_Weighted_Moving_AverageIndicator }; -export type Volume_Weighted_Moving_AverageInputs = IndicatorInputs; +export type Volume_Weighted_Moving_AverageInputs = IndicatorInputs; \ No newline at end of file diff --git a/indicators/wma/wma.ts b/indicators/wma/wma.ts index 6ebfdb0..92d5401 100644 --- a/indicators/wma/wma.ts +++ b/indicators/wma/wma.ts @@ -1,53 +1,12 @@ -import { Series, ta, type IndicatorResult } from '@deepentropy/oakscriptjs'; +import { Series, ta, taCore, math, array, type IndicatorResult } from '@deepentropy/oakscriptjs'; -export interface IndicatorInputs { - len: number; - src: "open" | "high" | "low" | "close" | "hl2" | "hlc3" | "ohlc4" | "hlcc4"; +// Helper functions +function na(value: number | null | undefined): boolean { + return value === null || value === undefined || Number.isNaN(value); } -const defaultInputs: IndicatorInputs = { - len: 9, - src: "close", -}; - -export function Moving_Average_Weighted(bars: any[], inputs: Partial = {}): IndicatorResult { - const { len, src } = { ...defaultInputs, ...inputs }; - - // OHLCV Series - const open = new Series(bars, (bar) => bar.open); - const high = new Series(bars, (bar) => bar.high); - const low = new Series(bars, (bar) => bar.low); - const close = new Series(bars, (bar) => bar.close); - - // Calculated price sources - const hl2 = high.add(low).div(2); - const hlc3 = high.add(low).add(close).div(3); - const ohlc4 = open.add(high).add(low).add(close).div(4); - const hlcc4 = high.add(low).add(close).add(close).div(4); - - // Map source inputs to Series - const srcSeries = (() => { - switch (src) { - case "open": return open; - case "high": return high; - case "low": return low; - case "close": return close; - case "hl2": return hl2; - case "hlc3": return hlc3; - case "ohlc4": return ohlc4; - case "hlcc4": return hlcc4; - default: return close; - } - })(); - - // @version=6 - // WMA calculation using ta.wma - const out = ta.wma(srcSeries, len); - - return { - metadata: { title: "Moving Average Weighted", shorttitle: "WMA", overlay: true }, - plots: { 'plot0': out.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, - }; +function nz(value: number | null | undefined, replacement: number = 0): number { + return na(value) ? replacement : value as number; } // Plot configuration interface @@ -70,11 +29,74 @@ export interface InputConfig { options?: string[]; } +export interface IndicatorInputs { + len: number; + src: "open" | "high" | "low" | "close" | "hl2" | "hlc3" | "ohlc4" | "hlcc4"; + offset: number; +} + +const defaultInputs: IndicatorInputs = { + len: 9, + src: "close", + offset: 0, +}; + +export function Moving_Average_Weighted(bars: any[], inputs: Partial = {}): IndicatorResult { + const { len, src, offset } = { ...defaultInputs, ...inputs }; + +// OHLCV Series +const open = new Series(bars, (bar) => bar.open); +const high = new Series(bars, (bar) => bar.high); +const low = new Series(bars, (bar) => bar.low); +const close = new Series(bars, (bar) => bar.close); +const volume = new Series(bars, (bar) => bar.volume ?? 0); + +// Calculated price sources +const hl2 = high.add(low).div(2); +const hlc3 = high.add(low).add(close).div(3); +const ohlc4 = open.add(high).add(low).add(close).div(4); +const hlcc4 = high.add(low).add(close).add(close).div(4); + +// Map source inputs to Series +const srcSeries = (() => { + switch (src) { + case "open": return open; + case "high": return high; + case "low": return low; + case "close": return close; + case "hl2": return hl2; + case "hlc3": return hlc3; + case "ohlc4": return ohlc4; + case "hlcc4": return hlcc4; + default: return close; + } +})(); + +// Time series +const year = new Series(bars, (bar) => new Date(bar.time).getFullYear()); +const month = new Series(bars, (bar) => new Date(bar.time).getMonth() + 1); +const dayofmonth = new Series(bars, (bar) => new Date(bar.time).getDate()); +const dayofweek = new Series(bars, (bar) => new Date(bar.time).getDay() + 1); +const hour = new Series(bars, (bar) => new Date(bar.time).getHours()); +const minute = new Series(bars, (bar) => new Date(bar.time).getMinutes()); + +// Bar index +const last_bar_index = bars.length - 1; + + // @version=6 + const out = ta.wma(srcSeries, len); + + return { + metadata: { title: "Moving Average Weighted", shorttitle: "WMA", overlay: true }, + plots: { 'plot0': out.toArray().map((v: number | undefined, i: number) => ({ time: bars[i]!.time, value: v ?? NaN })) }, + }; +} + // Additional exports for compatibility export const metadata = { title: "Moving Average Weighted", shortTitle: "WMA", overlay: true }; export { defaultInputs }; -export const inputConfig: InputConfig[] = [{ id: 'len', type: 'int', title: 'Length', defval: 9, min: 1 }, { id: 'src', type: 'source', title: 'Source', defval: "close", options: ['open', 'high', 'low', 'close', 'hl2', 'hlc3', 'ohlc4', 'hlcc4'] }]; +export const inputConfig: InputConfig[] = [{ id: 'len', type: 'int', title: 'Length', defval: 9, min: 1 }, { id: 'src', type: 'source', title: 'Source', defval: "close" }, { id: 'offset', type: 'int', title: 'Offset', defval: 0, min: -500, max: 500 }]; export const plotConfig: PlotConfig[] = [{ id: 'plot0', title: 'WMA', color: '#2962FF', lineWidth: 2 }]; export const calculate = Moving_Average_Weighted; export { Moving_Average_Weighted as Moving_Average_WeightedIndicator }; -export type Moving_Average_WeightedInputs = IndicatorInputs; +export type Moving_Average_WeightedInputs = IndicatorInputs; \ No newline at end of file