diff --git a/lib/calculators/ValuationCalculator.js b/lib/calculators/ValuationCalculator.js new file mode 100644 index 0000000..66e4ec8 --- /dev/null +++ b/lib/calculators/ValuationCalculator.js @@ -0,0 +1,84 @@ +const Decimal = require('@barchart/common-js/lang/Decimal'), + is = require('@barchart/common-js/lang/is'); + +const InstrumentType = require('./../data/InstrumentType'); + +module.exports = (() => { + 'use strict'; + + class ValuationCalculator { + constructor() { + + } + + static calculate(instrument, price, quantity) { + let priceToUse = null; + + if (is.number(price)) { + priceToUse = new Decimal(price); + } else if (price instanceof Decimal) { + priceToUse = price; + } + + if (priceToUse === null) { + return null; + } + + const calculator = calculators.get(instrument.type); + + return calculator(instrument, priceToUse, quantity); + } + + toString() { + return `[ValuationCalculator]`; + } + } + + function calculateForCash(instrument, price, quantity) { + return new Decimal(quantity); + } + + function calculateForEquity(instrument, price, quantity) { + return price.multiply(quantity); + } + + function calculateForEquityOption(instrument, price, quantity) { + const priceMultiplier = instrument.option.multiplier; + + return price.multiply(priceMultiplier).multiply(quantity); + } + + function calculateForFund(instrument, price, quantity) { + return price.multiply(quantity); + } + + function calculateForFuture(instrument, price, quantity) { + const minimumTick = instrument.future.tick; + const minimumTickValue = instrument.future.value; + + return price.divide(minimumTick).multiply(minimumTickValue).multiply(quantity); + } + + function calculateForFutureOption(instrument, price, quantity) { + const minimumTick = instrument.option.tick; + const minimumTickValue = instrument.option.value; + + return price.divide(minimumTick).multiply(minimumTickValue).multiply(quantity); + } + + function calculateForOther(instrument, price, quantity) { + return price.multiply(quantity); + } + + const calculators = new Map(); + + calculators.set(InstrumentType.CASH, calculateForCash); + calculators.set(InstrumentType.EQUITY, calculateForEquity); + calculators.set(InstrumentType.EQUITY_OPTION, calculateForEquityOption); + calculators.set(InstrumentType.FUND, calculateForFund); + calculators.set(InstrumentType.FUTURE, calculateForFuture); + calculators.set(InstrumentType.FUTURE_OPTION, calculateForFutureOption); + calculators.set(InstrumentType.OTHER, calculateForOther); + + return ValuationCalculator; +})(); diff --git a/lib/processing/PositionItem.js b/lib/processing/PositionItem.js index 88bef81..5b1cc35 100644 --- a/lib/processing/PositionItem.js +++ b/lib/processing/PositionItem.js @@ -8,6 +8,8 @@ const assert = require('@barchart/common-js/lang/assert'), const InstrumentType = require('./../data/InstrumentType'), PositionDirection = require('./../data/PositionDirection'); +const ValuationCalculator = require('./../calculators/ValuationCalculator'); + module.exports = (() => { 'use strict'; @@ -514,18 +516,8 @@ module.exports = (() => { market = snapshot.value; } else if (position.instrument.type === InstrumentType.CASH) { market = snapshot.open; - } else if (position.instrument.type === InstrumentType.FUTURE) { - market = getFuturesValue(position.instrument, snapshot.open, price) || snapshot.value; - } else if (position.instrument.type === InstrumentType.FUTURE_OPTION) { - market = getFuturesOptionValue(position.instrument, snapshot.open, price) || snapshot.value; - } else if (position.instrument.type === InstrumentType.EQUITY_OPTION) { - market = getEquityOptionValue(position.instrument, snapshot.open, price) || snapshot.value; } else { - if (price) { - market = snapshot.open.multiply(price); - } else { - market = snapshot.value; - } + market = ValuationCalculator.calculate(position.instrument, price, snapshot.open) || snapshot.value; } let marketChange; @@ -555,17 +547,7 @@ module.exports = (() => { let unrealizedTodayChange; if (data.previousPrice && price) { - let unrealizedTodayBase; - - if (position.instrument.type === InstrumentType.FUTURE) { - unrealizedTodayBase = getFuturesValue(position.instrument, snapshot.open, data.previousPrice); - } else if (position.instrument.type === InstrumentType.FUTURE_OPTION) { - unrealizedTodayBase = getFuturesOptionValue(position.instrument, snapshot.open, data.previousPrice); - } else if (position.instrument.type === InstrumentType.EQUITY_OPTION) { - unrealizedTodayBase = getEquityOptionValue(position.instrument, snapshot.open, data.previousPrice); - } else { - unrealizedTodayBase = snapshot.open.multiply(data.previousPrice); - } + const unrealizedTodayBase = ValuationCalculator.calculate(position.instrument, data.previousPrice, snapshot.open); unrealizedToday = market.subtract(unrealizedTodayBase); @@ -599,17 +581,7 @@ module.exports = (() => { } if (priceToUse !== null) { - let unrealized; - - if (position.instrument.type === InstrumentType.FUTURE) { - unrealized = getFuturesValue(position.instrument, currentSummary.end.open, priceToUse).add(currentSummary.end.basis); - } else if (position.instrument.type === InstrumentType.FUTURE_OPTION) { - unrealized = getFuturesOptionValue(position.instrument, currentSummary.end.open, priceToUse).add(currentSummary.end.basis); - } else if (position.instrument.type === InstrumentType.EQUITY_OPTION) { - unrealized = getEquityOptionValue(position.instrument, currentSummary.end.open, priceToUse).add(currentSummary.end.basis); - } else { - unrealized = currentSummary.end.open.multiply(priceToUse).add(currentSummary.end.basis); - } + const unrealized = ValuationCalculator.calculate(position.instrument, priceToUse, currentSummary.end.open).add(currentSummary.end.basis); let unrealizedChange; @@ -694,15 +666,7 @@ module.exports = (() => { let endValue; if (overridePrice) { - if (type === InstrumentType.FUTURE) { - endValue = getFuturesValue(instrument, currentSummary.end.open, overridePrice); - } else if (type === InstrumentType.FUTURE_OPTION) { - endValue = getFuturesOptionValue(instrument, currentSummary.end.open, overridePrice); - } else if (type === InstrumentType.EQUITY_OPTION) { - endValue = getEquityOptionValue(instrument, currentSummary.end.open, overridePrice); - } else { - endValue = currentSummary.end.open.multiply(overridePrice); - } + endValue = ValuationCalculator.calculate(instrument, overridePrice, currentSummary.end.open); } else { endValue = currentSummary.end.value; } @@ -823,44 +787,5 @@ module.exports = (() => { return snapshot; } - function getFuturesValue(instrument, contracts, price) { - if (price || price === 0) { - const priceDecimal = new Decimal(price); - - const minimumTick = instrument.future.tick; - const minimumTickValue = instrument.future.value; - - return priceDecimal.divide(minimumTick).multiply(minimumTickValue).multiply(contracts); - } else { - return null; - } - } - - function getFuturesOptionValue(instrument, contracts, price) { - if (price || price === 0) { - const priceDecimal = new Decimal(price); - - const minimumTick = instrument.option.tick; - const minimumTickValue = instrument.option.value; - - const multiplier = instrument.option.multiplier; - - return priceDecimal.divide(minimumTick).multiply(minimumTickValue).multiply(multiplier).multiply(contracts); - } else { - return null; - } - } - - function getEquityOptionValue(instrument, contracts, price) { - if (price || price === 0) { - const priceDecimal = new Decimal(price); - const multiplier = instrument.option.multiplier; - - return priceDecimal.multiply(contracts).multiply(multiplier); - } else { - return null; - } - } - return PositionItem; })(); diff --git a/test/specs/calculators/ValuationCalculatorSpec.js b/test/specs/calculators/ValuationCalculatorSpec.js new file mode 100644 index 0000000..7ba5f5a --- /dev/null +++ b/test/specs/calculators/ValuationCalculatorSpec.js @@ -0,0 +1,218 @@ +const Decimal = require('@barchart/common-js/lang/Decimal'); + +const InstrumentType = require('./../../../lib/data/InstrumentType'), + ValuationCalculator = require('./../../../lib/calculators/ValuationCalculator'); + +describe('When calculating the value of a cash', () => { + 'use strict'; + + let instrument; + + beforeEach(() => { + instrument = { type: InstrumentType.CASH }; + }); + + it('$100 should equal $100 (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 0, 100).toFloat()).toEqual(100); + }); + + it('$100 should equal $100 (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, 0, new Decimal(100)).toFloat()).toEqual(100); + }); + + it('100 shares (long) valued at an undefined price should return null', () => { + expect(ValuationCalculator.calculate(instrument, undefined, 100)).toBe(null); + }); + + it('100 shares (long) valued at a null price should return null', () => { + expect(ValuationCalculator.calculate(instrument, null, 100)).toBe(null); + }); +}); + +describe('When calculating the value of an equity', () => { + 'use strict'; + + let instrument; + + beforeEach(() => { + instrument = { type: InstrumentType.EQUITY }; + }); + + it('100 shares (long) @ $17.50 should equal $1,750 (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 17.5, 100).toFloat()).toEqual(1750); + }); + + it('100 shares (long) @ $17.50 should equal $1,750 (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(17.5), new Decimal(100)).toFloat()).toEqual(1750); + }); + + it('50 shares (short) @ $17.50 should equal ($875) (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 17.5, -50).toFloat()).toEqual(-875); + }); + + it('50 shares (short) @ $17.50 should equal ($875) (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(17.5), new Decimal(-50)).toFloat()).toEqual(-875); + }); + + it('100 shares (long) valued at an undefined price should return null', () => { + expect(ValuationCalculator.calculate(instrument, undefined, 100)).toBe(null); + }); + + it('100 shares (long) valued at a null price should return null', () => { + expect(ValuationCalculator.calculate(instrument, null, 100)).toBe(null); + }); +}); + +describe('When calculating the value of an equity option (with a multiplier of 100)', () => { + 'use strict'; + + let instrument; + + beforeEach(() => { + instrument = { type: InstrumentType.EQUITY_OPTION, option: { multiplier: 100 } }; + }); + + it('2 contracts (long) shares @ $1.75 should equal $350 (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 1.75, 2).toFloat()).toEqual(350); + }); + + it('2 contracts (long) shares @ $1.75 should equal $350 (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(1.75), new Decimal(2)).toFloat()).toEqual(350); + }); + + it('2 contracts (short) shares @ $1.75 should equal ($350) (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 1.75, -2).toFloat()).toEqual(-350); + }); + + it('2 contracts (short) shares @ $1.75 should equal ($350) (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(1.75), new Decimal(-2)).toFloat()).toEqual(-350); + }); + + it('2 contracts (long) valued at an undefined price should return null', () => { + expect(ValuationCalculator.calculate(instrument, undefined, 2)).toBe(null); + }); + + it('2 contracts (long) valued at a null price should return null', () => { + expect(ValuationCalculator.calculate(instrument, null, 2)).toBe(null); + }); +}); + +describe('When calculating the value of a fund', () => { + 'use strict'; + + let instrument; + + beforeEach(() => { + instrument = { type: InstrumentType.FUND }; + }); + + it('100 units @ $17.50 should equal $1,750 (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 17.5, 100).toFloat()).toEqual(1750); + }); + + it('100 units @ $17.50 should equal $1,750 (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(17.5), new Decimal(100)).toFloat()).toEqual(1750); + }); + + it('100 units valued at an undefined price should return null', () => { + expect(ValuationCalculator.calculate(instrument, undefined, 100)).toBe(null); + }); + + it('100 units valued at a null price should return null', () => { + expect(ValuationCalculator.calculate(instrument, null, 100)).toBe(null); + }); +}); + +describe('When calculating the value of a future (with a minimum tick of 0.25 tick, and each tick valued at $12.50 each)', () => { + 'use strict'; + + let instrument; + + beforeEach(() => { + instrument = { type: InstrumentType.FUTURE, future: { tick: 0.25, value: 12.50 } }; + }); + + it('3 contracts (long) @ $461.75 should equal $69,262.50 (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 461.75, 3).toFloat()).toEqual(69262.5); + }); + + it('3 contracts (long) @ $461.75 should equal $69,262.50 (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(461.75), new Decimal(3)).toFloat()).toEqual(69262.5); + }); + + it('3 contracts (short) @ $461.75 should equal $69,262.50 (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 461.75, -3).toFloat()).toEqual(-69262.5); + }); + + it('3 contracts (short) @ $461.75 should equal $69,262.50 (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(461.75), new Decimal(-3)).toFloat()).toEqual(-69262.5); + }); + + it('3 contracts (long) valued at an undefined price should return null', () => { + expect(ValuationCalculator.calculate(instrument, undefined, 3)).toBe(null); + }); + + it('3 contracts (long) valued at a null price should return null', () => { + expect(ValuationCalculator.calculate(instrument, null, 3)).toBe(null); + }); +}); + +describe('When calculating the value of a futures option (with a minimum tick of 0.125 tick, and each tick valued at $6.5 each)', () => { + 'use strict'; + + let instrument; + + beforeEach(() => { + instrument = { type: InstrumentType.FUTURE_OPTION, option: { tick: 0.125, value: 6.5 } }; + }); + + it('5 contracts (long) @ $20.75 should equal $5,395.00 (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 20.75, 5).toFloat()).toEqual(5395); + }); + + it('5 contracts (long) @ $20.75 should equal $5,395.00 (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(20.75), new Decimal(5)).toFloat()).toEqual(5395); + }); + + it('5 contracts (short) @ $20.75 should equal ($5,395.00) (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 20.75, -5).toFloat()).toEqual(-5395); + }); + + it('5 contracts (short) @ $20.75 should equal ($5,395.00) (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(20.75), new Decimal(-5)).toFloat()).toEqual(-5395); + }); + + it('5 contracts (long) valued at an undefined price should return null', () => { + expect(ValuationCalculator.calculate(instrument, undefined, 5)).toBe(null); + }); + + it('5 contracts (long) valued at a null price should return null', () => { + expect(ValuationCalculator.calculate(instrument, null, 5)).toBe(null); + }); +}); + +describe('When calculating the value of an "other" item"', () => { + 'use strict'; + + let instrument; + + beforeEach(() => { + instrument = { type: InstrumentType.OTHER }; + }); + + it('4 units @ $200,000 should equal $800,000 (using numbers)', () => { + expect(ValuationCalculator.calculate(instrument, 200000, 4).toFloat()).toEqual(800000); + }); + + it('4 units @ $200,000 should equal $1,750 (using decimals)', () => { + expect(ValuationCalculator.calculate(instrument, new Decimal(200000), new Decimal(4)).toFloat()).toEqual(800000); + }); + + it('4 units valued at an undefined price should return null', () => { + expect(ValuationCalculator.calculate(instrument, undefined, 4)).toBe(null); + }); + + it('4 units valued at a null price should return null', () => { + expect(ValuationCalculator.calculate(instrument, null, 4)).toBe(null); + }); +}); \ No newline at end of file