diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index 1eb841f6b..14fc95e05 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -1648,42 +1648,9 @@ function NetscriptFunctions(workerScript) { if (stock == null) { throw makeRuntimeRejectMsg(workerScript, "Invalid stock symbol passed into buyStock()"); } - if (shares < 0 || isNaN(shares)) { - workerScript.scriptRef.log("ERROR: Invalid 'shares' argument passed to buyStock()"); - return 0; - } - shares = Math.round(shares); - if (shares === 0) {return 0;} + const res = buyStock(stock, shares, workerScript); - // Does player have enough money? - var totalPrice = stock.price * shares; - if (Player.money.lt(totalPrice + CONSTANTS.StockMarketCommission)) { - workerScript.scriptRef.log("Not enough money to purchase " + formatNumber(shares, 0) + " shares of " + - symbol + ". Need $" + - formatNumber(totalPrice + CONSTANTS.StockMarketCommission, 2).toString()); - return 0; - } - - // Would this purchase exceed the maximum number of shares? - if (shares + stock.playerShares + stock.playerShortShares > stock.maxShares) { - workerScript.scriptRef.log(`You cannot purchase this many shares. ${stock.symbol} has a maximum of ` + - `${stock.maxShares} shares.`); - return 0; - } - - var origTotal = stock.playerShares * stock.playerAvgPx; - Player.loseMoney(totalPrice + CONSTANTS.StockMarketCommission); - var newTotal = origTotal + totalPrice; - stock.playerShares += shares; - stock.playerAvgPx = newTotal / stock.playerShares; - if (routing.isOn(Page.StockMarket)) { - updateStockPlayerPosition(stock); - } - if (workerScript.disableLogs.ALL == null && workerScript.disableLogs.buyStock == null) { - workerScript.scriptRef.log("Bought " + formatNumber(shares, 0) + " shares of " + stock.symbol + " at $" + - formatNumber(stock.price, 2) + " per share"); - } - return stock.price; + return res ? stock.price : 0; }, sellStock : function(symbol, shares) { if (workerScript.checkingRam) { @@ -1697,36 +1664,10 @@ function NetscriptFunctions(workerScript) { if (stock == null) { throw makeRuntimeRejectMsg(workerScript, "Invalid stock symbol passed into sellStock()"); } - if (shares < 0 || isNaN(shares)) { - workerScript.scriptRef.log("ERROR: Invalid 'shares' argument passed to sellStock()"); - return 0; - } - shares = Math.round(shares); - if (shares > stock.playerShares) {shares = stock.playerShares;} - if (shares === 0) {return 0;} - var gains = stock.price * shares - CONSTANTS.StockMarketCommission; - Player.gainMoney(gains); - // Calculate net profit and add to script stats - var netProfit = ((stock.price - stock.playerAvgPx) * shares) - CONSTANTS.StockMarketCommission; - if (isNaN(netProfit)) {netProfit = 0;} - workerScript.scriptRef.onlineMoneyMade += netProfit; - Player.scriptProdSinceLastAug += netProfit; - Player.recordMoneySource(netProfit, "stock"); + const res = sellStock(stock, shares, workerScript); - stock.playerShares -= shares; - if (stock.playerShares == 0) { - stock.playerAvgPx = 0; - } - if (routing.isOn(Page.StockMarket)) { - updateStockPlayerPosition(stock); - } - if (workerScript.disableLogs.ALL == null && workerScript.disableLogs.sellStock == null) { - workerScript.scriptRef.log("Sold " + formatNumber(shares, 0) + " shares of " + stock.symbol + " at $" + - formatNumber(stock.price, 2) + " per share. Gained " + - "$" + formatNumber(gains, 2)); - } - return stock.price; + return res ? stock.price : 0; }, shortStock(symbol, shares) { if (workerScript.checkingRam) { @@ -1745,7 +1686,8 @@ function NetscriptFunctions(workerScript) { if (stock == null) { throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid stock symbol passed into shortStock()"); } - var res = shortStock(stock, shares, workerScript); + const res = shortStock(stock, shares, workerScript); + return res ? stock.price : 0; }, sellShort(symbol, shares) { @@ -1765,7 +1707,8 @@ function NetscriptFunctions(workerScript) { if (stock == null) { throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid stock symbol passed into sellShort()"); } - var res = sellShort(stock, shares, workerScript); + const res = sellShort(stock, shares, workerScript); + return res ? stock.price : 0; }, placeOrder(symbol, shares, price, type, pos) { diff --git a/src/Server/data/servers.ts b/src/Server/data/servers.ts index ec661c458..7d75dcc65 100644 --- a/src/Server/data/servers.ts +++ b/src/Server/data/servers.ts @@ -1,23 +1,7 @@ // tslint:disable:max-file-line-count // This could actually be a JSON file as it should be constant metadata to be imported... - -/** - * Defines the minimum and maximum values for a range. - * It is up to the consumer if these values are inclusive or exclusive. - * It is up to the implementor to ensure max > min. - */ -interface IMinMaxRange { - /** - * The maximum bound of the range. - */ - max: number; - - /** - * The minimum bound of the range. - */ - min: number; -} +import { IMinMaxRange } from "../../types"; /** * The metadata describing the base state of servers on the network. diff --git a/src/StockMarket/Order.ts b/src/StockMarket/Order.ts new file mode 100644 index 000000000..dc65be16c --- /dev/null +++ b/src/StockMarket/Order.ts @@ -0,0 +1,45 @@ +/** + * Represents a Limit or Buy Order on the stock market. Does not represent + * a Market Order since those are just executed immediately + */ +import { Stock } from "./Stock"; +import { OrderTypes } from "./data/OrderTypes"; +import { PositionTypes } from "./data/PositionTypes"; + +import { + Generic_fromJSON, + Generic_toJSON, + Reviver +} from "../../utils/JSONReviver"; + +export class Order { + /** + * Initializes a Order from a JSON save state + */ + static fromJSON(value: any): Order { + return Generic_fromJSON(Order, value.data); + } + + readonly pos: PositionTypes; + readonly price: number; + readonly shares: number; + readonly stock: Stock; + readonly type: OrderTypes; + + constructor(stk: Stock = new Stock(), shares: number=0, price: number=0, typ: OrderTypes=OrderTypes.LimitBuy, pos: PositionTypes=PositionTypes.Long) { + this.stock = stk; + this.shares = shares; + this.price = price; + this.type = typ; + this.pos = pos; + } + + /** + * Serialize the Order to a JSON save state. + */ + toJSON(): any { + return Generic_toJSON("Order", this); + } +} + +Reviver.constructors.Order = Order; diff --git a/src/StockMarket/Stock.ts b/src/StockMarket/Stock.ts index 6030434a8..401d250b1 100644 --- a/src/StockMarket/Stock.ts +++ b/src/StockMarket/Stock.ts @@ -1,6 +1,57 @@ -import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; +import { IMinMaxRange } from "../types"; +import { + Generic_fromJSON, + Generic_toJSON, + Reviver +} from "../../utils/JSONReviver"; import { getRandomInt } from "../../utils/helpers/getRandomInt"; +export interface IConstructorParams { + b: boolean; + initPrice: number | IMinMaxRange; + marketCap: number; + mv: number | IMinMaxRange; + name: string; + otlkMag: number; + spreadPerc: number | IMinMaxRange; + shareTxForMovement: number | IMinMaxRange; + symbol: string; +} + +const defaultConstructorParams: IConstructorParams = { + b: true, + initPrice: 10e3, + marketCap: 1e12, + mv: 1, + name: "", + otlkMag: 0, + spreadPerc: 0, + shareTxForMovement: 1e6, + symbol: "", +} + +// Helper function that convert a IMinMaxRange to a number +function toNumber(n: number | IMinMaxRange): number { + let value: number; + switch (typeof n) { + case "number": { + return n; + } + case "object": { + value = getRandomInt(n.min, n.max); + break; + } + default: + throw Error(`Do not know how to convert the type '${typeof n}' to a number`); + } + + if (typeof n === "object" && typeof n.divisor === "number") { + return value / n.divisor; + } + + return value; +} + /** * Represents the valuation of a company in the World Stock Exchange. */ @@ -73,6 +124,28 @@ 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 + */ + readonly shareTxForMovement: number; + + /** + * How many share transactions remaining until a price movement occurs + */ + shareTxUntilMovement: number; + + /** + * Spread percentage. The bid/ask prices for this stock are N% above or below + * the "real price" to emulate spread. + */ + readonly spreadPerc: number; + /** * The stock's ticker symbol */ @@ -85,36 +158,48 @@ export class Stock { */ readonly totalShares: number; - constructor(name: string = "", - symbol: string = "", - mv: number = 1, - b: boolean = true, - otlkMag: number = 0, - initPrice: number = 10e3, - marketCap: number = 1e12) { - this.name = name; - this.symbol = symbol; - this.price = initPrice; - this.playerShares = 0; - this.playerAvgPx = 0; - this.playerShortShares = 0; - this.playerAvgShortPx = 0; - this.mv = mv; - this.b = b; - this.otlkMag = otlkMag; - this.cap = getRandomInt(initPrice * 1e3, initPrice * 25e3); + constructor(p: IConstructorParams = defaultConstructorParams) { + this.name = p.name; + this.symbol = p.symbol; + this.price = toNumber(p.initPrice); + this.playerShares = 0; + this.playerAvgPx = 0; + this.playerShortShares = 0; + this.playerAvgShortPx = 0; + this.mv = toNumber(p.mv); + this.b = p.b; + this.otlkMag = p.otlkMag; + 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.shareTxUntilMovement = this.shareTxForMovement; // Total shares is determined by market cap, and is rounded to nearest 100k - let totalSharesUnrounded: number = (marketCap / initPrice); + let totalSharesUnrounded: number = (p.marketCap / this.price); this.totalShares = Math.round(totalSharesUnrounded / 1e5) * 1e5; // Max Shares (Outstanding shares) is a percentage of total shares - const outstandingSharePercentage: number = 0.2; + const outstandingSharePercentage: number = 0.15; this.maxShares = Math.round((this.totalShares * outstandingSharePercentage) / 1e5) * 1e5; this.posTxtEl = null; } + /** + * Return the price at which YOUR stock is bought (market ask price). Accounts for spread + */ + getAskPrice(): number { + return this.price * (1 + (this.spreadPerc / 100)); + } + + /** + * Return the price at which YOUR stock is sold (market bid price). Accounts for spread + */ + getBidPrice(): number { + return this.price * (1 - (this.spreadPerc / 100)); + } + /** * Serialize the Stock to a JSON save state. */ diff --git a/src/StockMarket/StockMarket.js b/src/StockMarket/StockMarket.js index 5b84da52d..d705fd6fb 100644 --- a/src/StockMarket/StockMarket.js +++ b/src/StockMarket/StockMarket.js @@ -1,20 +1,31 @@ -import {Stock} from "./Stock"; -import { getStockMarket4SDataCost, - getStockMarket4STixApiCost } from "./StockMarketCosts"; +import { + Order, + OrderTypes, + PositionTypes +} from "./Order"; +import { Stock } from "./Stock"; +import { + getStockMarket4SDataCost, + getStockMarket4STixApiCost +} from "./StockMarketCosts"; +import { StockSymbols } from "./data/StockSymbols"; -import {CONSTANTS} from "../Constants"; -import { LocationName } from "../Locations/data/LocationNames"; -import {hasWallStreetSF, wallStreetSFLvl} from "../NetscriptFunctions"; -import {WorkerScript} from "../NetscriptWorker"; -import {Player} from "../Player"; +import { CONSTANTS } from "../Constants"; +import { LocationName } from "../Locations/data/LocationNames"; +import { hasWallStreetSF, wallStreetSFLvl } from "../NetscriptFunctions"; +import { WorkerScript } from "../NetscriptWorker"; +import { Player } from "../Player"; -import {Page, routing} from ".././ui/navigationTracking"; -import {numeralWrapper} from ".././ui/numeralFormat"; +import { Page, routing } from ".././ui/navigationTracking"; +import { numeralWrapper } from ".././ui/numeralFormat"; -import {dialogBoxCreate} from "../../utils/DialogBox"; -import {clearEventListeners} from "../../utils/uiHelpers/clearEventListeners"; -import {Reviver, Generic_toJSON, - Generic_fromJSON} from "../../utils/JSONReviver"; +import { dialogBoxCreate } from "../../utils/DialogBox"; +import { clearEventListeners } from "../../utils/uiHelpers/clearEventListeners"; +import { + Reviver, + Generic_toJSON, + Generic_fromJSON +} from "../../utils/JSONReviver"; import {exceptionAlert} from "../../utils/helpers/exceptionAlert"; import {getRandomInt} from "../../utils/helpers/getRandomInt"; import {KEY} from "../../utils/helpers/keyCodes"; @@ -27,19 +38,7 @@ import {yesNoBoxCreate, yesNoTxtInpBoxCreate, yesNoTxtInpBoxGetInput, yesNoBoxClose, yesNoTxtInpBoxClose, yesNoBoxOpen} from "../../utils/YesNoBox"; -var OrderTypes = { - LimitBuy: "Limit Buy Order", - LimitSell: "Limit Sell Order", - StopBuy: "Stop Buy Order", - StopSell: "Stop Sell Order" -} - -var PositionTypes = { - Long: "L", - Short: "S" -} - -function placeOrder(stock, shares, price, type, position, workerScript=null) { +export function placeOrder(stock, shares, price, type, position, workerScript=null) { var tixApi = (workerScript instanceof WorkerScript); var order = new Order(stock, shares, price, type, position); if (isNaN(shares) || isNaN(price)) { @@ -51,12 +50,12 @@ function placeOrder(stock, shares, price, type, position, workerScript=null) { return false; } if (StockMarket["Orders"] == null) { - var orders = {}; - for (var name in StockMarket) { + const orders = {}; + for (const name in StockMarket) { if (StockMarket.hasOwnProperty(name)) { - var stock = StockMarket[name]; - if (!(stock instanceof Stock)) {continue;} - orders[stock.symbol] = []; + const stk = StockMarket[name]; + if (!(stk instanceof Stock)) { continue; } + orders[stk.symbol] = []; } } StockMarket["Orders"] = orders; @@ -68,8 +67,8 @@ function placeOrder(stock, shares, price, type, position, workerScript=null) { return true; } -//Returns true if successfully cancels an order, false otherwise -function cancelOrder(params, workerScript=null) { +// Returns true if successfully cancels an order, false otherwise +export function cancelOrder(params, workerScript=null) { var tixApi = (workerScript instanceof WorkerScript); if (StockMarket["Orders"] == null) {return false;} if (params.order && params.order instanceof Order) { @@ -153,34 +152,20 @@ function executeOrder(order) { } } -function Order(stock, shares, price, type, position) { - this.stock = stock; - this.shares = shares; - this.price = price; - this.type = type; - this.pos = position; -} - -Order.prototype.toJSON = function() { - return Generic_toJSON("Order", this); -} - -Order.fromJSON = function(value) { - return Generic_fromJSON(Order, value.data); -} - -Reviver.constructors.Order = Order; - -let StockMarket = {} //Full name to stock object -let StockSymbols = {} //Full name to symbol -let SymbolToStockMap = {}; //Symbol to Stock object +export let StockMarket = {}; // Maps full stock name -> Stock object +export let SymbolToStockMap = {}; // Maps symbol -> Stock object let formatHelpData = { longestName: 0, longestSymbol: 0, }; -function loadStockMarket(saveString) { +for (const key in StockSymbols) { + formatHelpData.longestName = key.length > formatHelpData.longestName ? key.length : formatHelpData.longestName; + formatHelpData.longestSymbol = StockSymbols[key].length > formatHelpData.longestSymbol ? StockSymbols[key].length : formatHelpData.longestSymbol; +} + +export function loadStockMarket(saveString) { if (saveString === "") { StockMarket = {}; } else { @@ -188,51 +173,7 @@ function loadStockMarket(saveString) { } } -function initStockSymbols() { - //Stocks for companies at which you can work - StockSymbols[LocationName.AevumECorp] = "ECP"; - StockSymbols[LocationName.Sector12MegaCorp] = "MGCP"; - StockSymbols[LocationName.Sector12BladeIndustries] = "BLD"; - StockSymbols[LocationName.AevumClarkeIncorporated] = "CLRK"; - StockSymbols[LocationName.VolhavenOmniTekIncorporated] = "OMTK"; - StockSymbols[LocationName.Sector12FourSigma] = "FSIG"; - StockSymbols[LocationName.ChongqingKuaiGongInternational] = "KGI"; - StockSymbols[LocationName.AevumFulcrumTechnologies] = "FLCM"; - StockSymbols[LocationName.IshimaStormTechnologies] = "STM"; - StockSymbols[LocationName.NewTokyoDefComm] = "DCOMM"; - StockSymbols[LocationName.VolhavenHeliosLabs] = "HLS"; - StockSymbols[LocationName.NewTokyoVitaLife] = "VITA"; - StockSymbols[LocationName.Sector12IcarusMicrosystems] = "ICRS"; - StockSymbols[LocationName.Sector12UniversalEnergy] = "UNV"; - StockSymbols[LocationName.AevumAeroCorp] = "AERO"; - StockSymbols[LocationName.VolhavenOmniaCybersystems] = "OMN"; - StockSymbols[LocationName.ChongqingSolarisSpaceSystems] = "SLRS"; - StockSymbols[LocationName.NewTokyoGlobalPharmaceuticals] = "GPH"; - StockSymbols[LocationName.IshimaNovaMedical] = "NVMD"; - StockSymbols[LocationName.AevumWatchdogSecurity] = "WDS"; - StockSymbols[LocationName.VolhavenLexoCorp] = "LXO"; - StockSymbols[LocationName.AevumRhoConstruction] = "RHOC"; - StockSymbols[LocationName.Sector12AlphaEnterprises] = "APHE"; - StockSymbols[LocationName.VolhavenSysCoreSecurities] = "SYSC"; - StockSymbols[LocationName.VolhavenCompuTek] = "CTK"; - StockSymbols[LocationName.AevumNetLinkTechnologies] = "NTLK"; - StockSymbols[LocationName.IshimaOmegaSoftware] = "OMGA"; - StockSymbols[LocationName.Sector12FoodNStuff] = "FNS"; - - //Stocks for other companies - StockSymbols["Sigma Cosmetics"] = "SGC"; - StockSymbols["Joes Guns"] = "JGN"; - StockSymbols["Catalyst Ventures"] = "CTYS"; - StockSymbols["Microdyne Technologies"] = "MDYN"; - StockSymbols["Titan Laboratories"] = "TITN"; - - for (const key in StockSymbols) { - formatHelpData.longestName = key.length > formatHelpData.longestName ? key.length : formatHelpData.longestName; - formatHelpData.longestSymbol = StockSymbols[key].length > formatHelpData.longestSymbol ? StockSymbols[key].length : formatHelpData.longestSymbol; - } -} - -function initStockMarket() { +export function initStockMarket() { for (var stk in StockMarket) { if (StockMarket.hasOwnProperty(stk)) { delete StockMarket[stk]; @@ -387,7 +328,7 @@ function initStockMarket() { StockMarket.lastUpdate = 0; } -function initSymbolToStockMap() { +export function initSymbolToStockMap() { for (var name in StockSymbols) { if (StockSymbols.hasOwnProperty(name)) { var stock = StockMarket[name]; @@ -401,7 +342,7 @@ function initSymbolToStockMap() { } } -function stockMarketCycle() { +export function stockMarketCycle() { for (var name in StockMarket) { if (StockMarket.hasOwnProperty(name)) { var stock = StockMarket[name]; @@ -415,78 +356,151 @@ function stockMarketCycle() { } } -//Returns true if successful, false otherwise -function buyStock(stock, shares) { +/** + * Attempt to buy a stock in the long position + * @param {Stock} stock - Stock to buy + * @param {number} shares - Number of shares to buy + * @param {WorkerScript} workerScript - If this is being called through Netscript + * @returns {boolean} - true if successful, false otherwise + */ +export function buyStock(stock, shares, workerScript=null) { + const tixApi = (workerScript instanceof WorkerScript); + // Validate arguments shares = Math.round(shares); if (shares == 0 || shares < 0) { return false; } if (stock == null || isNaN(shares)) { - dialogBoxCreate("Failed to buy stock. This may be a bug, contact developer"); + if (tixApi) { + workerScript.log(`ERROR: buyStock() failed due to invalid arguments`); + } else { + dialogBoxCreate("Failed to buy stock. This may be a bug, contact developer"); + } + return false; } // Does player have enough money? - var totalPrice = stock.price * shares; + const totalPrice = stock.price * shares; if (Player.money.lt(totalPrice + CONSTANTS.StockMarketCommission)) { - dialogBoxCreate("You do not have enough money to purchase this. You need " + - numeralWrapper.format(totalPrice + CONSTANTS.StockMarketCommission, '($0.000a)') + "."); + if (tixApi) { + workerScript.log(`ERROR: buyStock() failed because you do not have enough money to purchase this potiion. You need ${numeralWrapper.formatMoney(totalPrice + CONSTANTS.StockMarketCommission)}`); + } else { + dialogBoxCreate(`You do not have enough money to purchase this. You need ${numeralWrapper.formatMoney(totalPrice + CONSTANTS.StockMarketCommission)}`); + } + return false; } // Would this purchase exceed the maximum number of shares? if (shares + stock.playerShares + stock.playerShortShares > stock.maxShares) { - dialogBoxCreate(`You cannot purchase this many shares. ${stock.symbol} has a maximum of ` + - `${numeralWrapper.formatBigNumber(stock.maxShares)} shares.`); + if (tixApi) { + workerScript.log(`ERROR: buyStock() failed because purchasing this many shares would exceed ${stock.symbol}'s maximum number of shares`); + } else { + dialogBoxCreate(`You cannot purchase this many shares. ${stock.symbol} has a maximum of ${numeralWrapper.formatBigNumber(stock.maxShares)} shares.`); + } + return false; } - var origTotal = stock.playerShares * stock.playerAvgPx; + const origTotal = stock.playerShares * stock.playerAvgPx; Player.loseMoney(totalPrice + CONSTANTS.StockMarketCommission); - var newTotal = origTotal + totalPrice; + const newTotal = origTotal + totalPrice; stock.playerShares = Math.round(stock.playerShares + shares); stock.playerAvgPx = newTotal / stock.playerShares; updateStockPlayerPosition(stock); - dialogBoxCreate("Bought " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + - numeralWrapper.format(stock.price, '($0.000a)') + " per share. Paid " + - numeralWrapper.format(CONSTANTS.StockMarketCommission, '($0.000a)') + " in commission fees."); + if (tixApi) { + if (workerScript.shouldLog("buyStock")) { + workerScript.log( + "Bought " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + + numeralWrapper.format(stock.price, '($0.000a)') + " per share. Paid " + + numeralWrapper.format(CONSTANTS.StockMarketCommission, '($0.000a)') + " in commission fees." + ); + } + } else { + dialogBoxCreate( + "Bought " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + + numeralWrapper.format(stock.price, '($0.000a)') + " per share. Paid " + + numeralWrapper.format(CONSTANTS.StockMarketCommission, '($0.000a)') + " in commission fees." + ); + } + return true; } -//Returns true if successful and false otherwise -function sellStock(stock, shares) { - if (shares == 0) {return false;} +/** + * Attempt to sell a stock in the long position + * @param {Stock} stock - Stock to sell + * @param {number} shares - Number of shares to sell + * @param {WorkerScript} workerScript - If this is being called through Netscript + * returns {boolean} - true if successfully sells given number of shares OR MAX owned, false otherwise + */ +export function sellStock(stock, shares, workerScript=null) { + const tixApi = (workerScript instanceof WorkerScript); + + // Sanitize/Validate arguments if (stock == null || shares < 0 || isNaN(shares)) { - dialogBoxCreate("Failed to sell stock. This may be a bug, contact developer"); + if (tixApi) { + workerScript.log(`ERROR: sellStock() failed due to invalid arguments`); + } else { + dialogBoxCreate("Failed to sell stock. This is probably due to an invalid quantity. Otherwise, this may be a bug, contact developer"); + } + return false; } shares = Math.round(shares); if (shares > stock.playerShares) {shares = stock.playerShares;} if (shares === 0) {return false;} + const gains = stock.price * shares - CONSTANTS.StockMarketCommission; - const netProfit = ((stock.price - stock.playerAvgPx) * shares) - CONSTANTS.StockMarketCommission; + let netProfit = ((stock.price - stock.playerAvgPx) * shares) - CONSTANTS.StockMarketCommission; + if (isNaN(netProfit)) { netProfit = 0; } Player.gainMoney(gains); Player.recordMoneySource(netProfit, "stock"); + if (tixApi) { + workerScript.scriptRef.onlineMoneyMade += netProfit; + Player.scriptProdSinceLastAug += netProfit; + } + stock.playerShares = Math.round(stock.playerShares - shares); - if (stock.playerShares == 0) { + if (stock.playerShares === 0) { stock.playerAvgPx = 0; } updateStockPlayerPosition(stock); - dialogBoxCreate("Sold " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + - numeralWrapper.format(stock.price, '($0.000a)') + " per share. After commissions, you gained " + - "a total of " + numeralWrapper.format(gains, '($0.000a)') + "."); + if (tixApi) { + if (workerScript.shouldLog("sellStock")) { + workerScript.log( + "Sold " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + + numeralWrapper.format(stock.price, '($0.000a)') + " per share. After commissions, you gained " + + "a total of " + numeralWrapper.format(gains, '($0.000a)') + "." + ); + } + } else { + dialogBoxCreate( + "Sold " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + + numeralWrapper.format(stock.price, '($0.000a)') + " per share. After commissions, you gained " + + "a total of " + numeralWrapper.format(gains, '($0.000a)') + "." + ); + } + return true; } -//Returns true if successful and false otherwise -function shortStock(stock, shares, workerScript=null) { - var tixApi = (workerScript instanceof WorkerScript); +/** + * Attempt to buy a stock in the short position + * @param {Stock} stock - Stock to sell + * @param {number} shares - Number of shares to short + * @param {WorkerScript} workerScript - If this is being called through Netscript + * @returns {boolean} - true if successful, false otherwise + */ +export function shortStock(stock, shares, workerScript=null) { + const tixApi = (workerScript instanceof WorkerScript); // Validate arguments shares = Math.round(shares); if (shares === 0 || shares < 0) { return false; } if (stock == null || isNaN(shares)) { if (tixApi) { - workerScript.scriptRef.log("ERROR: shortStock() failed because of invalid arguments."); + workerScript.log("ERROR: shortStock() failed because of invalid arguments."); } else { dialogBoxCreate("Failed to initiate a short position in a stock. This is probably " + "due to an invalid quantity. Otherwise, this may be a bug, so contact developer"); @@ -495,15 +509,15 @@ function shortStock(stock, shares, workerScript=null) { } // Does the player have enough money? - var totalPrice = stock.price * shares; + const totalPrice = stock.price * shares; if (Player.money.lt(totalPrice + CONSTANTS.StockMarketCommission)) { if (tixApi) { - workerScript.scriptRef.log("ERROR: shortStock() failed because you do not have enough " + - "money to purchase this short position. You need " + - numeralWrapper.format(totalPrice + CONSTANTS.StockMarketCommission, '($0.000a)') + "."); + workerScript.log("ERROR: shortStock() failed because you do not have enough " + + "money to purchase this short position. You need " + + numeralWrapper.formatMoney(totalPrice + CONSTANTS.StockMarketCommission)); } else { dialogBoxCreate("You do not have enough money to purchase this short position. You need " + - numeralWrapper.format(totalPrice + CONSTANTS.StockMarketCommission, '($0.000a)') + "."); + numeralWrapper.formatMoney(totalPrice + CONSTANTS.StockMarketCommission)); } return false; @@ -512,55 +526,66 @@ function shortStock(stock, shares, workerScript=null) { // Would this purchase exceed the maximum number of shares? if (shares + stock.playerShares + stock.playerShortShares > stock.maxShares) { if (tixApi) { - workerScript.scriptRef.log("ERROR: shortStock() failed because purchasing this many short shares would exceed " + - `${stock.symbol}'s maximum number of shares.`); + workerScript.log(`ERROR: shortStock() failed because purchasing this many short shares would exceed ${stock.symbol}'s maximum number of shares.`); } else { - dialogBoxCreate(`You cannot purchase this many shares. ${stock.symbol} has a maximum of ` + - `${stock.maxShares} shares.`); + dialogBoxCreate(`You cannot purchase this many shares. ${stock.symbol} has a maximum of ${stock.maxShares} shares.`); } return false; } - var origTotal = stock.playerShortShares * stock.playerAvgShortPx; + const origTotal = stock.playerShortShares * stock.playerAvgShortPx; Player.loseMoney(totalPrice + CONSTANTS.StockMarketCommission); - var newTotal = origTotal + totalPrice; + const newTotal = origTotal + totalPrice; stock.playerShortShares = Math.round(stock.playerShortShares + shares); stock.playerAvgShortPx = newTotal / stock.playerShortShares; updateStockPlayerPosition(stock); if (tixApi) { if (workerScript.disableLogs.ALL == null && workerScript.disableLogs.shortStock == null) { - workerScript.scriptRef.log("Bought a short position of " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + - numeralWrapper.format(stock.price, '($0.000a)') + " per share. Paid " + - numeralWrapper.format(CONSTANTS.StockMarketCommission, '($0.000a)') + " in commission fees."); + workerScript.log( + "Bought a short position of " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + + numeralWrapper.format(stock.price, '($0.000a)') + " per share. Paid " + + numeralWrapper.format(CONSTANTS.StockMarketCommission, '($0.000a)') + " in commission fees." + ); } } else { - dialogBoxCreate("Bought a short position of " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + - numeralWrapper.format(stock.price, '($0.000a)') + " per share. Paid " + - numeralWrapper.format(CONSTANTS.StockMarketCommission, '($0.000a)') + " in commission fees."); + dialogBoxCreate( + "Bought a short position of " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + + numeralWrapper.format(stock.price, '($0.000a)') + " per share. Paid " + + numeralWrapper.format(CONSTANTS.StockMarketCommission, '($0.000a)') + " in commission fees." + ); } + return true; } -//Returns true if successful and false otherwise -function sellShort(stock, shares, workerScript=null) { - var tixApi = (workerScript instanceof WorkerScript); +/** + * Attempt to sell a stock in the short position + * @param {Stock} stock - Stock to sell + * @param {number} shares - Number of shares to sell + * @param {WorkerScript} workerScript - If this is being called through Netscript + * @returns {boolean} true if successfully sells given amount OR max owned, false otherwise + */ +export function sellShort(stock, shares, workerScript=null) { + const tixApi = (workerScript instanceof WorkerScript); + if (stock == null || isNaN(shares) || shares < 0) { if (tixApi) { - workerScript.scriptRef.log("ERROR: sellShort() failed because of invalid arguments."); + workerScript.log("ERROR: sellShort() failed because of invalid arguments."); } else { dialogBoxCreate("Failed to sell a short position in a stock. This is probably " + "due to an invalid quantity. Otherwise, this may be a bug, so contact developer"); } + return false; } shares = Math.round(shares); if (shares > stock.playerShortShares) {shares = stock.playerShortShares;} if (shares === 0) {return false;} - var origCost = shares * stock.playerAvgShortPx; - var profit = ((stock.playerAvgShortPx - stock.price) * shares) - CONSTANTS.StockMarketCommission; - if (isNaN(profit)) {profit = 0;} + const origCost = shares * stock.playerAvgShortPx; + let profit = ((stock.playerAvgShortPx - stock.price) * shares) - CONSTANTS.StockMarketCommission; + if (isNaN(profit)) { profit = 0; } Player.gainMoney(origCost + profit); Player.recordMoneySource(profit, "stock"); if (tixApi) { @@ -574,21 +599,25 @@ function sellShort(stock, shares, workerScript=null) { } updateStockPlayerPosition(stock); if (tixApi) { - if (workerScript.disableLogs.ALL == null && workerScript.disableLogs.sellShort == null) { - workerScript.scriptRef.log("Sold your short position of " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + - numeralWrapper.format(stock.price, '($0.000a)') + " per share. After commissions, you gained " + - "a total of " + numeralWrapper.format(origCost + profit, '($0.000a)') + "."); + if (workerScript.shouldLog("sellShort")) { + workerScript.log( + "Sold your short position of " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + + numeralWrapper.format(stock.price, '($0.000a)') + " per share. After commissions, you gained " + + "a total of " + numeralWrapper.format(origCost + profit, '($0.000a)') + "." + ); } } else { - dialogBoxCreate("Sold your short position of " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + - numeralWrapper.format(stock.price, '($0.000a)') + " per share. After commissions, you gained " + - "a total of " + numeralWrapper.format(origCost + profit, '($0.000a)') + "."); + dialogBoxCreate( + "Sold your short position of " + numeralWrapper.format(shares, '0,0') + " shares of " + stock.symbol + " at " + + numeralWrapper.format(stock.price, '($0.000a)') + " per share. After commissions, you gained " + + "a total of " + numeralWrapper.format(origCost + profit, '($0.000a)') + "." + ); } return true; } -function processStockPrices(numCycles=1) { +export function processStockPrices(numCycles=1) { if (StockMarket.storedCycles == null || isNaN(StockMarket.storedCycles)) { StockMarket.storedCycles = 0; } StockMarket.storedCycles += numCycles; @@ -726,14 +755,15 @@ function processOrders(stock, orderType, posType) { } } -function setStockMarketContentCreated(b) { +export function setStockMarketContentCreated(b) { stockMarketContentCreated = b; } var stockMarketContentCreated = false; var stockMarketPortfolioMode = false; var COMM = CONSTANTS.StockMarketCommission; -function displayStockMarketContent() { +export function displayStockMarketContent() { + // Backwards compatibility if (Player.hasWseAccount == null) {Player.hasWseAccount = false;} if (Player.hasTixApiAccess == null) {Player.hasTixApiAccess = false;} if (Player.has4SData == null) {Player.has4SData = false;} @@ -1341,9 +1371,9 @@ function setStockTickerClickHandlers() { } } -//'increase' argument is a boolean indicating whether the price increased or decreased -function updateStockTicker(stock, increase) { - if (!routing.isOn(Page.StockMarket)) {return;} +// 'increase' argument is a boolean indicating whether the price increased or decreased +export function updateStockTicker(stock, increase) { + if (!routing.isOn(Page.StockMarket)) { return; } if (!(stock instanceof Stock)) { console.log("Invalid stock in updateStockTicker():"); console.log(stock); @@ -1378,8 +1408,8 @@ function updateStockTicker(stock, increase) { } } -function updateStockPlayerPosition(stock) { - if (!routing.isOn(Page.StockMarket)) {return;} +export function updateStockPlayerPosition(stock) { + if (!routing.isOn(Page.StockMarket)) { return; } if (!(stock instanceof Stock)) { console.log("Invalid stock in updateStockPlayerPosition():"); console.log(stock); @@ -1395,8 +1425,8 @@ function updateStockPlayerPosition(stock) { removeElementById(tickerId + "-panel"); return; } else { - //If the ticker hasn't been created, create it (handles updating) - //If it has been created, continue normally + // If the ticker hasn't been created, create it (handles updating) + // If it has been created, continue normally if (document.getElementById(tickerId + "-hdr") == null) { createStockTicker(stock); setStockTickerClickHandlers(); @@ -1413,7 +1443,7 @@ function updateStockPlayerPosition(stock) { return; } - //Calculate returns + // Calculate returns const totalCost = stock.playerShares * stock.playerAvgPx; let gains = (stock.price - stock.playerAvgPx) * stock.playerShares; let percentageGains = gains / totalCost; @@ -1426,6 +1456,8 @@ function updateStockPlayerPosition(stock) { stock.posTxtEl.innerHTML = `Max Shares: ${numeralWrapper.format(stock.maxShares, "0.000a")}
` + + `

Ask Price: ${numeralWrapper.formatMoney(stock.getAskPrice())}See Investopedia for details on what this is


` + + `

Bid Price: ${numeralWrapper.formatMoney(stock.getBidPrice())}See Investopedia for details on what this is


` + "

Long Position: " + "Shares in the long position will increase " + "in value if the price of the corresponding stock increases

" + @@ -1493,7 +1525,7 @@ function updateStockOrderList(stock) { } } - //Remove everything from list + // Remove everything from list while (orderList.firstChild) { orderList.removeChild(orderList.firstChild); } @@ -1522,9 +1554,3 @@ function updateStockOrderList(stock) { } } - -export {StockMarket, StockSymbols, SymbolToStockMap, initStockSymbols, - initStockMarket, initSymbolToStockMap, stockMarketCycle, buyStock, - sellStock, shortStock, sellShort, processStockPrices, displayStockMarketContent, - updateStockTicker, updateStockPlayerPosition, loadStockMarket, - setStockMarketContentCreated, placeOrder, cancelOrder, Order, OrderTypes, PositionTypes}; diff --git a/src/StockMarket/StockMarketHelpers.ts b/src/StockMarket/StockMarketHelpers.ts new file mode 100644 index 000000000..ec985fa46 --- /dev/null +++ b/src/StockMarket/StockMarketHelpers.ts @@ -0,0 +1,109 @@ +import { Stock } from "./Stock"; +import { PositionTypes } from "./data/PositionTypes"; +import { CONSTANTS } from "../Constants"; + +/** + * Calculate the total cost of a "buy" transaction. This accounts for spread, + * price movements, and commission. + * @param {Stock} stock - Stock being purchased + * @param {number} shares - Number of shares being transacted + * @param {PositionTypes} posType - Long or short position + * @returns {number | null} Total transaction cost. Returns null for an invalid transaction + */ +export function getBuyTransactionCost(stock: Stock, shares: number, posType: PositionTypes): number | null { + if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return null; } + + const isLong = (posType === PositionTypes.Long); + + // If the number of shares doesn't trigger a price movement, its a simple calculation + if (shares <= stock.shareTxUntilMovement) { + if (isLong) { + return (shares * stock.getAskPrice()) + CONSTANTS.StockMarketCommission; + } else { + return (shares * stock.getBidPrice()) + CONSTANTS.StockMarketCommission; + } + } + + // 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); + + // The initial cost calculation takes care of the first "iteration" + let currPrice = isLong ? stock.getAskPrice() : stock.getBidPrice(); + let totalCost = (stock.shareTxUntilMovement * currPrice); + for (let i = 1; i < numIterations; ++i) { + 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; +} + +/** + * Calculate the TOTAL amount of money gained from a sale (NOT net profit). This accounts + * for spread, price movements, and commission. + * @param {Stock} stock - Stock being sold + * @param {number} shares - Number of sharse being transacted + * @param {PositionTypes} posType - Long or short position + * @returns {number | null} Amount of money gained from transaction. Returns null for an invalid transaction + */ +export function getSellTransactionGain(stock: Stock, shares: number, posType: PositionTypes): number | null { + if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return null; } + + const isLong = (posType === PositionTypes.Long); + + // If the number of shares doesn't trigger a price mvoement, its a simple calculation + if (shares <= stock.shareTxUntilMovement) { + 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; + } + } + + // Calculate how many iterations of price changes we need to accoutn for + let remainingShares = shares - stock.shareTxUntilMovement; + 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, stock.shareTxUntilMovement); + for (let i = 1; i < numIterations; ++i) { + 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; +} diff --git a/src/StockMarket/data/InitStockMetadata.ts b/src/StockMarket/data/InitStockMetadata.ts new file mode 100644 index 000000000..ab0ce50ea --- /dev/null +++ b/src/StockMarket/data/InitStockMetadata.ts @@ -0,0 +1,873 @@ +/** + * Initialization metadata for all Stocks. This is used to generate the + * stock parameter values upon a reset + * + * Some notes: + * - Megacorporations have better otlkMags + * - Higher volatility -> Bigger spread + * - Lower price -> Bigger spread + * - Share tx required for movement used for balancing + */ +import { StockSymbols } from "./StockSymbols"; +import { IConstructorParams } from "../Stock"; +import { LocationName } from "../../Locations/data/LocationNames"; + +export const InitStockMetadata: IConstructorParams[] = [ + { + b: true, + initPrice: { + max: 28e3, + min: 17e3, + }, + marketCap: 2.4e12, + mv: { + divisor: 100, + max: 50, + min: 40, + }, + name: LocationName.AevumECorp, + otlkMag: 19, + spreadPerc: { + divisor: 10, + max: 5, + min: 1, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.AevumECorp], + }, + + { + b: true, + initPrice: { + max: 34e3, + min: 24e3, + }, + marketCap: 2.4e12, + mv: { + divisor: 100, + max: 50, + min: 40, + }, + name: LocationName.Sector12MegaCorp, + otlkMag: 19, + spreadPerc: { + divisor: 10, + max: 5, + min: 1, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.Sector12MegaCorp], + }, + + { + b: true, + initPrice: { + max: 25e3, + min: 12e3, + }, + marketCap: 1.6e12, + mv: { + divisor: 100, + max: 80, + min: 70, + }, + name: LocationName.Sector12BladeIndustries, + otlkMag: 13, + spreadPerc: { + divisor: 10, + max: 6, + min: 1, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.Sector12BladeIndustries], + }, + + { + b: true, + initPrice: { + max: 25e3, + min: 10e3, + }, + marketCap: 1.5e12, + mv: { + divisor: 100, + max: 75, + min: 65, + }, + name: LocationName.AevumClarkeIncorporated, + otlkMag: 12, + spreadPerc: { + divisor: 10, + max: 5, + min: 1, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.AevumClarkeIncorporated], + }, + + { + b: true, + initPrice: { + max: 43e3, + min: 32e3, + }, + marketCap: 1.8e12, + mv: { + divisor: 100, + max: 70, + min: 60, + }, + name: LocationName.VolhavenOmniTekIncorporated, + otlkMag: 12, + spreadPerc: { + divisor: 10, + max: 6, + min: 1, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.VolhavenOmniTekIncorporated], + }, + + { + b: true, + initPrice: { + max: 80e3, + min: 50e3, + }, + marketCap: 2e12, + mv: { + divisor: 100, + max: 110, + min: 100, + }, + name: LocationName.Sector12FourSigma, + otlkMag: 17, + spreadPerc: { + divisor: 10, + max: 10, + min: 1, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.Sector12FourSigma], + }, + + { + b: true, + initPrice: { + max: 28e3, + min: 16e3, + }, + marketCap: 1.9e12, + mv: { + divisor: 100, + max: 85, + min: 75, + }, + name: LocationName.ChongqingKuaiGongInternational, + otlkMag: 10, + spreadPerc: { + divisor: 10, + max: 7, + min: 1, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.ChongqingKuaiGongInternational], + }, + + { + b: true, + initPrice: { + max: 36e3, + min: 29e3, + }, + marketCap: 2e12, + mv: { + divisor: 100, + max: 130, + min: 120, + }, + name: LocationName.AevumFulcrumTechnologies, + otlkMag: 16, + spreadPerc: { + divisor: 10, + max: 10, + min: 1, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.AevumFulcrumTechnologies], + }, + + { + b: true, + initPrice: { + max: 25e3, + min: 20e3, + }, + marketCap: 1.2e12, + mv: { + divisor: 100, + max: 90, + min: 80, + }, + name: LocationName.IshimaStormTechnologies, + otlkMag: 7, + spreadPerc: { + divisor: 10, + max: 10, + min: 2, + }, + shareTxForMovement: { + max: 12e3, + min: 6e3, + }, + symbol: StockSymbols[LocationName.IshimaStormTechnologies], + }, + + { + b: true, + initPrice: { + max: 19e3, + min: 6e3, + }, + marketCap: 900e9, + mv: { + divisor: 100, + max: 70, + min: 60, + }, + name: LocationName.NewTokyoDefComm, + otlkMag: 10, + spreadPerc: { + divisor: 10, + max: 10, + min: 2, + }, + shareTxForMovement: { + max: 12e3, + min: 6e3, + }, + symbol: StockSymbols[LocationName.NewTokyoDefComm], + }, + + { + b: true, + initPrice: { + max: 18e3, + min: 10e3, + }, + marketCap: 825e9, + mv: { + divisor: 100, + max: 65, + min: 55, + }, + name: LocationName.VolhavenHeliosLabs, + otlkMag: 9, + spreadPerc: { + divisor: 10, + max: 10, + min: 2, + }, + shareTxForMovement: { + max: 12e3, + min: 6e3, + }, + symbol: StockSymbols[LocationName.VolhavenHeliosLabs], + }, + + { + b: true, + initPrice: { + max: 14e3, + min: 8e3, + }, + marketCap: 1e12, + mv: { + divisor: 100, + max: 80, + min: 70, + }, + name: LocationName.NewTokyoVitaLife, + otlkMag: 7, + spreadPerc: { + divisor: 10, + max: 10, + min: 2, + }, + shareTxForMovement: { + max: 12e3, + min: 6e3, + }, + symbol: StockSymbols[LocationName.NewTokyoVitaLife], + }, + + { + b: true, + initPrice: { + max: 24e3, + min: 12e3, + }, + marketCap: 800e9, + mv: { + divisor: 100, + max: 70, + min: 60, + }, + name: LocationName.Sector12IcarusMicrosystems, + otlkMag: 7.5, + spreadPerc: { + divisor: 10, + max: 10, + min: 3, + }, + shareTxForMovement: { + max: 12e3, + min: 6e3, + }, + symbol: StockSymbols[LocationName.Sector12IcarusMicrosystems], + }, + + { + b: true, + initPrice: { + max: 29e3, + min: 16e3, + }, + marketCap: 900e9, + mv: { + divisor: 100, + max: 60, + min: 50, + }, + name: LocationName.Sector12UniversalEnergy, + otlkMag: 10, + spreadPerc: { + divisor: 10, + max: 10, + min: 2, + }, + shareTxForMovement: { + max: 12e3, + min: 6e3, + }, + symbol: StockSymbols[LocationName.Sector12UniversalEnergy], + }, + + { + b: true, + initPrice: { + max: 17e3, + min: 8e3, + }, + marketCap: 640e9, + mv: { + divisor: 100, + max: 65, + min: 55, + }, + name: LocationName.AevumAeroCorp, + otlkMag: 6, + spreadPerc: { + divisor: 10, + max: 10, + min: 3, + }, + shareTxForMovement: { + max: 14e3, + min: 7e3, + }, + symbol: StockSymbols[LocationName.AevumAeroCorp], + }, + + { + b: true, + initPrice: { + max: 15e3, + min: 6e3, + }, + marketCap: 600e9, + mv: { + divisor: 100, + max: 75, + min: 65, + }, + name: LocationName.VolhavenOmniaCybersystems, + otlkMag: 4.5, + spreadPerc: { + divisor: 10, + max: 11, + min: 4, + }, + shareTxForMovement: { + max: 14e3, + min: 7e3, + }, + symbol: StockSymbols[LocationName.VolhavenOmniaCybersystems], + }, + + { + b: true, + initPrice: { + max: 28e3, + min: 14e3, + }, + marketCap: 705e9, + mv: { + divisor: 100, + max: 80, + min: 70, + }, + name: LocationName.ChongqingSolarisSpaceSystems, + otlkMag: 8.5, + spreadPerc: { + divisor: 10, + max: 12, + min: 4, + }, + shareTxForMovement: { + max: 14e3, + min: 7e3, + }, + symbol: StockSymbols[LocationName.ChongqingSolarisSpaceSystems], + }, + + { + b: true, + initPrice: { + max: 30e3, + min: 12e3, + }, + marketCap: 695e9, + mv: { + divisor: 100, + max: 65, + min: 55, + }, + name: LocationName.NewTokyoGlobalPharmaceuticals, + otlkMag: 10.5, + spreadPerc: { + divisor: 10, + max: 10, + min: 4, + }, + shareTxForMovement: { + max: 14e3, + min: 7e3, + }, + symbol: StockSymbols[LocationName.NewTokyoGlobalPharmaceuticals], + }, + + { + b: true, + initPrice: { + max: 27e3, + min: 15e3, + }, + marketCap: 600e9, + mv: { + divisor: 100, + max: 80, + min: 70, + }, + name: LocationName.IshimaNovaMedical, + otlkMag: 5, + spreadPerc: { + divisor: 10, + max: 11, + min: 4, + }, + shareTxForMovement: { + max: 14e3, + min: 7e3, + }, + symbol: StockSymbols[LocationName.IshimaNovaMedical], + }, + + { + b: true, + initPrice: { + max: 8.5e3, + min: 4e3, + }, + marketCap: 450e9, + mv: { + divisor: 100, + max: 260, + min: 240, + }, + name: LocationName.AevumWatchdogSecurity, + otlkMag: 1.5, + spreadPerc: { + divisor: 10, + max: 12, + min: 5, + }, + shareTxForMovement: { + max: 6e3, + min: 2e3, + }, + symbol: StockSymbols[LocationName.AevumWatchdogSecurity], + }, + + { + b: true, + initPrice: { + max: 8e3, + min: 4.5e3, + }, + marketCap: 300e9, + mv: { + divisor: 100, + max: 135, + min: 115, + }, + name: LocationName.VolhavenLexoCorp, + otlkMag: 6, + spreadPerc: { + divisor: 10, + max: 12, + min: 5, + }, + shareTxForMovement: { + max: 12e3, + min: 6e3, + }, + symbol: StockSymbols[LocationName.VolhavenLexoCorp], + }, + + { + b: true, + initPrice: { + max: 7e3, + min: 2e3, + }, + marketCap: 180e9, + mv: { + divisor: 100, + max: 70, + min: 50, + }, + name: LocationName.AevumRhoConstruction, + otlkMag: 1, + spreadPerc: { + divisor: 10, + max: 10, + min: 3, + }, + shareTxForMovement: { + max: 20e3, + min: 10e3, + }, + symbol: StockSymbols[LocationName.AevumRhoConstruction], + }, + + { + b: true, + initPrice: { + max: 8.5e3, + min: 4e3, + }, + marketCap: 240e9, + mv: { + divisor: 100, + max: 205, + min: 175, + }, + name: LocationName.Sector12AlphaEnterprises, + otlkMag: 10, + spreadPerc: { + divisor: 10, + max: 16, + min: 5, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.Sector12AlphaEnterprises], + }, + + { + b: true, + initPrice: { + max: 8e3, + min: 3e3, + }, + marketCap: 200e9, + mv: { + divisor: 100, + max: 170, + min: 150, + }, + name: LocationName.VolhavenSysCoreSecurities, + otlkMag: 3, + spreadPerc: { + divisor: 10, + max: 12, + min: 5, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.VolhavenSysCoreSecurities], + }, + + { + b: true, + initPrice: { + max: 6e3, + min: 1e3, + }, + marketCap: 185e9, + mv: { + divisor: 100, + max: 100, + min: 80, + }, + name: LocationName.VolhavenCompuTek, + otlkMag: 4, + spreadPerc: { + divisor: 10, + max: 12, + min: 4, + }, + shareTxForMovement: { + max: 15e3, + min: 10e3, + }, + symbol: StockSymbols[LocationName.VolhavenCompuTek], + }, + + { + b: true, + initPrice: { + max: 5e3, + min: 1e3, + }, + marketCap: 58e9, + mv: { + divisor: 100, + max: 430, + min: 400, + }, + name: LocationName.AevumNetLinkTechnologies, + otlkMag: 1, + spreadPerc: { + divisor: 10, + max: 20, + min: 5, + }, + shareTxForMovement: { + max: 6e3, + min: 3e3, + }, + symbol: StockSymbols[LocationName.AevumNetLinkTechnologies], + }, + + { + b: true, + initPrice: { + max: 8e3, + min: 1e3, + }, + marketCap: 60e9, + mv: { + divisor: 100, + max: 110, + min: 90, + }, + name: LocationName.IshimaOmegaSoftware, + otlkMag: 0.5, + spreadPerc: { + divisor: 10, + max: 13, + min: 4, + }, + shareTxForMovement: { + max: 10e3, + min: 5e3, + }, + symbol: StockSymbols[LocationName.IshimaOmegaSoftware], + }, + + { + b: false, + initPrice: { + max: 4.5e3, + min: 500, + }, + marketCap: 45e9, + mv: { + divisor: 100, + max: 80, + min: 70, + }, + name: LocationName.Sector12FoodNStuff, + otlkMag: 1, + spreadPerc: { + divisor: 10, + max: 10, + min: 6, + }, + shareTxForMovement: { + max: 20e3, + min: 10e3, + }, + symbol: StockSymbols[LocationName.Sector12FoodNStuff], + }, + + { + b: true, + initPrice: { + max: 3.5e3, + min: 1.5e3, + }, + marketCap: 30e9, + mv: { + divisor: 100, + max: 300, + min: 260, + }, + name: "Sigma Cosmetics", + otlkMag: 0, + spreadPerc: { + divisor: 10, + max: 14, + min: 6, + }, + shareTxForMovement: { + max: 8e3, + min: 4e3, + }, + symbol: StockSymbols["Sigma Cosmetics"], + }, + + { + b: true, + initPrice: { + max: 1.5e3, + min: 250, + }, + marketCap: 42e9, + mv: { + divisor: 100, + max: 400, + min: 360, + }, + name: "Joes Guns", + otlkMag: 1, + spreadPerc: { + divisor: 10, + max: 14, + min: 6, + }, + shareTxForMovement: { + max: 7e3, + min: 3e3, + }, + symbol: StockSymbols["Joes Guns"], + }, + + { + b: true, + initPrice: { + max: 1.5e3, + min: 250, + }, + marketCap: 100e9, + mv: { + divisor: 100, + max: 175, + min: 120, + }, + name: "Catalyst Ventures", + otlkMag: 13.5, + spreadPerc: { + divisor: 10, + max: 14, + min: 5, + }, + shareTxForMovement: { + max: 8e3, + min: 4e3, + }, + symbol: StockSymbols["Catalyst Ventures"], + }, + + { + b: true, + initPrice: { + max: 30e3, + min: 15e3, + }, + marketCap: 360e9, + mv: { + divisor: 100, + max: 80, + min: 70, + }, + name: "Microdyne Technologies", + otlkMag: 8, + spreadPerc: { + divisor: 10, + max: 10, + min: 3, + }, + shareTxForMovement: { + max: 25e3, + min: 15e3, + }, + symbol: StockSymbols["Microdyne Technologies"], + }, + + { + b: true, + initPrice: { + max: 24e3, + min: 12e3, + }, + marketCap: 420e9, + mv: { + divisor: 100, + max: 70, + min: 50, + }, + name: "Titan Laboratories", + otlkMag: 11, + spreadPerc: { + divisor: 10, + max: 10, + min: 2, + }, + shareTxForMovement: { + max: 25e3, + min: 15e3, + }, + symbol: StockSymbols["Titan Laboratories"], + }, +]; diff --git a/src/StockMarket/data/OrderTypes.ts b/src/StockMarket/data/OrderTypes.ts new file mode 100644 index 000000000..0780fb83f --- /dev/null +++ b/src/StockMarket/data/OrderTypes.ts @@ -0,0 +1,6 @@ +export enum OrderTypes { + LimitBuy = "Limit Buy Order", + LimitSell = "Limit Sell Order", + StopBuy = "Stop Buy Order", + StopSell = "Stop Sell Order" +} diff --git a/src/StockMarket/data/PositionTypes.ts b/src/StockMarket/data/PositionTypes.ts new file mode 100644 index 000000000..4425f85bb --- /dev/null +++ b/src/StockMarket/data/PositionTypes.ts @@ -0,0 +1,4 @@ +export enum PositionTypes { + Long = "L", + Short = "S" +} diff --git a/src/StockMarket/data/StockSymbols.ts b/src/StockMarket/data/StockSymbols.ts new file mode 100644 index 000000000..8acb04814 --- /dev/null +++ b/src/StockMarket/data/StockSymbols.ts @@ -0,0 +1,41 @@ +import { IMap } from "../../types"; +import { LocationName } from "../../Locations/data/LocationNames"; + +export const StockSymbols: IMap = {}; + +// Stocks for companies at which you can work +StockSymbols[LocationName.AevumECorp] = "ECP"; +StockSymbols[LocationName.Sector12MegaCorp] = "MGCP"; +StockSymbols[LocationName.Sector12BladeIndustries] = "BLD"; +StockSymbols[LocationName.AevumClarkeIncorporated] = "CLRK"; +StockSymbols[LocationName.VolhavenOmniTekIncorporated] = "OMTK"; +StockSymbols[LocationName.Sector12FourSigma] = "FSIG"; +StockSymbols[LocationName.ChongqingKuaiGongInternational] = "KGI"; +StockSymbols[LocationName.AevumFulcrumTechnologies] = "FLCM"; +StockSymbols[LocationName.IshimaStormTechnologies] = "STM"; +StockSymbols[LocationName.NewTokyoDefComm] = "DCOMM"; +StockSymbols[LocationName.VolhavenHeliosLabs] = "HLS"; +StockSymbols[LocationName.NewTokyoVitaLife] = "VITA"; +StockSymbols[LocationName.Sector12IcarusMicrosystems] = "ICRS"; +StockSymbols[LocationName.Sector12UniversalEnergy] = "UNV"; +StockSymbols[LocationName.AevumAeroCorp] = "AERO"; +StockSymbols[LocationName.VolhavenOmniaCybersystems] = "OMN"; +StockSymbols[LocationName.ChongqingSolarisSpaceSystems] = "SLRS"; +StockSymbols[LocationName.NewTokyoGlobalPharmaceuticals] = "GPH"; +StockSymbols[LocationName.IshimaNovaMedical] = "NVMD"; +StockSymbols[LocationName.AevumWatchdogSecurity] = "WDS"; +StockSymbols[LocationName.VolhavenLexoCorp] = "LXO"; +StockSymbols[LocationName.AevumRhoConstruction] = "RHOC"; +StockSymbols[LocationName.Sector12AlphaEnterprises] = "APHE"; +StockSymbols[LocationName.VolhavenSysCoreSecurities] = "SYSC"; +StockSymbols[LocationName.VolhavenCompuTek] = "CTK"; +StockSymbols[LocationName.AevumNetLinkTechnologies] = "NTLK"; +StockSymbols[LocationName.IshimaOmegaSoftware] = "OMGA"; +StockSymbols[LocationName.Sector12FoodNStuff] = "FNS"; + +// Stocks for other companies +StockSymbols["Sigma Cosmetics"] = "SGC"; +StockSymbols["Joes Guns"] = "JGN"; +StockSymbols["Catalyst Ventures"] = "CTYS"; +StockSymbols["Microdyne Technologies"] = "MDYN"; +StockSymbols["Titan Laboratories"] = "TITN"; diff --git a/src/engine.jsx b/src/engine.jsx index 5245efb81..5ebd12ddb 100644 --- a/src/engine.jsx +++ b/src/engine.jsx @@ -84,7 +84,6 @@ import { StockMarket, StockSymbols, SymbolToStockMap, - initStockSymbols, initSymbolToStockMap, stockMarketCycle, processStockPrices, @@ -1085,7 +1084,6 @@ const Engine = { Engine.init(); // Initialize buttons, work, etc. initAugmentations(); // Also calls Player.reapplyAllAugmentations() Player.reapplyAllSourceFiles(); - initStockSymbols(); if (Player.hasWseAccount) { initSymbolToStockMap(); } @@ -1215,7 +1213,6 @@ const Engine = { initFactions(); initAugmentations(); initMessages(); - initStockSymbols(); initLiterature(); initSingularitySFFlags(); diff --git a/src/types.ts b/src/types.ts index ec1581932..09c071767 100644 --- a/src/types.ts +++ b/src/types.ts @@ -44,3 +44,25 @@ export interface IReturnStatus { res: boolean; msg?: string; } + +/** + * Defines the minimum and maximum values for a range. + * It is up to the consumer if these values are inclusive or exclusive. + * It is up to the implementor to ensure max > min. + */ +export interface IMinMaxRange { + /** + * Value by which the bounds are to be divided for the final range + */ + divisor?: number; + + /** + * The maximum bound of the range. + */ + max: number; + + /** + * The minimum bound of the range. + */ + min: number; +}