dsh-hyperliquid 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/store.js CHANGED
@@ -5,18 +5,22 @@
5
5
  * @module lib/store.js
6
6
  */
7
7
 
8
- import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, rmSync } from 'node:fs'
8
+ import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, rmSync, unlinkSync } from 'node:fs'
9
9
  import { dirname, join } from 'node:path'
10
10
  import { randomUUID, createHash } from 'node:crypto'
11
11
  import { canonicalMarketCoin } from './market-symbol.js'
12
+ import { normalizePaperCosts } from './workspace-settings.js'
12
13
 
13
14
  const PAPER_START_USD = 100_000
14
15
  const WORKSPACE_DEFAULTS = Object.freeze({
15
- interval: '15m', bookGrouping: 'full', chartTab: 'chart', tapeTab: 'book', bottomTab: 'positions', indicators: ['volume'],
16
- priceScale: 'normal', chartRanges: {}, chartLayers: { orders: true, fills: true, drawings: true },
16
+ interval: '15m', bookGrouping: 'full', chartTab: 'chart', tapeTab: 'book', bottomTab: 'positions', indicators: ['volume'], maLines: [],
17
+ priceScale: 'normal', chartType: 'candles', chartRanges: {}, chartLayers: { orders: true, fills: true, drawings: true },
17
18
  layout: { sessionsWidth: 232, chatWidth: 480, sessionsCollapsed: false, deskCollapsed: false, chatCollapsed: false },
18
19
  })
19
20
 
