Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ATR and expression fact plus cloning params #6

Merged
merged 1 commit into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/facts/base.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const _ = require('lodash')
const { ValidationError } = require('../errors')
const { validate } = require('../validator')

Expand All @@ -18,7 +19,9 @@ class Fact {
return {}
}

value (params = {}) {
value (originalParams = {}) {
const params = _.cloneDeep(originalParams)

this.#validateAndSanitize(params)

return params
Expand Down
33 changes: 33 additions & 0 deletions lib/facts/expression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const jsonata = require('jsonata')

const Fact = require('./base')

class Expression extends Fact {
get id () {
return 'expression'
}

get name () {
return 'Evaluate Expression'
}

get description () {
return 'Evaluate an expression against provided paramters.'
}

get schema () {
return {
expression: { type: 'string', required: true, description: 'The expression.' },
data: { type: 'object', optional: true, default: {}, description: 'The data to evaluate the expression against.' },
$$strict: true
}
}

async value (params = {}) {
params = super.value(params)

return jsonata(params.expression).evaluate(params.data)
}
}

module.exports = Expression
3 changes: 2 additions & 1 deletion lib/facts/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
ta: require('./ta'),
market: require('./market')
market: require('./market'),
expression: require('./expression')
}
57 changes: 57 additions & 0 deletions lib/facts/ta/atr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const _ = require('lodash')
const BigNumber = require('bignumber.js')
const techIndicators = require('technicalindicators')

const Fact = require('../base')

class ATR extends Fact {
get id () {
return 'ta.atr'
}

get name () {
return 'Average True Range (ATR)'
}

get description () {
return 'Calculate the Average True Range from values over period'
}

get schema () {
return {
values: {
type: 'array',
items: {
type: 'object',
props: {
high: { type: 'number', required: true, convert: true, description: 'The open value' },
low: { type: 'number', required: true, convert: true, description: 'The high value' },
close: { type: 'number', required: true, convert: true, description: 'The low value' }
},
strict: 'remove',
convert: true
},
optional: true,
default: [],
description: 'Values to apply indicator to.'
},
period: { type: 'number', optional: true, default: 14, min: 1, convert: true, description: 'The period to calculate.' },
$$strict: 'remove'
}
}

async value (params = {}) {
params = super.value(params)

const atr = techIndicators.ATR.calculate({
high: params.values.map(v => BigNumber(v.high).toNumber()),
low: params.values.map(v => BigNumber(v.low).toNumber()),
close: params.values.map(v => BigNumber(v.close).toNumber()),
period: params.period
})

return _.takeRight(atr, params.period)
}
}

module.exports = ATR
1 change: 1 addition & 0 deletions lib/facts/ta/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
ema: require('./ema'),
sma: require('./sma'),
atr: require('./atr'),
rsi: require('./rsi'),
crossUp: require('./crossUp'),
crossDown: require('./crossDown')
Expand Down
2 changes: 1 addition & 1 deletion lib/strategyEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class StrategyEngine {
this.#validateAndSanitizeOpts(opts)

this.engine = new Engine(undefined, {
pathResolver: (object, path) => jsonata(path).evaluate(object)
pathResolver: (data, path) => jsonata(path).evaluate(data)
})

opts.rules.forEach(rule => {
Expand Down
67 changes: 67 additions & 0 deletions test/facts/expression.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const { expect } = require('../helpers')
const Expression = require('../../lib/facts/expression')

describe('Fact expression', () => {
describe('#value', () => {
const fact = new Expression()

describe('params', () => {
const defaultParams = {
expression: 'a + b',
data: { a: 1, b: 2 }
}

describe('expression', () => {
it('is required', async () => {
let thrownErr = null

try {
await fact.value({ ...defaultParams, expression: undefined })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('expression is required')
})

it('must a string', async () => {
let thrownErr = null

try {
await fact.value({ ...defaultParams, expression: {} })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('expression must be a string')
})
})

describe('data', () => {
it('must be an object', async () => {
let thrownErr = null

try {
await fact.value({ ...defaultParams, data: 'invalid' })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('data must be an Object')
})
})
})

it('evaluates expression against provided data', async () => {
const result = await fact.value({
expression: 'a + b',
data: { a: 1, b: 2 }
})

expect(result).to.eql(3)
})
})
})
179 changes: 179 additions & 0 deletions test/facts/ta/atr.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const { expect } = require('../../helpers')
const ATR = require('../../../lib/facts/ta/atr')

describe('Fact ta.atr', () => {
describe('#value', () => {
const fact = new ATR()
const defaultParams = {
values: [
{ high: 48.70, low: 47.79, close: 48.16 },
{ high: 48.72, low: 48.14, close: 48.61 },
{ high: 48.90, low: 48.39, close: 48.75 },
{ high: 48.87, low: 48.37, close: 48.63 },
{ high: 48.82, low: 48.24, close: 48.74 },
{ high: 49.05, low: 48.64, close: 49.03 },
{ high: 49.20, low: 48.94, close: 49.07 },
{ high: 49.35, low: 48.86, close: 49.32 },
{ high: 49.92, low: 49.50, close: 49.91 },
{ high: 50.19, low: 49.87, close: 50.13 },
{ high: 50.12, low: 49.20, close: 49.53 },
{ high: 49.66, low: 48.90, close: 49.50 },
{ high: 49.88, low: 49.43, close: 49.75 },
{ high: 50.19, low: 49.73, close: 50.03 },
{ high: 50.36, low: 49.26, close: 50.31 }
],
period: 14
}

describe('params', () => {
describe('values', () => {
it('defaults to an empty array', async () => {
const result = await fact.value()

expect(result).to.eql([])
})

describe('props', () => {
describe('high', () => {
it('is required', async () => {
let thrownErr = null

try {
await fact.value({ ...defaultParams, values: [{ ...defaultParams.values[0], high: undefined }] })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('values[0].high is required')
})

it('must be a number', async () => {
let thrownErr = null

try {
await fact.value({ ...defaultParams, values: [{ ...defaultParams.values[0], high: {} }] })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('values[0].high must be a number')
})
})

describe('low', () => {
it('is required', async () => {
let thrownErr = null

try {
await fact.value({ ...defaultParams, values: [{ ...defaultParams.values[0], low: undefined }] })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('values[0].low is required')
})

it('must be a number', async () => {
let thrownErr = null

try {
await fact.value({ ...defaultParams, values: [{ ...defaultParams.values[0], low: {} }] })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('values[0].low must be a number')
})
})

describe('close', () => {
it('is required', async () => {
let thrownErr = null

try {
await fact.value({ ...defaultParams, values: [{ ...defaultParams.values[0], close: undefined }] })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('values[0].close is required')
})

it('must be a number', async () => {
let thrownErr = null

try {
await fact.value({ ...defaultParams, values: [{ ...defaultParams.values[0], close: {} }] })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('values[0].close must be a number')
})
})
})
})

describe('period', () => {
it('defaults to 14', async () => {
const result = await fact.value(defaultParams)

expect(result).to.eql([0.5678571428571431])
})

it('must be an number', async () => {
let thrownErr = null

try {
await fact.value({ period: 'ten' })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('period must be a number')
})

it('must be at least 1', async () => {
let thrownErr = null

try {
await fact.value({ period: 0 })
} catch (err) {
thrownErr = err
}

expect(thrownErr.type).to.eql('VALIDATION_ERROR')
expect(thrownErr.data[0].message).to.eql('period must be greater than or equal to 1')
})
})
})

it('returns the ATR', async () => {
const result = await fact.value(defaultParams)

expect(result).to.eql([0.5678571428571431])
})

it('only returns up to periods', async () => {
const result = await fact.value({
values: defaultParams.values,
period: 5
})

expect(result).to.eql([
0.5545580800000003,
0.5956464639999999,
0.5665171712000004,
0.5452137369600005,
0.6561709895680007
])
})
})
})
Loading