diff --git a/package-lock.json b/package-lock.json index 9b83b98cd..b17f2dbad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -928,12 +928,6 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -1319,12 +1313,6 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, "browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -1626,29 +1614,6 @@ "lazy-cache": "1.0.4" } }, - "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", - "dev": true, - "requires": { - "assertion-error": "1.1.0", - "check-error": "1.0.2", - "deep-eql": "3.0.1", - "get-func-name": "2.0.0", - "pathval": "1.1.0", - "type-detect": "4.0.8" - } - }, - "chai-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", - "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", - "dev": true, - "requires": { - "check-error": "1.0.2" - } - }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -1700,12 +1665,6 @@ "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", "dev": true }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, "chokidar": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", @@ -2870,15 +2829,6 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, "deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", @@ -4440,12 +4390,6 @@ "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", "dev": true }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", @@ -4654,12 +4598,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, "handle-thing": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", @@ -6852,59 +6790,6 @@ "minimist": "0.0.8" } }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", - "dev": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - } - } - }, - "mocha-lcov-reporter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mocha-lcov-reporter/-/mocha-lcov-reporter-1.3.0.tgz", - "integrity": "sha1-Rpve9PivyaEWBW8HnfYYLQr7A4Q=", - "dev": true - }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -7734,12 +7619,6 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", "dev": true }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, "pbkdf2": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", diff --git a/package.json b/package.json index 63fc9738a..1f2446d73 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,6 @@ "beautify-lint": "^1.0.3", "benchmark": "^2.1.1", "bundle-loader": "~0.5.0", - "chai": "^4.1.2", - "chai-as-promised": "^7.1.1", "css-loader": "^0.28.11", "es6-promise-polyfill": "^1.1.1", "eslint": "^4.19.1", @@ -65,8 +63,6 @@ "lodash": "^4.17.10", "mini-css-extract-plugin": "^0.4.1", "mkdirp": "^0.5.1", - "mocha": "^5.2.0", - "mocha-lcov-reporter": "^1.0.0", "node-sass": "^4.10.0", "raw-loader": "~0.5.0", "sass-loader": "^7.0.3", diff --git a/src/StockMarket/StockMarketHelpers.ts b/src/StockMarket/StockMarketHelpers.ts index 607af65d2..8754e9c9a 100644 --- a/src/StockMarket/StockMarketHelpers.ts +++ b/src/StockMarket/StockMarketHelpers.ts @@ -2,6 +2,33 @@ import { Stock } from "./Stock"; import { PositionTypes } from "./data/PositionTypes"; import { CONSTANTS } from "../Constants"; +/** + * Given a stock, calculates the amount by which the stock price is multiplied + * for an 'upward' price movement. This does not actually increase the stock's price, + * just calculates the multiplier + * @param {Stock} stock - Stock for price movement + * @returns {number | null} Number by which stock's price should be multiplied. Null for invalid args + */ +export function calculateIncreasingPriceMovement(stock: Stock): number | null { + if (!(stock instanceof Stock)) { return null; } + + return (1 + (stock.priceMovementPerc / 100)); +} + +/** + * Given a stock, calculates the amount by which the stock price is multiplied + * for a "downward" price movement. This does not actually increase the stock's price, + * just calculates the multiplier + * @param {Stock} stock - Stock for price movement + * @returns {number | null} Number by which stock's price should be multiplied. Null for invalid args + */ +export function calculateDecreasingPriceMovement(stock: Stock): number | null { + if (!(stock instanceof Stock)) { return null; } + + return (1 - (stock.priceMovementPerc / 100)); +} + + /** * Calculate the total cost of a "buy" transaction. This accounts for spread, * price movements, and commission. @@ -32,20 +59,26 @@ export function getBuyTransactionCost(stock: Stock, shares: number, posType: Pos let remainingShares = shares - stock.shareTxUntilMovement; let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement); + + // The initial cost calculation takes care of the first "iteration" let currPrice = isLong ? stock.getAskPrice() : stock.getBidPrice(); let totalCost = (stock.shareTxUntilMovement * currPrice); + + function processPriceMovement() { + if (isLong) { + currPrice *= calculateIncreasingPriceMovement(stock)!; + } else { + currPrice *= calculateDecreasingPriceMovement(stock)!; + } + } + for (let i = 1; i < numIterations; ++i) { + processPriceMovement(); + const amt = Math.min(stock.shareTxForMovement, remainingShares); totalCost += (amt * currPrice); remainingShares -= amt; - - // Price movement - if (isLong) { - currPrice *= (1 + (stock.priceMovementPerc / 100)); - } else { - currPrice *= (1 - (stock.priceMovementPerc / 100)); - } } return totalCost + CONSTANTS.StockMarketCommission; @@ -76,20 +109,19 @@ export function processBuyTransactionPriceMovement(stock: Stock, shares: number, let remainingShares = shares - stock.shareTxUntilMovement; let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement); - // The initial cost calculation takes care of the first "iteration" - let currPrice = isLong ? stock.getAskPrice() : stock.getBidPrice(); - for (let i = 1; i < numIterations; ++i) { - const amt = Math.min(stock.shareTxForMovement, remainingShares); - remainingShares -= amt; - - // Price movement + let currPrice = stock.price; + function processPriceMovement() { if (isLong) { - currPrice *= (1 + (stock.priceMovementPerc / 100)); + currPrice *= calculateIncreasingPriceMovement(stock)!; } else { - currPrice *= (1 - (stock.priceMovementPerc / 100)); + currPrice *= calculateDecreasingPriceMovement(stock)!; } } + for (let i = 1; i < numIterations; ++i) { + processPriceMovement(); + } + stock.price = currPrice; stock.shareTxUntilMovement = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovement) % stock.shareTxForMovement); } @@ -124,7 +156,7 @@ export function getSellTransactionGain(stock: Stock, shares: number, posType: Po } } - // Calculate how many iterations of price changes we need to accoutn for + // Calculate how many iterations of price changes we need to account for let remainingShares = shares - stock.shareTxUntilMovement; let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement); @@ -144,16 +176,16 @@ export function getSellTransactionGain(stock: Stock, shares: number, posType: Po let currPrice = isLong ? stock.getBidPrice() : stock.getAskPrice(); let totalGain = calculateGain(currPrice, stock.shareTxUntilMovement); for (let i = 1; i < numIterations; ++i) { + // Price movement + if (isLong) { + currPrice *= calculateDecreasingPriceMovement(stock)!; + } else { + currPrice *= calculateIncreasingPriceMovement(stock)!; + } + const amt = Math.min(stock.shareTxForMovement, remainingShares); totalGain += calculateGain(currPrice, amt); remainingShares -= amt; - - // Price movement - if (isLong) { - currPrice *= (1 - (stock.priceMovementPerc / 100)); - } else { - currPrice *= (1 + (stock.priceMovementPerc / 100)); - } } return totalGain - CONSTANTS.StockMarketCommission; @@ -183,17 +215,13 @@ export function processSellTransactionPriceMovement(stock: Stock, shares: number let remainingShares = shares - stock.shareTxUntilMovement; let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement); - // The initial cost calculation takes care of the first "iteration" - let currPrice = isLong ? stock.getBidPrice() : stock.getAskPrice(); + let currPrice = stock.price; for (let i = 1; i < numIterations; ++i) { - const amt = Math.min(stock.shareTxForMovement, remainingShares); - remainingShares -= amt; - // Price movement if (isLong) { - currPrice *= (1 - (stock.priceMovementPerc / 100)); + currPrice *= calculateDecreasingPriceMovement(stock)!; } else { - currPrice *= (1 + (stock.priceMovementPerc / 100)); + currPrice *= calculateIncreasingPriceMovement(stock)!; } } diff --git a/tests/StockMarketTests.js b/tests/StockMarketTests.js new file mode 100644 index 000000000..d53c90526 --- /dev/null +++ b/tests/StockMarketTests.js @@ -0,0 +1,432 @@ +import { CONSTANTS } from "../src/Constants"; +import { Stock } from "../src/StockMarket/Stock"; +import { + calculateIncreasingPriceMovement, + calculateDecreasingPriceMovement, + getBuyTransactionCost, + getSellTransactionGain, + processBuyTransactionPriceMovement, + processSellTransactionPriceMovement, +} from "../src/StockMarket/StockMarketHelpers"; +import { PositionTypes } from "../src/StockMarket/data/PositionTypes"; + +const assert = chai.assert; +const expect = chai.expect; + +console.log("Beginning Stock Market Tests"); + +describe("Stock Market Tests", function() { + const commission = CONSTANTS.StockMarketCommission; + + // Generic Stock object that can be used by each test + let stock; + const ctorParams = { + b: true, + initPrice: 10e3, + marketCap: 5e9, + mv: 1, + name: "MockStock", + otlkMag: 10, + spreadPerc: 1, + shareTxForMovement: 5e3, + symbol: "mock", + }; + + beforeEach(function() { + function construct() { + stock = new Stock(ctorParams); + } + + expect(construct).to.not.throw(); + }); + + describe("Stock Class", function() { + describe("constructor", function() { + it("should have default parameters", function() { + let defaultStock; + function construct() { + defaultStock = new Stock(); + } + + expect(construct).to.not.throw(); + expect(defaultStock.name).to.equal(""); + }); + + it("should properly initialize props from parameters", function() { + expect(stock.name).to.equal(ctorParams.name); + expect(stock.symbol).to.equal(ctorParams.symbol); + expect(stock.price).to.equal(ctorParams.initPrice); + expect(stock.lastPrice).to.equal(ctorParams.initPrice); + expect(stock.b).to.equal(ctorParams.b); + expect(stock.mv).to.equal(ctorParams.mv); + expect(stock.shareTxForMovement).to.equal(ctorParams.shareTxForMovement); + expect(stock.shareTxUntilMovement).to.equal(ctorParams.shareTxForMovement); + expect(stock.maxShares).to.be.below(stock.totalShares); + expect(stock.spreadPerc).to.equal(ctorParams.spreadPerc); + expect(stock.priceMovementPerc).to.be.a("number"); + expect(stock.priceMovementPerc).to.be.at.most(stock.spreadPerc); + expect(stock.priceMovementPerc).to.be.at.least(0); + }); + + it ("should properly initialize props from range-values", function() { + let stock; + const params = { + b: true, + initPrice: { + max: 10e3, + min: 1e3, + }, + marketCap: 5e9, + mv: { + divisor: 100, + max: 150, + min: 50, + }, + name: "MockStock", + otlkMag: 10, + spreadPerc: { + divisor: 10, + max: 10, + min: 1, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: "mock", + }; + + function construct() { + stock = new Stock(params); + } + + expect(construct).to.not.throw(); + expect(stock.price).to.be.within(params.initPrice.min, params.initPrice.max); + expect(stock.mv).to.be.within(params.mv.min / params.mv.divisor, params.mv.max / params.mv.divisor); + expect(stock.spreadPerc).to.be.within(params.spreadPerc.min / params.spreadPerc.divisor, params.spreadPerc.max / params.spreadPerc.divisor); + expect(stock.shareTxForMovement).to.be.within(params.shareTxForMovement.min, params.shareTxForMovement.max); + }); + + it("should round the 'totalShare' prop to the nearest 100k", function() { + expect(stock.totalShares % 100e3).to.equal(0); + }); + }); + + describe("#changePrice()", function() { + it("should set both the last price and current price properties", function() { + const newPrice = 20e3; + stock.changePrice(newPrice); + expect(stock.lastPrice).to.equal(ctorParams.initPrice); + expect(stock.price).to.equal(newPrice); + }); + }); + + describe("#getAskPrice()", function() { + it("should return the price increased by spread percentage", function() { + const perc = stock.spreadPerc / 100; + expect(perc).to.be.at.most(1); + expect(perc).to.be.at.least(0); + + const expected = stock.price * (1 + perc); + expect(stock.getAskPrice()).to.equal(expected); + }); + }); + + describe("#getBidPrice()", function() { + it("should return the price decreased by spread percentage", function() { + const perc = stock.spreadPerc / 100; + expect(perc).to.be.at.most(1); + expect(perc).to.be.at.least(0); + + const expected = stock.price * (1 - perc); + expect(stock.getBidPrice()).to.equal(expected); + }); + }); + }); + + describe("Transaction Cost Calculator Functions", function() { + describe("getBuyTransactionCost()", function() { + it("should fail on invalid 'stock' argument", function() { + const res = getBuyTransactionCost({}, 10, PositionTypes.Long); + expect(res).to.equal(null); + }); + + it("should fail on invalid 'shares' arg", function() { + let res = getBuyTransactionCost(stock, NaN, PositionTypes.Long); + expect(res).to.equal(null); + + res = getBuyTransactionCost(stock, -1, PositionTypes.Long); + expect(res).to.equal(null); + }); + + it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() { + const shares = ctorParams.shareTxForMovement / 2; + const res = getBuyTransactionCost(stock, shares, PositionTypes.Long); + expect(res).to.equal(shares * stock.getAskPrice() + commission); + }); + + it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() { + const shares = ctorParams.shareTxForMovement / 2; + const res = getBuyTransactionCost(stock, shares, PositionTypes.Short); + expect(res).to.equal(shares * stock.getBidPrice() + commission); + }); + + it("should properly evaluate LONG transactions that trigger price movements", function() { + const sharesPerMvmt = ctorParams.shareTxForMovement; + const shares = sharesPerMvmt * 3; + const res = getBuyTransactionCost(stock, shares, PositionTypes.Long); + + // Calculate expected cost + const secondPrice = stock.getAskPrice() * calculateIncreasingPriceMovement(stock); + const thirdPrice = secondPrice * calculateIncreasingPriceMovement(stock); + let expected = (sharesPerMvmt * stock.getAskPrice()) + (sharesPerMvmt * secondPrice) + (sharesPerMvmt * thirdPrice); + + expect(res).to.equal(expected + commission); + }); + + it("should properly evaluate SHORT transactions that trigger price movements", function() { + const sharesPerMvmt = ctorParams.shareTxForMovement; + const shares = sharesPerMvmt * 3; + const res = getBuyTransactionCost(stock, shares, PositionTypes.Short); + + // Calculate expected cost + const secondPrice = stock.getBidPrice() * calculateDecreasingPriceMovement(stock); + const thirdPrice = secondPrice * calculateDecreasingPriceMovement(stock); + let expected = (sharesPerMvmt * stock.getBidPrice()) + (sharesPerMvmt * secondPrice) + (sharesPerMvmt * thirdPrice); + + expect(res).to.equal(expected + commission); + }); + + it("should cap the 'shares' argument at the stock's maximum number of shares", function() { + const maxRes = getBuyTransactionCost(stock, stock.maxShares, PositionTypes.Long); + const exceedRes = getBuyTransactionCost(stock, stock.maxShares * 10, PositionTypes.Long); + expect(maxRes).to.equal(exceedRes); + }); + }); + + describe("getSellTransactionGain()", function() { + it("should fail on invalid 'stock' argument", function() { + const res = getSellTransactionGain({}, 10, PositionTypes.Long); + expect(res).to.equal(null); + }); + + it("should fail on invalid 'shares' arg", function() { + let res = getSellTransactionGain(stock, NaN, PositionTypes.Long); + expect(res).to.equal(null); + + res = getSellTransactionGain(stock, -1, PositionTypes.Long); + expect(res).to.equal(null); + }); + + it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() { + const shares = ctorParams.shareTxForMovement / 2; + const res = getSellTransactionGain(stock, shares, PositionTypes.Long); + const expected = shares * stock.getBidPrice() - commission; + expect(res).to.equal(expected); + }); + + it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() { + // We need to set this property in order to calculate gains from short position + stock.playerAvgShortPx = stock.price * 2; + + const shares = ctorParams.shareTxForMovement / 2; + const res = getSellTransactionGain(stock, shares, PositionTypes.Short); + const expected = (shares * stock.playerAvgShortPx) + (shares * (stock.playerAvgShortPx - stock.getAskPrice())) - commission; + expect(res).to.equal(expected); + }); + + it("should properly evaluate LONG transactions that trigger price movements", function() { + const sharesPerMvmt = ctorParams.shareTxForMovement; + const shares = sharesPerMvmt * 3; + const res = getSellTransactionGain(stock, shares, PositionTypes.Long); + + // Calculated expected gain + const mvmt = calculateDecreasingPriceMovement(stock); + const secondPrice = stock.getBidPrice() * mvmt; + const thirdPrice = secondPrice * mvmt; + const expected = (sharesPerMvmt * stock.getBidPrice()) + (sharesPerMvmt * secondPrice) + (sharesPerMvmt * thirdPrice); + + expect(res).to.equal(expected - commission); + }); + + it("should properly evaluate SHORT transactions that trigger price movements", function() { + // We need to set this property in order to calculate gains from short position + stock.playerAvgShortPx = stock.price * 2; + + const sharesPerMvmt = ctorParams.shareTxForMovement; + const shares = sharesPerMvmt * 3; + const res = getSellTransactionGain(stock, shares, PositionTypes.Short); + + // Calculate expected gain + const mvmt = calculateIncreasingPriceMovement(stock); + const secondPrice = stock.getAskPrice() * mvmt; + const thirdPrice = secondPrice * mvmt; + function getGainForPrice(thisPrice) { + const origCost = sharesPerMvmt * stock.playerAvgShortPx; + return origCost + ((stock.playerAvgShortPx - thisPrice) * sharesPerMvmt); + } + const expected = getGainForPrice(stock.getAskPrice()) + getGainForPrice(secondPrice) + getGainForPrice(thirdPrice); + + expect(res).to.equal(expected - commission); + }); + + it("should cap the 'shares' argument at the stock's maximum number of shares", function() { + const maxRes = getSellTransactionGain(stock, stock.maxShares, PositionTypes.Long); + const exceedRes = getSellTransactionGain(stock, stock.maxShares * 10, PositionTypes.Long); + expect(maxRes).to.equal(exceedRes); + }); + }); + }); + + describe("Price Movement Processor Functions", function() { + describe("processBuyTransactionPriceMovement()", function() { + const noMvmtShares = Math.round(ctorParams.shareTxForMovement / 2.2); + const mvmtShares = ctorParams.shareTxForMovement * 3 + noMvmtShares; + + it("should do nothing on invalid 'stock' argument", function() { + const oldPrice = stock.price; + const oldTracker = stock.shareTxUntilMovement; + + processBuyTransactionPriceMovement({}, mvmtShares, PositionTypes.Long); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(oldTracker); + }); + + it("should do nothing on invalid 'shares' arg", function() { + const oldPrice = stock.price; + const oldTracker = stock.shareTxUntilMovement; + + processBuyTransactionPriceMovement(stock, NaN, PositionTypes.Long); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(oldTracker); + + processBuyTransactionPriceMovement(stock, -1, PositionTypes.Long); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(oldTracker); + }); + + it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() { + const oldPrice = stock.price; + + processBuyTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Long); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); + }); + + it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() { + const oldPrice = stock.price; + + processBuyTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Short); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); + }); + + it("should properly evaluate LONG transactions that trigger price movements", function() { + const oldPrice = stock.price; + function getNthPrice(n) { + let price = oldPrice; + for (let i = 1; i < n; ++i) { + price *= calculateIncreasingPriceMovement(stock); + } + + return price; + } + + processBuyTransactionPriceMovement(stock, mvmtShares, PositionTypes.Long); + expect(stock.price).to.equal(getNthPrice(4)); + expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); + }); + + it("should properly evaluate SHORT transactions that trigger price movements", function() { + const oldPrice = stock.price; + function getNthPrice(n) { + let price = oldPrice; + for (let i = 1; i < n; ++i) { + price *= calculateDecreasingPriceMovement(stock); + } + + return price; + } + + processBuyTransactionPriceMovement(stock, mvmtShares, PositionTypes.Short); + expect(stock.price).to.equal(getNthPrice(4)); + expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); + }); + }); + + describe("processSellTransactionPriceMovement()", function() { + const noMvmtShares = Math.round(ctorParams.shareTxForMovement / 2.2); + const mvmtShares = ctorParams.shareTxForMovement * 3 + noMvmtShares; + + it("should do nothing on invalid 'stock' argument", function() { + const oldPrice = stock.price; + const oldTracker = stock.shareTxUntilMovement; + + processSellTransactionPriceMovement({}, mvmtShares, PositionTypes.Long); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(oldTracker); + }); + + it("should do nothing on invalid 'shares' arg", function() { + const oldPrice = stock.price; + const oldTracker = stock.shareTxUntilMovement; + + processSellTransactionPriceMovement(stock, NaN, PositionTypes.Long); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(oldTracker); + + processSellTransactionPriceMovement(stock, -1, PositionTypes.Long); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(oldTracker); + }); + + it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() { + const oldPrice = stock.price; + + processSellTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Long); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); + }); + + it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() { + const oldPrice = stock.price; + + processSellTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Short); + expect(stock.price).to.equal(oldPrice); + expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); + }); + + it("should properly evaluate LONG transactions that trigger price movements", function() { + const oldPrice = stock.price; + function getNthPrice(n) { + let price = oldPrice; + for (let i = 1; i < n; ++i) { + price *= calculateDecreasingPriceMovement(stock); + } + + return price; + } + + processSellTransactionPriceMovement(stock, mvmtShares, PositionTypes.Long); + expect(stock.price).to.equal(getNthPrice(4)); + expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); + }); + + it("should properly evaluate SHORT transactions that trigger price movements", function() { + const oldPrice = stock.price; + function getNthPrice(n) { + let price = oldPrice; + for (let i = 1; i < n; ++i) { + price *= calculateIncreasingPriceMovement(stock); + } + + return price; + } + + processSellTransactionPriceMovement(stock, mvmtShares, PositionTypes.Short); + expect(stock.price).to.equal(getNthPrice(4)); + expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares); + }); + }); + }); +}); diff --git a/tests/index.html b/tests/index.html index 548f118fc..d0e4c0c80 100644 --- a/tests/index.html +++ b/tests/index.html @@ -3,17 +3,20 @@