21
+ /** Drawings per chart (session + coin, all intervals); the pan budget is verified at this size. */
22
+ export const MAX_CHART_MARKS = 100
23
+
20
24
  export class HyperStore {
21
25
  constructor(dataDir, paperNetwork = 'testnet') {
22
26
  this.dataDir = dataDir
@@ -26,6 +30,7 @@ export class HyperStore {
26
30
  this.liveOrdersFile = join(dataDir, 'live-orders.json')
27
31
  this.liveDecisionsFile = join(dataDir, 'live-decisions.json')
28
32
  this.chartMarksFile = join(dataDir, 'chart-marks.json')
33
+ this.riskSettingsFile = join(dataDir, 'risk-settings.json')
29
34
  this.events = [] // UI event ring
30
35
  this.eventSeq = 0
31
36
  this.sessions = this.#readJson(this.sessionsFile, { sessions: {} })
@@ -109,6 +114,14 @@ export class HyperStore {
109
114
  catch { throw new Error(`会话文件损坏:${file}`) }
110
115
  }
111
116
 
117
+ getRiskSettings() { return this.#readArtifact(this.riskSettingsFile, null) }
118
+
119
+ saveRiskSettings(settings) { this.#writeJson(this.riskSettingsFile, settings) }
120
+
121
+ clearRiskSettings() {
122
+ if (existsSync(this.riskSettingsFile)) unlinkSync(this.riskSettingsFile)
123
+ }
124
+
112
125
  #migrateChartMarks() {
113
126
  if (!existsSync(this.chartMarksFile)) return
114
127
  const legacy = this.#readArtifact(this.chartMarksFile, [])
@@ -211,29 +224,95 @@ export class HyperStore {
211
224
  return this.sessions.sessions[sessionId] ?? null
212
225
  }
213
226
 
227
+ /**
228
+ * Drawings synced to every trading session of a coin (TradingView's "sync drawings"):
229
+ * one shared file; each drawing keeps the session that created it as `sessionId`.
230
+ */
231
+ #shared() {
232
+ if (!this.sharedMarks) {
233
+ const marks = this.#readArtifact(join(this.dataDir, 'shared-chart-marks.json'), [])
234
+ if (!Array.isArray(marks)) throw new Error('共享图表标注文件损坏')
235
+ this.sharedMarks = marks
236
+ }
237
+ return this.sharedMarks
238
+ }
239
+
240
+ #saveShared() { this.#writeJson(join(this.dataDir, 'shared-chart-marks.json'), this.#shared()) }
241
+
242
+ #saveSession(sessionId) { this.#writeJson(this.#sessionFile(sessionId, 'chart-marks.json'), this.#marks(sessionId)) }
243
+
244
+ #notify(mark, sessionId) {
245
+ this.recordEvent('chart.mark', mark.shared
246
+ ? { sessionId: null, shared: true, coin: mark.coin, interval: mark.interval, id: mark.id }
247
+ : { sessionId, coin: mark.coin, interval: mark.interval, id: mark.id })
248
+ }
249
+
214
250
  listChartMarks(sessionId, coin, interval) {
215
- return this.#marks(sessionId).filter((mark) => mark.coin === coin && mark.interval === interval)
251
+ const match = (mark) => mark.coin === coin && (interval == null || mark.interval === interval)
252
+ return [...this.#marks(sessionId).filter(match), ...this.#shared().filter(match)]
253
+ }
254
+
255
+ /** Every drawing that lives in a session file or the shared file (for host-side watchers). */
256
+ allChartMarks() {
257
+ const sessions = Object.keys(this.sessions?.sessions ?? {})
258
+ return [...sessions.flatMap((id) => { try { return this.#marks(id) } catch { return [] } }), ...this.#shared()]
216
259
  }
217
260
 
218
261
  addChartMark(mark) {
219
- const related = this.listChartMarks(mark.sessionId, mark.coin, mark.interval)
220
- if (related.length >= 30) throw new Error('当前图表最多保留 30 条标注,请先删除旧线')
221
- const marks = this.#marks(mark.sessionId)
262
+ // The chart shows a coin's drawings from every interval, so the cap counts them all.
263
+ const related = this.listChartMarks(mark.sessionId, mark.coin)
264
+ if (related.length >= MAX_CHART_MARKS) throw new Error(`当前图表最多保留 ${MAX_CHART_MARKS} 条标注,请先删除旧线`)
265
+ const marks = mark.shared ? this.#shared() : this.#marks(mark.sessionId)
222
266
  if (marks.length >= 300) throw new Error('当前会话最多保留 300 条图表标注,请先删除旧线')
223
267
  const saved = { ...mark, id: randomUUID(), createdAt: Date.now() }
224
268
  marks.push(saved)
225
- this.#writeJson(this.#sessionFile(mark.sessionId, 'chart-marks.json'), marks)
226
- this.recordEvent('chart.mark', { sessionId: mark.sessionId, coin: mark.coin, interval: mark.interval, id: saved.id })
269
+ if (mark.shared) this.#saveShared(); else this.#saveSession(mark.sessionId)
270
+ this.#notify(saved, mark.sessionId)
227
271
  return saved
228
272
  }
229
273
 
274
+ getChartMark(sessionId, id) {
275
+ const mark = this.#marks(sessionId).find((row) => row.id === id) ?? this.#shared().find((row) => row.id === id)
276
+ if (!mark) throw new Error('图表标注不存在或不属于当前会话')
277
+ return mark
278
+ }
279
+
280
+ /** Patch one drawing; toggling `shared` moves it between the session file and the shared file. */
281
+ updateChartMark(sessionId, id, fields) {
282
+ const own = this.#marks(sessionId), shared = this.#shared()
283
+ const inShared = !own.some((row) => row.id === id)
284
+ const list = inShared ? shared : own
285
+ const index = list.findIndex((row) => row.id === id)
286
+ if (index < 0) throw new Error('图表标注不存在或不属于当前会话')
287
+ const next = { ...list[index], ...fields, updatedAt: Date.now() }
288
+ // Cleared fields (undefined) are removed, so stored and returned drawings stay lossless JSON.
289
+ for (const [key, value] of Object.entries(next)) if (value === undefined) delete next[key]
290
+ if (!next.shared) delete next.shared
291
+ const moving = Boolean(next.shared) !== inShared
292
+ if (moving) {
293
+ list.splice(index, 1)
294
+ if (next.shared) shared.push(next); else own.push({ ...next, sessionId })
295
+ this.#saveShared(); this.#saveSession(sessionId)
296
+ // Both the old and the new owners must reload.
297
+ this.recordEvent('chart.mark', { sessionId: null, shared: true, coin: next.coin, interval: next.interval, id })
298
+ this.recordEvent('chart.mark', { sessionId, coin: next.coin, interval: next.interval, id })
299
+ return next.shared ? next : { ...next, sessionId }
300
+ }
301
+ list[index] = next
302
+ if (inShared) this.#saveShared(); else this.#saveSession(sessionId)
303
+ this.#notify(next, sessionId)
304
+ return next
305
+ }
306
+
230
307
  removeChartMark(sessionId, id) {
231
- const marks = this.#marks(sessionId)
232
- const mark = marks.find((row) => row.id === id)
308
+ const own = this.#marks(sessionId)
309
+ const inShared = !own.some((row) => row.id === id)
310
+ const list = inShared ? this.#shared() : own
311
+ const mark = list.find((row) => row.id === id)
233
312
  if (!mark) throw new Error('图表标注不存在或不属于当前会话')
234
- this.chartMarksBySession.set(sessionId, marks.filter((row) => row !== mark))
235
- this.#writeJson(this.#sessionFile(sessionId, 'chart-marks.json'), this.#marks(sessionId))
236
- this.recordEvent('chart.mark', { sessionId, coin: mark.coin, interval: mark.interval, id })
313
+ list.splice(list.indexOf(mark), 1)
314
+ if (inShared) this.#saveShared(); else this.#saveSession(sessionId)
315
+ this.#notify(mark, sessionId)
237
316
  return mark
238
317
  }
239
318
 
@@ -254,16 +333,39 @@ export class HyperStore {
254
333
  return JSON.parse(JSON.stringify(this.paper))
255
334
  }
256
335
 
336
+ paperCosts() {
337
+ const file = join(this.dataDir, 'paper-costs.json')
338
+ return normalizePaperCosts(existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : undefined)
339
+ }
340
+ setPaperCosts(input) {
341
+ const costs = normalizePaperCosts(input)
342
+ this.#writeJson(join(this.dataDir, 'paper-costs.json'), costs)
343
+ return costs
344
+ }
345
+ indicatorTemplates() { return this.#readJson(join(this.dataDir, 'indicator-templates.json'), []) }
346
+ saveIndicatorTemplate(template) {
347
+ const rows = this.indicatorTemplates()
348
+ if (!rows.some((row) => row.id === template.id) && rows.length >= 20) throw Error('最多保存 20 个指标模板')
349
+ const next = { ...template, id: template.id || randomUUID(), updatedAt: Date.now() }
350
+ this.#writeJson(join(this.dataDir, 'indicator-templates.json'), [...rows.filter((row) => row.id !== next.id), next])
351
+ return next
352
+ }
353
+ removeIndicatorTemplate(id) {
354
+ this.#writeJson(join(this.dataDir, 'indicator-templates.json'), this.indicatorTemplates().filter((row) => row.id !== id))
355
+ }
356
+
257
357
  /**
258
358
  * Fill a paper order against the given mid with a simple slippage model.
259
359
  * @returns the fill record
260
360
  */
261
- paperFill({ coin, isBuy, sz, px, time = Date.now(), reason = 'order' }) {
361
+ paperFill({ coin, isBuy, sz, px, time = Date.now(), reason = 'order', sessionId = null, feeBps = 0, referencePx = null, slippageBps = null }) {
262
362
  const szNum = Number(sz)
263
363
  const pxNum = Number(px)
264
364
  if (!Number.isFinite(szNum) || szNum <= 0) throw new Error('paper: 无效数量')
265
365
  if (!Number.isFinite(pxNum) || pxNum <= 0) throw new Error('paper: 无效价格')
266
366
  const notional = szNum * pxNum
367
+ if (!Number.isFinite(feeBps) || feeBps < 0 || feeBps > 100) throw Error('paper: 无效手续费')
368
+ const fee = notional * feeBps / 10_000
267
369
  const pos = this.paper.positions[coin] ?? { size: 0, entryPx: pxNum, leverage: this.paper.leverageByCoin?.[coin] ?? 1 }
268
370
  const signed = isBuy ? szNum : -szNum
269
371
  const prevSize = pos.size
@@ -286,10 +388,13 @@ export class HyperStore {
286
388
  pos.size = newSize
287
389
  this.paper.positions[coin] = pos
288
390
  }
289
- this.paper.usd += realized // realized PnL settles into cash; margin tracked conceptually
290
- const fill = { coin, isBuy, sz: szNum, px: pxNum, notional, realized, time, reason }
391
+ this.paper.usd += realized - fee
392
+ // sessionId: the trading conversation whose decision produced this fill.
393
+ const fill = { oid: `paper-${randomUUID()}`, coin, isBuy, sz: szNum, px: pxNum, notional, realized, closedPnl: realized,
394
+ fee, feeToken: 'USDC', netPnl: realized - fee, feeBps, referencePx, slippageBps,
395
+ closedSize: Math.min(Math.abs(prevSize), Math.sign(prevSize) !== Math.sign(signed) ? szNum : 0), startPosition: prevSize, time, reason, sessionId }
291
396
  this.paper.fills.unshift(fill)
292
- if (this.paper.fills.length > 200) this.paper.fills.length = 200
397
+ if (this.paper.fills.length > 5000) this.paper.fills.length = 5000
293
398
  this.#writeJson(this.paperFile, this.paper)
294
399
  this.recordEvent('paper.fill', fill)
295
400
  return fill
@@ -298,7 +403,7 @@ export class HyperStore {
298
403
  paperOrderRecord(order) {
299
404
  const row = { ...order, time: Date.now(), status: 'filled' }
300
405
  this.paper.orders.unshift(row)
301
- if (this.paper.orders.length > 200) this.paper.orders.length = 200
406
+ if (this.paper.orders.length > 5000) this.paper.orders.length = 5000
302
407
  this.#writeJson(this.paperFile, this.paper)
303
408
  this.recordEvent('paper.order', row)
304
409
  }
@@ -0,0 +1,13 @@
1
+ export interface IndicatorSettings {
2
+ rsi: { period: number; lower: number; upper: number }
3
+ macd: { fast: number; slow: number; signal: number }
4
+ bollinger: { period: number; multiplier: number }
5
+ stochastic: { period: number; smoothK: number; smoothD: number; lower: number; upper: number }
6
+ adx: { period: number; smoothing: number }
7
+ }
8
+ export type StudyKey = 'sma20' | 'sma60' | 'ema20' | 'ema50' | 'basis' | 'upper' | 'lower' | 'rsi14' | 'macd' | 'signal' | 'histogram' | 'atr14' | 'vwap' | 'stochK' | 'stochD' | 'adx' | 'plusDI' | 'minusDI' | 'obv'
9
+ export declare const INDICATOR_IDS: string[]
10
+ export declare const DEFAULT_INDICATOR_SETTINGS: IndicatorSettings
11
+ export declare function normalizeIndicatorSettings(input?: unknown): IndicatorSettings
12
+ export declare function computeStudies(candles: readonly {t: number; o: number; h: number; l: number; c: number; v: number}[], input?: IndicatorSettings): Record<StudyKey, Array<{ time: number; value: number }>>
13
+ export declare function studyDefinitions(input?: IndicatorSettings): Array<[string, StudyKey, string]>
package/lib/studies.js ADDED
@@ -0,0 +1,157 @@
1
+ /** Shared chart/agent study definitions. All values are derived from the supplied candle window. */
2
+ export const INDICATOR_IDS = ['volume', 'sma20', 'sma60', 'ema20', 'ema50', 'bollinger20', 'rsi14', 'macd', 'atr14', 'vwap', 'stochastic', 'adx', 'obv']
3
+ export const DEFAULT_INDICATOR_SETTINGS = {
4
+ rsi: { period: 14, lower: 30, upper: 70 }, macd: { fast: 12, slow: 26, signal: 9 },
5
+ bollinger: { period: 20, multiplier: 2 }, stochastic: { period: 14, smoothK: 3, smoothD: 3, lower: 20, upper: 80 },
6
+ adx: { period: 14, smoothing: 14 },
7
+ }
8
+ export function normalizeIndicatorSettings(input = {}) {
9
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some((key) => !Object.hasOwn(DEFAULT_INDICATOR_SETTINGS, key))) throw Error('指标参数无效')
10
+ const result = {}
11
+ for (const [section, defaults] of Object.entries(DEFAULT_INDICATOR_SETTINGS)) {
12
+ const supplied = input[section] ?? {}
13
+ if (typeof supplied !== 'object' || Array.isArray(supplied) || Object.keys(supplied).some((key) => !Object.hasOwn(defaults, key))) throw Error('指标参数字段无效')
14
+ const values = { ...defaults, ...supplied }
15
+ for (const [key, value] of Object.entries(values)) {
16
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw Error(`${section}.${key} 必须为数值`)
17
+ if (key === 'multiplier') { if (value <= 0 || value > 10) throw Error('布林带倍数必须大于 0 且不超过 10') }
18
+ else if (key === 'lower' || key === 'upper') { if (value < 0 || value > 100) throw Error('指标阈值必须为 0–100') }
19
+ else if (!Number.isInteger(value) || value < 1 || value > 500) throw Error('指标周期必须为 1–500 的整数')
20
+ }
21
+ if ('lower' in values && values.lower >= values.upper) throw Error('指标下阈值必须小于上阈值')
22
+ result[section] = values
23
+ }
24
+ if (result.macd.fast >= result.macd.slow) throw Error('MACD 快线周期必须小于慢线周期')
25
+ return result
26
+ }
27
+ function nullableSma(values, period) {
28
+ return values.map((_, i) => {
29
+ if (i < period - 1) return null
30
+ const window = values.slice(i - period + 1, i + 1)
31
+ return window.some((value) => value == null) ? null : window.reduce((sum, value) => sum + value, 0) / period
32
+ })
33
+ }
34
+ function rma(values, period) {
35
+ let prior = null, seed = []
36
+ return values.map((value) => {
37
+ if (value == null) return null
38
+ if (prior == null) { seed.push(value); if (seed.length < period) return null; prior = seed.reduce((sum, item) => sum + item, 0) / period }
39
+ else prior = (prior * (period - 1) + value) / period
40
+ return prior
41
+ })
42
+ }
43
+ export function studyDefinitions(input) {
44
+ const p = normalizeIndicatorSettings(input)
45
+ return [
46
+ ['sma20', 'sma20', 'SMA20'], ['sma60', 'sma60', 'MA60'], ['ema20', 'ema20', 'EMA20'], ['ema50', 'ema50', 'EMA50'],
47
+ ['bollinger20', 'basis', `BOLL ${p.bollinger.period} × ${p.bollinger.multiplier}`], ['bollinger20', 'upper', 'BOLL upper'], ['bollinger20', 'lower', 'BOLL lower'],
48
+ ['rsi14', 'rsi14', `RSI${p.rsi.period}`], ['macd', 'macd', p.macd.fast === 12 && p.macd.slow === 26 && p.macd.signal === 9 ? 'MACD' : `MACD ${p.macd.fast}/${p.macd.slow}/${p.macd.signal}`], ['macd', 'signal', 'Signal'], ['macd', 'histogram', 'Hist'],
49
+ ['atr14', 'atr14', 'ATR14'], ['vwap', 'vwap', 'VWAP (UTC HLC3)'], ['stochastic', 'stochK', `Stoch %K ${p.stochastic.period}/${p.stochastic.smoothK}`], ['stochastic', 'stochD', `Stoch %D ${p.stochastic.smoothD}`],
50
+ ['adx', 'adx', `ADX ${p.adx.period}/${p.adx.smoothing}`], ['adx', 'plusDI', '+DI'], ['adx', 'minusDI', '-DI'], ['obv', 'obv', 'OBV'],
51
+ ]
52
+ }
53
+ function points(rows, values) {
54
+ return values.flatMap((value, index) => value == null || !Number.isFinite(value)
55
+ ? [] : [{ time: Math.floor(rows[index].t / 1000), value }])
56
+ }
57
+
58
+ function sma(values, period) {
59
+ let sum = 0
60
+ return values.map((value, index) => {
61
+ sum += value
62
+ if (index >= period) sum -= values[index - period]
63
+ return index >= period - 1 ? sum / period : null
64
+ })
65
+ }
66
+
67
+ function ema(values, period) {
68
+ const result = Array(values.length).fill(null)
69
+ const alpha = 2 / (period + 1)
70
+ let seed = []
71
+ let prior = null
72
+ for (let i = 0; i < values.length; i += 1) {
73
+ const value = values[i]
74
+ if (value == null) continue
75
+ if (prior == null) {
76
+ seed.push(value)
77
+ if (seed.length < period) continue
78
+ prior = seed.reduce((total, current) => total + current, 0) / period
79
+ } else prior = value * alpha + prior * (1 - alpha)
80
+ result[i] = prior
81
+ }
82
+ return result
83
+ }
84
+
85
+ export function computeStudies(candles, input) {
86
+ const settings = normalizeIndicatorSettings(input)
87
+ const rows = candles.filter((row) => Number.isFinite(row.c) && row.c > 0 && Number.isFinite(row.t))
88
+ const closes = rows.map((row) => row.c)
89
+ const mean = sma(closes, settings.bollinger.period)
90
+ const ema12 = ema(closes, settings.macd.fast)
91
+ const ema26 = ema(closes, settings.macd.slow)
92
+ const macd = ema12.map((value, index) => value == null || ema26[index] == null ? null : value - ema26[index])
93
+ const signal = ema(macd, settings.macd.signal)
94
+ const deviation = mean.map((value, index) => value == null ? null : Math.sqrt(
95
+ closes.slice(index - settings.bollinger.period + 1, index + 1).reduce((total, close) => total + (close - value) ** 2, 0) / settings.bollinger.period,
96
+ ))
97
+ const rsi = Array(rows.length).fill(null)
98
+ let gain = 0; let loss = 0
99
+ for (let i = 1; i < rows.length; i += 1) {
100
+ const delta = closes[i] - closes[i - 1]
101
+ const up = Math.max(delta, 0); const down = Math.max(-delta, 0)
102
+ if (i <= settings.rsi.period) { gain += up; loss += down }
103
+ else { gain = (gain * (settings.rsi.period - 1) + up) / settings.rsi.period; loss = (loss * (settings.rsi.period - 1) + down) / settings.rsi.period }
104
+ if (i === settings.rsi.period) { gain /= settings.rsi.period; loss /= settings.rsi.period }
105
+ if (i >= settings.rsi.period) rsi[i] = loss === 0 ? gain === 0 ? 50 : 100 : 100 - 100 / (1 + gain / loss)
106
+ }
107
+ const point = (values) => points(rows, values)
108
+ const atr = Array(rows.length).fill(null)
109
+ let rangeSum = 0
110
+ for (let i = 1; i < rows.length; i += 1) {
111
+ const range = Math.max(rows[i].h - rows[i].l, Math.abs(rows[i].h - rows[i - 1].c), Math.abs(rows[i].l - rows[i - 1].c))
112
+ if (i <= 14) rangeSum += range
113
+ if (i === 14) atr[i] = rangeSum / 14
114
+ if (i > 14) atr[i] = (atr[i - 1] * 13 + range) / 14
115
+ }
116
+ // UTC sessions, HLC3 weighted by base volume. A partial first day is omitted.
117
+ let day = -1, weighted = 0, volume = 0, complete = false
118
+ const vwap = rows.map((row) => {
119
+ const nextDay = Math.floor(row.t / 86_400_000)
120
+ if (nextDay !== day) {
121
+ // After the first observed day, the exchange can legitimately skip empty
122
+ // midnight bars (e.g. closed HIP-3 markets); the next available bar starts it.
123
+ complete = day >= 0 || row.t % 86_400_000 === 0
124
+ day = nextDay; weighted = 0; volume = 0
125
+ }
126
+ if (!Number.isFinite(row.v) || row.v < 0) return null
127
+ weighted += (row.h + row.l + row.c) / 3 * row.v
128
+ volume += row.v
129
+ return complete && volume > 0 ? weighted / volume : null
130
+ })
131
+ const rawK = rows.map((row, index) => {
132
+ if (index < settings.stochastic.period - 1) return null
133
+ const window = rows.slice(index - settings.stochastic.period + 1, index + 1)
134
+ const low = Math.min(...window.map((bar) => bar.l)), high = Math.max(...window.map((bar) => bar.h))
135
+ return high === low ? null : 100 * (row.c - low) / (high - low)
136
+ })
137
+ const stochK = nullableSma(rawK, settings.stochastic.smoothK)
138
+ const stochD = nullableSma(stochK, settings.stochastic.smoothD)
139
+ const tr = rows.map((row, i) => i === 0 ? null : Math.max(row.h - row.l, Math.abs(row.h - rows[i - 1].c), Math.abs(row.l - rows[i - 1].c)))
140
+ const plus = rows.map((row, i) => { if (!i) return null; const up = row.h - rows[i - 1].h, down = rows[i - 1].l - row.l; return up > down && up > 0 ? up : 0 })
141
+ const minus = rows.map((row, i) => { if (!i) return null; const up = row.h - rows[i - 1].h, down = rows[i - 1].l - row.l; return down > up && down > 0 ? down : 0 })
142
+ const atrDm = rma(tr, settings.adx.period), plusRma = rma(plus, settings.adx.period), minusRma = rma(minus, settings.adx.period)
143
+ const di = (values) => values.map((value, i) => value == null || atrDm[i] == null ? null : atrDm[i] === 0 ? 0 : 100 * value / atrDm[i])
144
+ const plusDI = di(plusRma), minusDI = di(minusRma)
145
+ const dx = plusDI.map((value, i) => value == null || minusDI[i] == null ? null : value + minusDI[i] === 0 ? 0 : 100 * Math.abs(value - minusDI[i]) / (value + minusDI[i]))
146
+ let balance = 0
147
+ const obv = rows.map((row, i) => { if (i) balance += Math.sign(row.c - rows[i - 1].c) * row.v; return balance })
148
+ return {
149
+ stochK: point(stochK), stochD: point(stochD), adx: point(rma(dx, settings.adx.smoothing)), plusDI: point(plusDI), minusDI: point(minusDI), obv: point(obv),
150
+ sma20: point(sma(closes, 20)), basis: point(mean), sma60: point(sma(closes, 60)), ema20: point(ema(closes, 20)), ema50: point(ema(closes, 50)),
151
+ upper: point(mean.map((value, index) => value == null ? null : value + settings.bollinger.multiplier * deviation[index])),
152
+ lower: point(mean.map((value, index) => value == null ? null : value - settings.bollinger.multiplier * deviation[index])),
153
+ rsi14: point(rsi), macd: point(macd), signal: point(signal),
154
+ histogram: point(macd.map((value, index) => value == null || signal[index] == null ? null : value - signal[index])),
155
+ atr14: point(atr), vwap: point(vwap),
156
+ }
157
+ }