From c485fdfa87090613e408fdbde04e685d865c3676 Mon Sep 17 00:00:00 2001 From: danielyxie Date: Wed, 22 May 2019 19:12:06 -0700 Subject: [PATCH] Removed stock market price movement. Now only forecast is influenced by big transactions --- doc/source/netscript/netscriptixapi.rst | 2 - .../netscript/tixapi/getStockPurchaseCost.rst | 15 - .../netscript/tixapi/getStockSaleGain.rst | 15 - src/Constants.ts | 6 +- src/NetscriptFunctions.js | 46 -- src/StockMarket/BuyingAndSelling.ts | 11 +- src/StockMarket/OrderProcessing.ts | 107 +---- src/StockMarket/Stock.ts | 13 +- src/StockMarket/StockMarket.jsx | 13 +- src/StockMarket/StockMarketHelpers.ts | 307 +------------ src/StockMarket/ui/StockTicker.tsx | 24 - test/Netscript/DynamicRamCalculationTests.js | 10 - test/StockMarketTests.js | 421 ++++++++---------- 13 files changed, 236 insertions(+), 754 deletions(-) delete mode 100644 doc/source/netscript/tixapi/getStockPurchaseCost.rst delete mode 100644 doc/source/netscript/tixapi/getStockSaleGain.rst diff --git a/doc/source/netscript/netscriptixapi.rst b/doc/source/netscript/netscriptixapi.rst index 76636efc5..a836ade0a 100644 --- a/doc/source/netscript/netscriptixapi.rst +++ b/doc/source/netscript/netscriptixapi.rst @@ -22,8 +22,6 @@ access even after you 'reset' by installing Augmentations getStockBidPrice() getStockPosition() getStockMaxShares() - getStockPurchaseCost() - getStockSaleGain() buyStock() sellStock() shortStock() diff --git a/doc/source/netscript/tixapi/getStockPurchaseCost.rst b/doc/source/netscript/tixapi/getStockPurchaseCost.rst deleted file mode 100644 index ef1de43bf..000000000 --- a/doc/source/netscript/tixapi/getStockPurchaseCost.rst +++ /dev/null @@ -1,15 +0,0 @@ -getStockPurchaseCost() Netscript Function -========================================= - -.. js:function:: getStockPurchaseCost(sym, shares, posType) - - :param string sym: Stock symbol - :param number shares: Number of shares to purchase - :param string posType: Specifies whether the order is a "Long" or "Short" position. - The values "L" or "S" can also be used. - :RAM cost: 2 GB - - Calculates and returns how much it would cost to buy a given number of - shares of a stock. This takes into account :ref:`spread `, - :ref:`large transactions influencing the price of the stock ` - and commission fees. diff --git a/doc/source/netscript/tixapi/getStockSaleGain.rst b/doc/source/netscript/tixapi/getStockSaleGain.rst deleted file mode 100644 index 4716f6514..000000000 --- a/doc/source/netscript/tixapi/getStockSaleGain.rst +++ /dev/null @@ -1,15 +0,0 @@ -getStockSaleGain() Netscript Function -===================================== - -.. js:function:: getStockSaleGain(sym, shares, posType) - - :param string sym: Stock symbol - :param number shares: Number of shares to sell - :param string posType: Specifies whether the order is a "Long" or "Short" position. - The values "L" or "S" can also be used. - :RAM cost: 2 GB - - Calculates and returns how much you would gain from selling a given number of - shares of a stock. This takes into account :ref:`spread `, - :ref:`large transactions influencing the price of the stock ` - and commission fees. diff --git a/src/Constants.ts b/src/Constants.ts index 0a4927aec..41a0985f0 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -222,7 +222,11 @@ export let CONSTANTS: IMap = { LatestUpdate: ` v0.47.0 - * + * Stock Market changes: + ** Transactions no longer influence stock prices (but they still influence forecast) + ** Removed getStockPurchaseCost() and getStockSaleGain() Netscript functions + ** + * Scripts now start/stop instantly v0.47.0 diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index 394dd0156..aa4c877dc 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -98,10 +98,6 @@ import { cancelOrder, displayStockMarketContent, } from "./StockMarket/StockMarket"; -import { - getBuyTransactionCost, - getSellTransactionGain, -} from "./StockMarket/StockMarketHelpers"; import { OrderTypes } from "./StockMarket/data/OrderTypes"; import { PositionTypes } from "./StockMarket/data/PositionTypes"; import { StockSymbols } from "./StockMarket/data/StockSymbols"; @@ -1496,48 +1492,6 @@ function NetscriptFunctions(workerScript) { return stock.maxShares; }, - getStockPurchaseCost: function(symbol, shares, posType) { - updateDynamicRam("getStockPurchaseCost", getRamCost("getStockPurchaseCost")); - checkTixApiAccess("getStockPurchaseCost"); - const stock = getStockFromSymbol(symbol, "getStockPurchaseCost"); - shares = Math.round(shares); - - let pos; - const sanitizedPosType = posType.toLowerCase(); - if (sanitizedPosType.includes("l")) { - pos = PositionTypes.Long; - } else if (sanitizedPosType.includes("s")) { - pos = PositionTypes.Short; - } else { - return Infinity; - } - - const res = getBuyTransactionCost(stock, shares, pos); - if (res == null) { return Infinity; } - - return res; - }, - getStockSaleGain: function(symbol, shares, posType) { - updateDynamicRam("getStockSaleGain", getRamCost("getStockSaleGain")); - checkTixApiAccess("getStockSaleGain"); - const stock = getStockFromSymbol(symbol, "getStockSaleGain"); - shares = Math.round(shares); - - let pos; - const sanitizedPosType = posType.toLowerCase(); - if (sanitizedPosType.includes("l")) { - pos = PositionTypes.Long; - } else if (sanitizedPosType.includes("s")) { - pos = PositionTypes.Short; - } else { - return 0; - } - - const res = getSellTransactionGain(stock, shares, pos); - if (res == null) { return 0; } - - return res; - }, buyStock: function(symbol, shares) { updateDynamicRam("buyStock", getRamCost("buyStock")); checkTixApiAccess("buyStock"); diff --git a/src/StockMarket/BuyingAndSelling.ts b/src/StockMarket/BuyingAndSelling.ts index e628595af..ba6ec3630 100644 --- a/src/StockMarket/BuyingAndSelling.ts +++ b/src/StockMarket/BuyingAndSelling.ts @@ -6,8 +6,7 @@ import { Stock } from "./Stock"; import { getBuyTransactionCost, getSellTransactionGain, - processBuyTransactionPriceMovement, - processSellTransactionPriceMovement + processTransactionForecastMovement, } from "./StockMarketHelpers"; import { PositionTypes } from "./data/PositionTypes"; @@ -81,7 +80,7 @@ export function buyStock(stock: Stock, shares: number, workerScript: WorkerScrip const newTotal = origTotal + totalPrice - CONSTANTS.StockMarketCommission; stock.playerShares = Math.round(stock.playerShares + shares); stock.playerAvgPx = newTotal / stock.playerShares; - processBuyTransactionPriceMovement(stock, shares, PositionTypes.Long); + processTransactionForecastMovement(stock, shares); if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") { opts.rerenderFn(); } @@ -138,7 +137,7 @@ export function sellStock(stock: Stock, shares: number, workerScript: WorkerScri stock.playerAvgPx = 0; } - processSellTransactionPriceMovement(stock, shares, PositionTypes.Long); + processTransactionForecastMovement(stock, shares); if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") { opts.rerenderFn(); @@ -211,7 +210,7 @@ export function shortStock(stock: Stock, shares: number, workerScript: WorkerScr const newTotal = origTotal + totalPrice - CONSTANTS.StockMarketCommission; stock.playerShortShares = Math.round(stock.playerShortShares + shares); stock.playerAvgShortPx = newTotal / stock.playerShortShares; - processBuyTransactionPriceMovement(stock, shares, PositionTypes.Short); + processTransactionForecastMovement(stock, shares); if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") { opts.rerenderFn(); @@ -278,7 +277,7 @@ export function sellShort(stock: Stock, shares: number, workerScript: WorkerScri if (stock.playerShortShares === 0) { stock.playerAvgShortPx = 0; } - processSellTransactionPriceMovement(stock, shares, PositionTypes.Short); + processTransactionForecastMovement(stock, shares); if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") { opts.rerenderFn(); diff --git a/src/StockMarket/OrderProcessing.ts b/src/StockMarket/OrderProcessing.ts index 8324fec76..f77e29a08 100644 --- a/src/StockMarket/OrderProcessing.ts +++ b/src/StockMarket/OrderProcessing.ts @@ -107,8 +107,6 @@ function executeOrder(order: Order, refs: IProcessOrderRefs) { const stockMarket = refs.stockMarket; const orderBook = stockMarket["Orders"]; const stockOrders = orderBook[stock.symbol]; - const isLimit = (order.type === OrderTypes.LimitBuy || order.type === OrderTypes.LimitSell); - let sharesTransacted = 0; // When orders are executed, the buying and selling functions shouldn't // emit popup dialog boxes. This options object configures the functions for that @@ -120,124 +118,37 @@ function executeOrder(order: Order, refs: IProcessOrderRefs) { let res = true; let isBuy = false; switch (order.type) { - case OrderTypes.LimitBuy: { + case OrderTypes.LimitBuy: + case OrderTypes.StopBuy: isBuy = true; - - // We only execute limit orders until the price fails to match the order condition - const isLong = (order.pos === PositionTypes.Long); - const firstShares = Math.min(order.shares, isLong ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown); - - // First transaction to trigger movement - let res = (isLong ? buyStock(stock, firstShares, null, opts) : shortStock(stock, firstShares, null, opts)); - if (res) { - sharesTransacted = firstShares; - } else { - break; - } - - let remainingShares = order.shares - firstShares; - let remainingIterations = Math.ceil(remainingShares / stock.shareTxForMovement); - for (let i = 0; i < remainingIterations; ++i) { - if (isLong && stock.price > order.price) { - break; - } else if (!isLong && stock.price < order.price) { - break; - } - - const shares = Math.min(remainingShares, stock.shareTxForMovement); - let res = (isLong ? buyStock(stock, shares, null, opts) : shortStock(stock, shares, null, opts)); - if (res) { - sharesTransacted += shares; - remainingShares -= shares; - } else { - break; - } - } - break; - } - case OrderTypes.StopBuy: { - isBuy = true; - sharesTransacted = order.shares; if (order.pos === PositionTypes.Long) { res = buyStock(stock, order.shares, null, opts) && res; } else if (order.pos === PositionTypes.Short) { res = shortStock(stock, order.shares, null, opts) && res; } break; - } - case OrderTypes.LimitSell: { - // TODO need to account for player's shares here - // We only execute limit orders until the price fails to match the order condition - const isLong = (order.pos === PositionTypes.Long); - const totalShares = Math.min((isLong ? stock.playerShares : stock.playerShortShares), order.shares); - if (totalShares === 0) { - return; // Player has no shares - } - const firstShares = Math.min(totalShares, isLong ? stock.shareTxUntilMovementDown : stock.shareTxUntilMovementUp); - - // First transaction to trigger movement - if (isLong ? sellStock(stock, firstShares, null, opts) : sellShort(stock, firstShares, null, opts)) { - sharesTransacted = firstShares; - } else { - break; - } - - let remainingShares = totalShares - firstShares; - let remainingIterations = Math.ceil(remainingShares / stock.shareTxForMovement); - for (let i = 0; i < remainingIterations; ++i) { - if (isLong && stock.price < order.price) { - break; - } else if (!isLong && stock.price > order.price) { - break; - } - - const shares = Math.min(remainingShares, stock.shareTxForMovement); - if (isLong ? sellStock(stock, shares, null, opts) : sellShort(stock, shares, null, opts)) { - sharesTransacted += shares; - remainingShares -= shares; - } else { - break; - } - } - break; - } - case OrderTypes.StopSell: { + case OrderTypes.LimitSell: + case OrderTypes.StopSell: if (order.pos === PositionTypes.Long) { - sharesTransacted = Math.min(stock.playerShares, order.shares); - if (sharesTransacted <= 0) { return; } - res = sellStock(stock, sharesTransacted, null, opts) && res; + res = sellStock(stock, order.shares, null, opts) && res; } else if (order.pos === PositionTypes.Short) { - sharesTransacted = Math.min(stock.playerShortShares, order.shares); - if (sharesTransacted <= 0) { return; } - res = sellShort(stock, sharesTransacted, null, opts) && res; + res = sellShort(stock, order.shares, null, opts) && res; } break; - } default: console.warn(`Invalid order type: ${order.type}`); return; } - if (isLimit) { - res = (sharesTransacted > 0); - } - // Position type, for logging/message purposes const pos = order.pos === PositionTypes.Long ? "Long" : "Short"; if (res) { for (let i = 0; i < stockOrders.length; ++i) { if (order == stockOrders[i]) { - // Limit orders might only transact a certain # of shares, so we have the adjust the order qty. - stockOrders[i].shares -= sharesTransacted; - if (stockOrders[i].shares <= 0) { - stockOrders.splice(i, 1); - dialogBoxCreate(`${order.type} for ${stock.symbol} @ ${numeralWrapper.formatMoney(order.price)} (${pos}) was filled ` + - `(${numeralWrapper.formatBigNumber(Math.round(sharesTransacted))} share)`); - } else { - dialogBoxCreate(`${order.type} for ${stock.symbol} @ ${numeralWrapper.formatMoney(order.price)} (${pos}) was partially filled ` + - `(${numeralWrapper.formatBigNumber(Math.round(sharesTransacted))} shares transacted, ${stockOrders[i].shares} shares remaining`); - } + stockOrders.splice(i, 1); + dialogBoxCreate(`${order.type} for ${stock.symbol} @ ${numeralWrapper.formatMoney(order.price)} (${pos}) was filled ` + + `(${numeralWrapper.formatBigNumber(Math.round(order.shares))} shares)`); refs.rerenderFn(); return; } diff --git a/src/StockMarket/Stock.ts b/src/StockMarket/Stock.ts index 5cd0afc1c..2147b7c16 100644 --- a/src/StockMarket/Stock.ts +++ b/src/StockMarket/Stock.ts @@ -131,12 +131,6 @@ export class Stock { */ price: number; - /** - * Percentage by which the stock's price changes for a transaction-induced - * price movement. - */ - readonly priceMovementPerc: number; - /** * How many shares need to be transacted in order to trigger a price movement */ @@ -146,8 +140,7 @@ export class Stock { * How many share transactions remaining until a price movement occurs * (separately tracked for upward and downward movements) */ - shareTxUntilMovementDown: number; - shareTxUntilMovementUp: number; + shareTxUntilMovement: number; /** * Spread percentage. The bid/ask prices for this stock are N% above or below @@ -182,10 +175,8 @@ export class Stock { this.otlkMagForecast = this.getAbsoluteForecast(); this.cap = getRandomInt(this.price * 1e3, this.price * 25e3); this.spreadPerc = toNumber(p.spreadPerc); - this.priceMovementPerc = this.spreadPerc / (getRandomInt(10, 30) / 10); this.shareTxForMovement = toNumber(p.shareTxForMovement); - this.shareTxUntilMovementDown = this.shareTxForMovement; - this.shareTxUntilMovementUp = this.shareTxForMovement; + this.shareTxUntilMovement = this.shareTxForMovement; // Total shares is determined by market cap, and is rounded to nearest 100k let totalSharesUnrounded: number = (p.marketCap / this.price); diff --git a/src/StockMarket/StockMarket.jsx b/src/StockMarket/StockMarket.jsx index 2d2511026..42fc315a6 100644 --- a/src/StockMarket/StockMarket.jsx +++ b/src/StockMarket/StockMarket.jsx @@ -7,12 +7,6 @@ import { import { Order } from "./Order"; import { processOrders } from "./OrderProcessing"; import { Stock } from "./Stock"; -import { - getBuyTransactionCost, - getSellTransactionGain, - processBuyTransactionPriceMovement, - processSellTransactionPriceMovement -} from "./StockMarketHelpers"; import { getStockMarket4SDataCost, getStockMarket4STixApiCost @@ -205,10 +199,10 @@ export function stockMarketCycle() { stock.flipForecastForecast(); } else if (roll < 0.6) { stock.otlkMagForecast += 0.5; - stock.otlkMagForecast = Math.min(stock.otlkMagForecast * 1.02, 50); + stock.otlkMagForecast = Math.min(stock.otlkMagForecast * 1.02, 100); } else if (roll < 0.8) { stock.otlkMagForecast -= 0.5; - stock.otlkMagForecast = otlkMagForecast * (1 / 1.02); + stock.otlkMagForecast = stock.otlkMagForecast * (1 / 1.02); } } } @@ -276,8 +270,7 @@ export function processStockPrices(numCycles=1) { stock.cycleForecast(otlkMagChange); // Shares required for price movement gradually approaches max over time - stock.shareTxUntilMovementUp = Math.min(stock.shareTxUntilMovementUp + 5, stock.shareTxForMovement); - stock.shareTxUntilMovementDown = Math.min(stock.shareTxUntilMovementDown + 5, stock.shareTxForMovement); + stock.shareTxUntilMovement = Math.min(stock.shareTxUntilMovement + 10, stock.shareTxForMovement); } displayStockMarketContent(); diff --git a/src/StockMarket/StockMarketHelpers.ts b/src/StockMarket/StockMarketHelpers.ts index 6393cd53c..cfb37d9c0 100644 --- a/src/StockMarket/StockMarketHelpers.ts +++ b/src/StockMarket/StockMarketHelpers.ts @@ -6,34 +6,7 @@ import { CONSTANTS } from "../Constants"; export const forecastChangePerPriceMovement = 0.005; /** - * 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. + * Calculate the total cost of a "buy" transaction. This accounts for spread and commission. * @param {Stock} stock - Stock being purchased * @param {number} shares - Number of shares being transacted * @param {PositionTypes} posType - Long or short position @@ -50,135 +23,15 @@ export function getBuyTransactionCost(stock: Stock, shares: number, posType: Pos // If the number of shares doesn't trigger a price movement, its a simple calculation if (isLong) { - if (shares <= stock.shareTxUntilMovementUp) { - return (shares * stock.getAskPrice()) + CONSTANTS.StockMarketCommission; - } + return (shares * stock.getAskPrice()) + CONSTANTS.StockMarketCommission; } else { - if (shares <= stock.shareTxUntilMovementDown) { - return (shares * stock.getBidPrice()) + CONSTANTS.StockMarketCommission; - } - } - - // Calculate how many iterations of price changes we need to account for - const firstShares = isLong ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown; - let remainingShares = shares - firstShares; - 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 = (firstShares * currPrice); - - const increasingMvmt = calculateIncreasingPriceMovement(stock)!; - const decreasingMvmt = calculateDecreasingPriceMovement(stock)!; - - function processPriceMovement() { - if (isLong) { - currPrice *= increasingMvmt; - } else { - currPrice *= decreasingMvmt; - } - } - - for (let i = 1; i < numIterations; ++i) { - processPriceMovement(); - - const amt = Math.min(stock.shareTxForMovement, remainingShares); - totalCost += (amt * currPrice); - remainingShares -= amt; - } - - return totalCost + CONSTANTS.StockMarketCommission; -} - -/** - * Processes a buy transaction's resulting price AND forecast movement. - * @param {Stock} stock - Stock being purchased - * @param {number} shares - Number of shares being transacted - * @param {PositionTypes} posType - Long or short position - */ -export function processBuyTransactionPriceMovement(stock: Stock, shares: number, posType: PositionTypes): void { - if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return; } - - // Cap the 'shares' arg at the stock's maximum shares. This'll prevent - // hanging in the case when a really big number is passed in - shares = Math.min(shares, stock.maxShares); - - const isLong = (posType === PositionTypes.Long); - - let currPrice = stock.price; - function processPriceMovement() { - if (isLong) { - currPrice *= calculateIncreasingPriceMovement(stock)!; - } else { - currPrice *= calculateDecreasingPriceMovement(stock)!; - } - } - - // If there's only going to be one iteration - const firstShares = isLong ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown; - if (shares <= firstShares) { - function triggerMovement() { - processPriceMovement(); - stock.changePrice(currPrice); - stock.otlkMag -= (forecastChangePerPriceMovement); - } - - if (isLong) { - stock.shareTxUntilMovementUp -= shares; - if (stock.shareTxUntilMovementUp <= 0) { - stock.shareTxUntilMovementUp = stock.shareTxForMovement; - triggerMovement(); - } - } else { - stock.shareTxUntilMovementDown -= shares; - if (stock.shareTxUntilMovementDown <= 0) { - stock.shareTxUntilMovementDown = stock.shareTxForMovement; - triggerMovement(); - } - } - - return; - } - - // Calculate how many iterations of price changes we need to account for - let remainingShares = shares - firstShares; - let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement); - - for (let i = 1; i < numIterations; ++i) { - processPriceMovement(); - } - - // If on the offchance we end up perfectly at the next price movement - if (isLong) { - stock.shareTxUntilMovementUp = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovementUp) % stock.shareTxForMovement); - if (stock.shareTxUntilMovementUp === stock.shareTxForMovement || stock.shareTxUntilMovementUp <= 0) { - // The shareTxUntilMovementUp ended up at 0 at the end of the "processing" - ++numIterations; - stock.shareTxUntilMovementUp = stock.shareTxForMovement; - processPriceMovement(); - } - } else { - stock.shareTxUntilMovementDown = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovementDown) % stock.shareTxForMovement); - if (stock.shareTxUntilMovementDown === stock.shareTxForMovement || stock.shareTxUntilMovementDown <= 0) { - // The shareTxUntilMovementDown ended up at 0 at the end of the "processing" - ++numIterations; - stock.shareTxUntilMovementDown = stock.shareTxForMovement; - processPriceMovement(); - } - } - - stock.changePrice(currPrice); - - // Forecast always decreases in magnitude - const forecastChange = Math.min(5, forecastChangePerPriceMovement * (numIterations - 1)); - if (stock.otlkMag > 10) { - stock.otlkMag -= forecastChange; + return (shares * stock.getBidPrice()) + CONSTANTS.StockMarketCommission; } } /** * Calculate the TOTAL amount of money gained from a sale (NOT net profit). This accounts - * for spread, price movements, and commission. + * for spread and commission. * @param {Stock} stock - Stock being sold * @param {number} shares - Number of sharse being transacted * @param {PositionTypes} posType - Long or short position @@ -192,104 +45,39 @@ export function getSellTransactionGain(stock: Stock, shares: number, posType: Po shares = Math.min(shares, stock.maxShares); const isLong = (posType === PositionTypes.Long); - const firstShares = isLong ? stock.shareTxUntilMovementDown : stock.shareTxUntilMovementUp; + if (isLong) { + return (shares * stock.getBidPrice()) - CONSTANTS.StockMarketCommission; + } else { + // Calculating gains for a short position requires calculating the profit made + const origCost = shares * stock.playerAvgShortPx; + const profit = ((stock.playerAvgShortPx - stock.getAskPrice()) * shares) - CONSTANTS.StockMarketCommission; - // If the number of shares doesn't trigger a price mvoement, its a simple calculation - if (shares <= firstShares) { - if (isLong) { - return (shares * stock.getBidPrice()) - CONSTANTS.StockMarketCommission; - } else { - // Calculating gains for a short position requires calculating the profit made - const origCost = shares * stock.playerAvgShortPx; - const profit = ((stock.playerAvgShortPx - stock.getAskPrice()) * shares) - CONSTANTS.StockMarketCommission; - - return origCost + profit; - } + return origCost + profit; } - - // Calculate how many iterations of price changes we need to account for - let remainingShares = shares - firstShares; - let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement); - - // Helper function to calculate gain for a single iteration - function calculateGain(thisPrice: number, thisShares: number) { - if (isLong) { - return thisShares * thisPrice; - } else { - const origCost = thisShares * stock.playerAvgShortPx; - const profit = ((stock.playerAvgShortPx - thisPrice) * thisShares); - - return origCost + profit; - } - } - - // The initial cost calculation takes care of the first "iteration" - let currPrice = isLong ? stock.getBidPrice() : stock.getAskPrice(); - let totalGain = calculateGain(currPrice, firstShares); - 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; - } - - return totalGain - CONSTANTS.StockMarketCommission; } /** - * Processes a sell transaction's resulting price movement + * Processes a stock's change in forecast whenever it is transacted * @param {Stock} stock - Stock being sold * @param {number} shares - Number of sharse being transacted * @param {PositionTypes} posType - Long or short position */ -export function processSellTransactionPriceMovement(stock: Stock, shares: number, posType: PositionTypes): void { +export function processTransactionForecastMovement(stock: Stock, shares: number): void { if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return; } // Cap the 'shares' arg at the stock's maximum shares. This'll prevent // hanging in the case when a really big number is passed in shares = Math.min(shares, stock.maxShares); - const isLong = (posType === PositionTypes.Long); - const firstShares = isLong ? stock.shareTxUntilMovementDown : stock.shareTxUntilMovementUp; - - let currPrice = stock.price; - function processPriceMovement() { - if (isLong) { - currPrice *= calculateDecreasingPriceMovement(stock)!; - } else { - currPrice *= calculateIncreasingPriceMovement(stock)!; - } - } - // If there's only going to be one iteration at most + const firstShares = stock.shareTxUntilMovement; if (shares <= firstShares) { - function triggerPriceMovement() { - processPriceMovement(); - stock.changePrice(currPrice); + stock.shareTxUntilMovement -= shares; + if (stock.shareTxUntilMovement <= 0) { + stock.shareTxUntilMovement = stock.shareTxForMovement; stock.otlkMag -= (forecastChangePerPriceMovement); } - if (isLong) { - stock.shareTxUntilMovementDown -= shares; - if (stock.shareTxUntilMovementDown <= 0) { - stock.shareTxUntilMovementDown = stock.shareTxForMovement; - triggerPriceMovement(); - } - } else { - stock.shareTxUntilMovementUp -= shares; - if (stock.shareTxUntilMovementUp <= 0) { - stock.shareTxUntilMovementUp = stock.shareTxForMovement; - triggerPriceMovement(); - } - } - - return; } @@ -297,28 +85,13 @@ export function processSellTransactionPriceMovement(stock: Stock, shares: number let remainingShares = shares - firstShares; let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement); - for (let i = 1; i < numIterations; ++i) { - processPriceMovement(); - } - // If on the offchance we end up perfectly at the next price movement - if (isLong) { - stock.shareTxUntilMovementDown = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovementDown) % stock.shareTxForMovement); - if (stock.shareTxUntilMovementDown === stock.shareTxForMovement || stock.shareTxUntilMovementDown <= 0) { - ++numIterations; - stock.shareTxUntilMovementDown = stock.shareTxForMovement; - processPriceMovement(); - } - } else { - stock.shareTxUntilMovementUp = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovementUp) % stock.shareTxForMovement); - if (stock.shareTxUntilMovementUp === stock.shareTxForMovement || stock.shareTxUntilMovementUp <= 0) { - ++numIterations; - stock.shareTxUntilMovementUp = stock.shareTxForMovement; - processPriceMovement(); - } + stock.shareTxUntilMovement = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovement) % stock.shareTxForMovement); + if (stock.shareTxUntilMovement === stock.shareTxForMovement || stock.shareTxUntilMovement <= 0) { + ++numIterations; + stock.shareTxUntilMovement = stock.shareTxForMovement; } - stock.changePrice(currPrice); // Forecast always decreases in magnitude const forecastChange = Math.min(5, forecastChangePerPriceMovement * (numIterations - 1)); @@ -341,44 +114,8 @@ export function calculateBuyMaxAmount(stock: Stock, posType: PositionTypes, mone const isLong = (posType === PositionTypes.Long); - const increasingMvmt = calculateIncreasingPriceMovement(stock); - const decreasingMvmt = calculateDecreasingPriceMovement(stock); - if (increasingMvmt == null || decreasingMvmt == null) { return 0; } - let remainingMoney = money - CONSTANTS.StockMarketCommission; let currPrice = isLong ? stock.getAskPrice() : stock.getBidPrice(); - // No price movement - const firstShares = isLong ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown; - const firstIterationCost = firstShares * currPrice; - if (remainingMoney < firstIterationCost) { - return Math.floor(remainingMoney / currPrice); - } - - // We'll avoid any accidental infinite loops by having a hardcoded maximum number of - // iterations - let numShares = firstShares; - remainingMoney -= firstIterationCost; - for (let i = 0; i < 10e3; ++i) { - if (isLong) { - currPrice *= increasingMvmt; - } else { - currPrice *= decreasingMvmt; - } - - const affordableShares = Math.floor(remainingMoney / currPrice); - const actualShares = Math.min(stock.shareTxForMovement, affordableShares); - - // Can't afford any more, so we're done - if (actualShares <= 0) { break; } - - numShares += actualShares; - - let cost = actualShares * currPrice; - remainingMoney -= cost; - - if (remainingMoney <= 0) { break; } - } - - return Math.floor(numShares); + return Math.floor(remainingMoney / currPrice); } diff --git a/src/StockMarket/ui/StockTicker.tsx b/src/StockMarket/ui/StockTicker.tsx index 42389c5e1..69f729111 100644 --- a/src/StockMarket/ui/StockTicker.tsx +++ b/src/StockMarket/ui/StockTicker.tsx @@ -117,12 +117,6 @@ export class StockTicker extends React.Component { let costTxt = `Purchasing ${numeralWrapper.formatBigNumber(qty)} shares (${this.state.position === PositionTypes.Long ? "Long" : "Short"}) ` + `will cost ${numeralWrapper.formatMoney(cost)}. `; - const amtNeededForMovement = this.state.position === PositionTypes.Long ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown; - const causesMovement = qty > amtNeededForMovement; - if (causesMovement) { - costTxt += `WARNING: Purchasing this many shares will influence the stock price`; - } - return costTxt; } @@ -151,12 +145,6 @@ export class StockTicker extends React.Component { let costTxt = `Selling ${numeralWrapper.formatBigNumber(qty)} shares (${this.state.position === PositionTypes.Long ? "Long" : "Short"}) ` + `will result in a gain of ${numeralWrapper.formatMoney(cost)}. `; - const amtNeededForMovement = this.state.position === PositionTypes.Long ? stock.shareTxUntilMovementDown : stock.shareTxUntilMovementUp; - const causesMovement = qty > amtNeededForMovement; - if (causesMovement) { - costTxt += `WARNING: Selling this many shares will influence the stock price`; - } - return costTxt; } @@ -357,11 +345,7 @@ export class StockTicker extends React.Component { render() { // Determine if the player's intended transaction will cause a price movement - let causesMovement: boolean = false; const qty = this.getQuantity(); - if (!isNaN(qty)) { - causesMovement = qty > this.props.stock.shareTxForMovement; - } return (
  • @@ -400,14 +384,6 @@ export class StockTicker extends React.Component { - { - causesMovement && -

    - WARNING: Buying/Selling {numeralWrapper.formatBigNumber(qty)} shares may affect - the stock's price. This applies during the transaction itself as well. See Investopedia - for more details. -

    - }