From 29be5ffa34f691d097bdbc2b259fa3433c19476c Mon Sep 17 00:00:00 2001 From: Steve Clarke <116336693+srcmayte@users.noreply.github.com> Date: Sun, 7 Apr 2024 14:25:01 +0100 Subject: [PATCH] Added market.base and market.quote facts (#4) --- README.md | 38 ++++++++++++++++++++++++++++ lib/facts/base.js | 5 +--- lib/facts/index.js | 3 ++- lib/facts/market/base.js | 30 ++++++++++++++++++++++ lib/facts/market/index.js | 4 +++ lib/facts/market/quote.js | 30 ++++++++++++++++++++++ lib/strategyEngine.js | 26 +++++++++++++++++-- test/facts/market/base.spec.js | 44 +++++++++++++++++++++++++++++++++ test/facts/market/quote.spec.js | 44 +++++++++++++++++++++++++++++++++ test/strategyEngine.spec.js | 12 +++++++++ 10 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 lib/facts/market/base.js create mode 100644 lib/facts/market/index.js create mode 100644 lib/facts/market/quote.js create mode 100644 test/facts/market/base.spec.js create mode 100644 test/facts/market/quote.spec.js diff --git a/README.md b/README.md index 7fb1fbf..2a94ce0 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,44 @@ Facts can be used in any of the following, recursively. # Built-in facts Strategy engine comes with some built in facts. +## Market base symbol: `market.base` +This fact provides the base symbol of `market`. + +**Example** +```json +{ + "fact": "market.base", + "params": { + "market": "BTC/USDT" + } +} +// Returns BTC +``` + +### Properties +Property | Default | Description +-------- | -------- | ----------- +`market` | | The market to get the base symbol from. + +## Market quote symbol: `market.quote` +This fact provides the quote symbol of `market`. + +**Example** +```json +{ + "fact": "market.quote", + "params": { + "market": "BTC/USDT" + } +} +// Returns USDT +``` + +### Properties +Property | Default | Description +-------- | -------- | ----------- +`market` | | The market to get the quote symbol from. + ## Exponential Moving Average: `ta.ema` This fact provides the EMA of `values` for a `period`. diff --git a/lib/facts/base.js b/lib/facts/base.js index f2f69dd..8db9f90 100644 --- a/lib/facts/base.js +++ b/lib/facts/base.js @@ -1,5 +1,3 @@ -const _ = require('lodash') - const { ValidationError } = require('../errors') const { validate } = require('../validator') @@ -20,8 +18,7 @@ class Fact { return {} } - value (originalParams = {}) { - const params = _.cloneDeep(originalParams) + value (params = {}) { this.#validateAndSanitize(params) return params diff --git a/lib/facts/index.js b/lib/facts/index.js index 3fb347d..5888626 100644 --- a/lib/facts/index.js +++ b/lib/facts/index.js @@ -1,3 +1,4 @@ module.exports = { - ta: require('./ta') + ta: require('./ta'), + market: require('./market') } diff --git a/lib/facts/market/base.js b/lib/facts/market/base.js new file mode 100644 index 0000000..d9f89fb --- /dev/null +++ b/lib/facts/market/base.js @@ -0,0 +1,30 @@ +const Fact = require('../base') + +class Base extends Fact { + get id () { + return 'market.base' + } + + get name () { + return 'Base symbol for market' + } + + get description () { + return 'Get the base symbol from a market symbol' + } + + get schema () { + return { + market: { type: 'market', required: true, description: 'The market symbol.' }, + $$strict: 'remove' + } + } + + async value (params = {}) { + params = super.value(params) + + return params.market.split('/')[0] + } +} + +module.exports = Base diff --git a/lib/facts/market/index.js b/lib/facts/market/index.js new file mode 100644 index 0000000..ac4eabf --- /dev/null +++ b/lib/facts/market/index.js @@ -0,0 +1,4 @@ +const base = require('./base') +const quote = require('./quote') + +module.exports = { base, quote } diff --git a/lib/facts/market/quote.js b/lib/facts/market/quote.js new file mode 100644 index 0000000..8d07b08 --- /dev/null +++ b/lib/facts/market/quote.js @@ -0,0 +1,30 @@ +const Fact = require('../base') + +class Quote extends Fact { + get id () { + return 'market.quote' + } + + get name () { + return 'Quote symbol for market' + } + + get description () { + return 'Get the quote symbol from a market symbol' + } + + get schema () { + return { + market: { type: 'market', required: true, description: 'The market symbol.' }, + $$strict: 'remove' + } + } + + async value (params = {}) { + params = super.value(params) + + return params.market.split('/')[1] + } +} + +module.exports = Quote diff --git a/lib/strategyEngine.js b/lib/strategyEngine.js index 65fa0da..60d2b73 100644 --- a/lib/strategyEngine.js +++ b/lib/strategyEngine.js @@ -24,8 +24,10 @@ class StrategyEngine { opts.facts.forEach(fact => this.addFact(fact)) - for (const name in facts.ta) { - const fact = new facts.ta[name]() + const factsArray = this.#factsToArray(facts) + + for (const factConstructor of factsArray) { + const fact = new factConstructor() /* eslint-disable-line new-cap */ if (!this.engine.facts.has(fact.id)) { this.addFact(fact) @@ -131,6 +133,26 @@ class StrategyEngine { return params } + #factsToArray (facts = {}) { + const array = [] + const keys = Object.keys(facts) + + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + const value = facts[key] + + if (typeof value === 'function') { + if (!value.toString().toLowerCase().includes('backtest')) { + array.push(value) + } + } else { + array.push(...this.#factsToArray(value)) + } + } + + return array + } + #validateAndSanitizeOpts (opts) { const valid = validate(opts, { rules: { type: 'array', items: 'object', optional: true, default: [], convert: false }, diff --git a/test/facts/market/base.spec.js b/test/facts/market/base.spec.js new file mode 100644 index 0000000..eb99ecf --- /dev/null +++ b/test/facts/market/base.spec.js @@ -0,0 +1,44 @@ +const { expect, chance } = require('../../helpers') +const Base = require('../../../lib/facts/market/base') + +describe('Fact market.base', () => { + describe('#value', () => { + const fact = new Base() + + describe('params', () => { + describe('market', () => { + it('is required', async () => { + let thrownErr = null + + try { + await fact.value() + } catch (err) { + thrownErr = err + } + + expect(thrownErr.type).to.eql('VALIDATION_ERROR') + expect(thrownErr.data[0].message).to.eql('market is required') + }) + + it('must be a string', async () => { + let thrownErr = null + + try { + await fact.value({ market: chance.bool() }) + } catch (err) { + thrownErr = err + } + + expect(thrownErr.type).to.eql('VALIDATION_ERROR') + expect(thrownErr.data[0].message).to.eql('market must be a string') + }) + }) + }) + + it('returns the base symbol', async () => { + const result = await fact.value({ market: 'BTC/USD' }) + + expect(result).to.eql('BTC') + }) + }) +}) diff --git a/test/facts/market/quote.spec.js b/test/facts/market/quote.spec.js new file mode 100644 index 0000000..5c9ac1f --- /dev/null +++ b/test/facts/market/quote.spec.js @@ -0,0 +1,44 @@ +const { expect, chance } = require('../../helpers') +const Quote = require('../../../lib/facts/market/quote') + +describe('Fact market.quote', () => { + describe('#value', () => { + const fact = new Quote() + + describe('params', () => { + describe('market', () => { + it('is required', async () => { + let thrownErr = null + + try { + await fact.value() + } catch (err) { + thrownErr = err + } + + expect(thrownErr.type).to.eql('VALIDATION_ERROR') + expect(thrownErr.data[0].message).to.eql('market is required') + }) + + it('must be a string', async () => { + let thrownErr = null + + try { + await fact.value({ market: chance.bool() }) + } catch (err) { + thrownErr = err + } + + expect(thrownErr.type).to.eql('VALIDATION_ERROR') + expect(thrownErr.data[0].message).to.eql('market must be a string') + }) + }) + }) + + it('returns the quote symbol', async () => { + const result = await fact.value({ market: 'BTC/USDT' }) + + expect(result).to.eql('USDT') + }) + }) +}) diff --git a/test/strategyEngine.spec.js b/test/strategyEngine.spec.js index 64da302..8a8cd78 100644 --- a/test/strategyEngine.spec.js +++ b/test/strategyEngine.spec.js @@ -170,6 +170,18 @@ describe('StrategyEngine', () => { await expect(engine.engine.facts.get('ta.sma').calculationMethod()).to.eventually.eql(true) }) + it('has market.base', () => { + const engine = new StrategyEngine() + + expect(engine.engine.facts.has('market.base')).to.eql(true) + }) + + it('has market.quote', () => { + const engine = new StrategyEngine() + + expect(engine.engine.facts.has('market.quote')).to.eql(true) + }) + it('has ta.sma', () => { const engine = new StrategyEngine()