Bug fixes for v0.47.0. Fixed the BUY MAX feature for new stock market. Added collapse/expand tickers buttons for new stock market UI

This commit is contained in:
danielyxie 2019-04-29 20:54:20 -07:00
parent 9df054dd0c
commit 580a7fac24
30 changed files with 361 additions and 164 deletions

@ -42,6 +42,7 @@
}
#stock-market-watchlist-filter {
display: block;
margin: 5px 5px 5px 10px;
padding: 4px;
width: 50%;

@ -51,7 +51,7 @@ bid price. Note that this is reversed for a short position. Purchasing a stock
in the short position will occur at the stock's bid price, and selling a stock
in the short position will occur at the stock's ask price.
.. _gameplay_stock_spread_price_movement:
.. _gameplay_stock_market_spread_price_movement:
Transactions Influencing Stock Price
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -73,6 +73,8 @@ be sold at $98.01, and so on.
This is an important concept to keep in mind if you are trying to purchase/sell a
large number of shares, as **it can negatively affect your profits**.
.. _gameplay_stock_market_order_types:
Order Types
^^^^^^^^^^^
There are three different types of orders you can make to buy or sell stocks on the exchange:

@ -61,5 +61,5 @@ getInformation() Netscript Function
workChaExpGain: charisma exp gained from work,
workMoneyGain: money gained from work,
},
workRepGain: sl.getRepGain(),
workRepGain: Reputation gain rate when working for factions or companies
}

@ -1,5 +1,5 @@
getSleeveAugmentations() Netscript Function
=======================================
===========================================
.. js:function:: getSleeveAugmentations(sleeveNumber)

@ -6,7 +6,11 @@ getOrders() Netscript Function
:RAM cost: 2.5 GB
Returns your order book for the stock market. This is an object containing information
for all the Limit and Stop Orders you have in the stock market.
for all the :ref:`Limit and Stop Orders <gameplay_stock_market_order_types>`
you have in the stock market.
.. note:: This function isn't accessible until you have unlocked the ability to use
Limit and Stop Orders.
The object has the following structure::

@ -11,5 +11,5 @@ getStockPurchaseCost() Netscript Function
Calculates and returns how much it would cost to buy a given number of
shares of a stock. This takes into account :ref:`spread <gameplay_stock_market_spread>`,
:ref:`large transactions influencing the price of the stock <gameplay_stock_spread_price_movement>`
:ref:`large transactions influencing the price of the stock <gameplay_stock_market_spread_price_movement>`
and commission fees.

@ -11,5 +11,5 @@ getStockSaleGain() Netscript Function
Calculates and returns how much you would gain from selling a given number of
shares of a stock. This takes into account :ref:`spread <gameplay_stock_market_spread>`,
:ref:`large transactions influencing the price of the stock <gameplay_stock_spread_price_movement>`
:ref:`large transactions influencing the price of the stock <gameplay_stock_market_spread_price_movement>`
and commission fees.

@ -19,8 +19,10 @@ placeOrder() Netscript Function
NOT case-sensitive.
:RAM cost: 2.5 GB
Places an order on the stock market. This function only works for `Limit and Stop Orders <http://bitburner.wikia.com/wiki/Stock_Market#Order_Types>`_.
The ability to place limit and stop orders is **not** immediately available to the player and must be unlocked later on in the game.
Places an order on the stock market. This function only works
for :ref:`Limit and Stop Orders <gameplay_stock_market_order_types>`.
Returns true if the order is successfully placed, and false otherwise.
.. note:: The ability to place limit and stop orders is **not** immediately available to
the player and must be unlocked later on in the game.

@ -7,6 +7,7 @@ import { Factions } from "./Faction/Factions";
import { Player } from "./Player";
import { AllServers } from "./Server/AllServers";
import { GetServerByHostname } from "./Server/ServerHelpers";
import { SpecialServerNames } from "./Server/SpecialServerIps";
import { getRandomInt } from "../utils/helpers/getRandomInt";
@ -162,7 +163,9 @@ function getRandomServer() {
// An infinite loop shouldn't ever happen, but to be safe we'll use
// a for loop with a limited number of tries
for (let i = 0; i < 200; ++i) {
if (randServer.purchasedByPlayer === false) { break; }
if (!randServer.purchasedByPlayer && randServer.hostname !== SpecialServerNames.WorldDaemon) {
break;
}
randIndex = getRandomInt(0, servers.length - 1);
randServer = AllServers[servers[randIndex]];
}

@ -284,6 +284,11 @@ export let CONSTANTS: IMap<any> = {
** Added getStockAskPrice(), getStockBidPrice() Netscript functions to the TIX API
** Added getStockPurchaseCost(), getStockSaleGain() Netscript functions to the TIX API
* Bug Fix: Fixed sleeve.getInformation() throwing error in certain scenarios
* Bug Fix: Coding contracts should no longer generate on the w0r1d_d43m0n server
* Bug Fix: Duplicate Sleeves now properly have access to all Augmentations if you have a gang
* Bug Fix: Fixed issue that caused messages (.msg) to be sent when refreshing/reloading the game
* Bug Fix: Purchasing hash upgrades for Bladeburner/Corporation when you don't actually have access to those mechanics no longer gives hashes
* Bug Fix: run(), exec(), and spawn() Netscript functions now throw if called with 0 threads
`
}

@ -99,14 +99,16 @@ export class HashManager {
*/
refundUpgrade(upgName: string): void {
const upg = HashUpgrades[upgName];
// Reduce the level first, so we get the right cost
--this.upgrades[upgName];
const currLevel = this.upgrades[upgName];
if (upg == null || currLevel == null || currLevel === 0) {
if (upg == null || currLevel == null || currLevel < 0) {
console.error(`Invalid Upgrade Name given to HashManager.upgrade(): ${upgName}`);
return;
}
// Reduce the level first, so we get the right cost
--this.upgrades[upgName];
const cost = upg.getCost(currLevel);
this.hashes += cost;
}

@ -1,15 +1,15 @@
import { Message } from "./Message";
import { Augmentatation } from "../Augmentation/Augmentation";
import { Augmentations } from "../Augmentation/Augmentations";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { Programs } from "../Programs/Programs";
import { inMission } from "../Missions";
import { Player } from "../Player";
import { redPillFlag } from "../RedPill";
import { GetServerByHostname } from "../Server/ServerHelpers";
import { Settings } from "../Settings/Settings";
import { dialogBoxCreate,
dialogBoxOpened} from "../../utils/DialogBox";
import { Message } from "./Message";
import { Augmentatation } from "../Augmentation/Augmentation";
import { Augmentations } from "../Augmentation/Augmentations";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { Programs } from "../Programs/Programs";
import { inMission } from "../Missions";
import { Player } from "../Player";
import { redPillFlag } from "../RedPill";
import { GetServerByHostname } from "../Server/ServerHelpers";
import { Settings } from "../Settings/Settings";
import { dialogBoxCreate, dialogBoxOpened} from "../../utils/DialogBox";
import { Reviver } from "../../utils/JSONReviver";
//Sends message to player, including a pop up
function sendMessage(msg, forced=false) {
@ -31,7 +31,7 @@ function showMessage(msg) {
function addMessageToServer(msg, serverHostname) {
var server = GetServerByHostname(serverHostname);
if (server == null) {
console.log("WARNING: Did not locate " + serverHostname);
console.warn(`Could not find server ${serverHostname}`);
return;
}
for (var i = 0; i < server.messages.length; ++i) {

@ -117,7 +117,7 @@ import {
} from "./NetscriptEvaluator";
import { NetscriptPort } from "./NetscriptPort";
import { SleeveTaskType } from "./PersonObjects/Sleeve/SleeveTaskTypesEnum";
import { findSleevePurchasableAugs } from "./PersonObjects/Sleeve/Sleeve";
import { findSleevePurchasableAugs } from "./PersonObjects/Sleeve/SleeveHelpers";
import { Page, routing } from "./ui/navigationTracking";
import { numeralWrapper } from "./ui/numeralFormat";
@ -288,6 +288,9 @@ function NetscriptFunctions(workerScript) {
* Checks if the player has TIX API access. Throws an error if the player does not
*/
const checkTixApiAccess = function(callingFn="") {
if (!Player.hasWseAccount) {
throw makeRuntimeRejectMsg(workerScript, `You don't have WSE Access! Cannot use ${callingFn}()`);
}
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, `You don't have TIX API Access! Cannot use ${callingFn}()`);
}
@ -977,7 +980,7 @@ function NetscriptFunctions(workerScript) {
if (scriptname === undefined || ip === undefined) {
throw makeRuntimeRejectMsg(workerScript, "exec() call has incorrect number of arguments. Usage: exec(scriptname, server, [numThreads], [arg1], [arg2]...)");
}
if (isNaN(threads) || threads < 0) {
if (isNaN(threads) || threads <= 0) {
throw makeRuntimeRejectMsg(workerScript, "Invalid argument for thread count passed into exec(). Must be numeric and greater than 0");
}
var argsForNewScript = [];
@ -1002,7 +1005,7 @@ function NetscriptFunctions(workerScript) {
if (scriptname === undefined) {
throw makeRuntimeRejectMsg(workerScript, "spawn() call has incorrect number of arguments. Usage: spawn(scriptname, numThreads, [arg1], [arg2]...)");
}
if (isNaN(threads) || threads < 0) {
if (isNaN(threads) || threads <= 0) {
throw makeRuntimeRejectMsg(workerScript, "Invalid argument for thread count passed into run(). Must be numeric and greater than 0");
}
var argsForNewScript = [];
@ -1858,9 +1861,7 @@ function NetscriptFunctions(workerScript) {
return updateStaticRam("getOrders", CONSTANTS.ScriptBuySellStockRamCost);
}
updateDynamicRam("getOrders", CONSTANTS.ScriptBuySellStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use getOrders()");
}
checkTixApiAccess("getOrders");
if (Player.bitNodeN !== 8) {
if (!(hasWallStreetSF && wallStreetSFLvl >= 3)) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Cannot use getOrders(). You must either be in BitNode-8 or have Level 3 of Source-File 8");
@ -5078,7 +5079,7 @@ function NetscriptFunctions(workerScript) {
workChaExpGain: sl.earningsForTask.cha,
workMoneyGain: sl.earningsForTask.money,
},
workRepGain: sl.getRepGain(),
workRepGain: sl.getRepGain(Player),
}
},
getSleeveAugmentations : function(sleeveNumber=0) {

@ -129,6 +129,7 @@ export interface IPlayer {
gainCharismaExp(exp: number): void;
gainMoney(money: number): void;
getCurrentServer(): Server;
getGangFaction(): Faction;
getGangName(): string;
getHomeComputer(): Server;
getNextCompanyPosition(company: Company, entryPosType: CompanyPosition): CompanyPosition;

@ -13,6 +13,15 @@ export function canAccessGang() {
return (this.karma <= GangKarmaRequirement);
}
export function getGangFaction() {
const fac = Factions[this.gang.facName];
if (fac == null) {
throw new Error(`Gang has invalid faction name: ${this.gang.facName}`);
}
return fac;
}
export function getGangName() {
return this.gang.facName;
}

@ -884,32 +884,4 @@ export class Sleeve extends Person {
}
}
export function findSleevePurchasableAugs(sleeve: Sleeve, p: IPlayer): Augmentation[] {
// You can only purchase Augmentations that are actually available from
// your factions. I.e. you must be in a faction that has the Augmentation
// and you must also have enough rep in that faction in order to purchase it.
const ownedAugNames: string[] = sleeve.augmentations.map((e) => {return e.name});
const availableAugs: Augmentation[] = [];
for (const facName of p.factions) {
if (facName === "Bladeburners") { continue; }
if (facName === "Netburners") { continue; }
const fac: Faction | null = Factions[facName];
if (fac == null) { continue; }
for (const augName of fac.augmentations) {
if (augName === AugmentationNames.NeuroFluxGovernor) { continue; }
if (ownedAugNames.includes(augName)) { continue; }
const aug: Augmentation | null = Augmentations[augName];
if (fac.playerReputation > aug.baseRepRequirement && !availableAugs.includes(aug)) {
availableAugs.push(aug);
}
}
}
return availableAugs;
}
Reviver.constructors.Sleeve = Sleeve;

@ -2,16 +2,13 @@
* Module for handling the UI for purchasing Sleeve Augmentations
* This UI is a popup, not a full page
*/
import { Sleeve, findSleevePurchasableAugs } from "./Sleeve";
import { Sleeve } from "./Sleeve";
import { findSleevePurchasableAugs } from "./SleeveHelpers";
import { IPlayer } from "../IPlayer";
import { Augmentation } from "../../Augmentation/Augmentation";
import { Augmentations } from "../../Augmentation/Augmentations";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Faction } from "../../Faction/Faction";
import { Factions } from "../../Faction/Factions";
import { numeralWrapper } from "../../ui/numeralFormat";

@ -0,0 +1,64 @@
import { Sleeve } from "./Sleeve";
import { IPlayer } from "../IPlayer";
import { Augmentation } from "../../Augmentation/Augmentation";
import { Augmentations } from "../../Augmentation/Augmentations";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Faction } from "../../Faction/Faction";
import { Factions } from "../../Faction/Factions";
export function findSleevePurchasableAugs(sleeve: Sleeve, p: IPlayer): Augmentation[] {
// You can only purchase Augmentations that are actually available from
// your factions. I.e. you must be in a faction that has the Augmentation
// and you must also have enough rep in that faction in order to purchase it.
const ownedAugNames: string[] = sleeve.augmentations.map((e) => {return e.name});
const availableAugs: Augmentation[] = [];
// Helper function that helps filter out augs that are already owned
// and augs that aren't allowed for sleeves
function isAvailableForSleeve(aug: Augmentation): boolean {
if (aug.name === AugmentationNames.NeuroFluxGovernor) { return false; }
if (ownedAugNames.includes(aug.name)) { return false; }
if (availableAugs.includes(aug)) { return false; }
if (aug.isSpecial) { return false; }
return true;
}
// If player is in a gang, then we return all augs that the player
// has enough reputation for (since that gang offers all augs)
if (p.inGang()) {
const fac = p.getGangFaction();
for (const augName in Augmentations) {
const aug = Augmentations[augName];
if (!isAvailableForSleeve(aug)) { continue; }
if (fac.playerReputation > aug.baseRepRequirement) {
availableAugs.push(aug);
}
}
return availableAugs;
}
for (const facName of p.factions) {
if (facName === "Bladeburners") { continue; }
if (facName === "Netburners") { continue; }
const fac: Faction | null = Factions[facName];
if (fac == null) { continue; }
for (const augName of fac.augmentations) {
const aug: Augmentation = Augmentations[augName];
if (!isAvailableForSleeve(aug)) { continue; }
if (fac.playerReputation > aug.baseRepRequirement) {
availableAugs.push(aug);
}
}
}
return availableAugs;
}

@ -45,6 +45,7 @@ import {
SpecialServerNames
} from "./Server/SpecialServerIps";
import {
deleteStockMarket,
initStockMarket,
initSymbolToStockMap,
} from "./StockMarket/StockMarket";
@ -326,6 +327,8 @@ function prestigeSourceFile() {
if (Player.hasWseAccount) {
initStockMarket();
initSymbolToStockMap();
} else {
deleteStockMarket();
}
if (Player.inGang()) { Player.gang.clearUI(); }

@ -204,27 +204,33 @@ function loadGame(saveString) {
try {
loadAliases(saveObj.AliasesSave);
} catch(e) {
console.warn(`Could not load Aliases from save`);
loadAliases("");
}
} else {
console.warn(`Save file did not contain an Aliases property`);
loadAliases("");
}
if (saveObj.hasOwnProperty("GlobalAliasesSave")) {
try {
loadGlobalAliases(saveObj.GlobalAliasesSave);
} catch(e) {
console.warn(`Could not load GlobalAliases from save`);
loadGlobalAliases("");
}
} else {
console.warn(`Save file did not contain a GlobalAliases property`);
loadGlobalAliases("");
}
if (saveObj.hasOwnProperty("MessagesSave")) {
try {
loadMessages(saveObj.MessagesSave);
} catch(e) {
console.warn(`Could not load Messages from save`);
initMessages();
}
} else {
console.warn(`Save file did not contain a Messages property`);
initMessages();
}
if (saveObj.hasOwnProperty("StockMarketSave")) {

@ -95,6 +95,7 @@ let NetscriptFunctions =
// TIX API
"getStockPrice|getStockPosition|getStockSymbols|getStockMaxShares|" +
"getStockAskPrice|getStockBidPrice|getStockPurchaseCost|getStockSaleGain|" +
"buyStock|sellStock|shortStock|sellShort|" +
"placeOrder|cancelOrder|getOrders|getStockVolatility|getStockForecast|" +
"purchase4SMarketData|purchase4SMarketDataTixApi|" +

@ -153,9 +153,13 @@ CodeMirror.defineMode("netscript", function(config, parserConfig) {
// Netscript TIX API
"getStockPrice": atom,
"getStockAskPrice": atom,
"getStockBidPrice": atom,
"getStockPosition": atom,
"getStockSymbols": atom,
"getStockMaxShares": atom,
"getStockPurchaseCost": atom,
"getStockSaleGain": atom,
"buyStock": atom,
"sellStock": atom,
"shortStock": atom,

@ -154,6 +154,10 @@ export function loadStockMarket(saveString) {
}
}
export function deleteStockMarket() {
StockMarket = {};
}
export function initStockMarket() {
for (const stk in StockMarket) {
if (StockMarket.hasOwnProperty(stk)) {
@ -195,15 +199,13 @@ export function initSymbolToStockMap() {
}
export function stockMarketCycle() {
for (var name in StockMarket) {
if (StockMarket.hasOwnProperty(name)) {
var stock = StockMarket[name];
if (!(stock instanceof Stock)) {continue;}
var thresh = 0.6;
if (stock.b) {thresh = 0.4;}
if (Math.random() < thresh) {
stock.b = !stock.b;
}
for (const name in StockMarket) {
const stock = StockMarket[name];
if (!(stock instanceof Stock)) { continue; }
let thresh = 0.6;
if (stock.b) { thresh = 0.4; }
if (Math.random() < thresh) {
stock.b = !stock.b;
}
}
}
@ -235,9 +237,9 @@ export function buyStock(stock, shares, workerScript=null) {
const totalPrice = getBuyTransactionCost(stock, shares, PositionTypes.Long);
if (Player.money.lt(totalPrice)) {
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)}`);
workerScript.log(`ERROR: buyStock() failed because you do not have enough money to purchase this potiion. You need ${numeralWrapper.formatMoney(totalPrice)}`);
} else {
dialogBoxCreate(`You do not have enough money to purchase this. You need ${numeralWrapper.formatMoney(totalPrice + CONSTANTS.StockMarketCommission)}`);
dialogBoxCreate(`You do not have enough money to purchase this. You need ${numeralWrapper.formatMoney(totalPrice)}`);
}
return false;
@ -256,7 +258,7 @@ export function buyStock(stock, shares, workerScript=null) {
const origTotal = stock.playerShares * stock.playerAvgPx;
Player.loseMoney(totalPrice);
const newTotal = origTotal + totalPrice;
const newTotal = origTotal + totalPrice - CONSTANTS.StockMarketCommission;
stock.playerShares = Math.round(stock.playerShares + shares);
stock.playerAvgPx = newTotal / stock.playerShares;
processBuyTransactionPriceMovement(stock, shares, PositionTypes.Long);
@ -317,7 +319,7 @@ export function sellStock(stock, shares, workerScript=null) {
const resultTxt = `Sold ${numeralWrapper.format(shares, '0,0')} shares of ${stock.symbol}. ` +
`After commissions, you gained a total of ${numeralWrapper.formatMoney(gains)}.`;
if (tixApi) {
if (workerScript.shouldLog("sellStock")) { workerScript.log(resultTxt); }
if (workerScript.shouldLog("sellStock")) { workerScript.log(resultTxt); }
} else {
dialogBoxCreate(resultTxt);
}
@ -350,14 +352,14 @@ export function shortStock(stock, shares, workerScript=null) {
// Does the player have enough money?
const totalPrice = getBuyTransactionCost(stock, shares, PositionTypes.Short);
if (Player.money.lt(totalPrice + CONSTANTS.StockMarketCommission)) {
if (Player.money.lt(totalPrice)) {
if (tixApi) {
workerScript.log("ERROR: shortStock() failed because you do not have enough " +
"money to purchase this short position. You need " +
numeralWrapper.formatMoney(totalPrice + CONSTANTS.StockMarketCommission));
numeralWrapper.formatMoney(totalPrice));
} else {
dialogBoxCreate("You do not have enough money to purchase this short position. You need " +
numeralWrapper.formatMoney(totalPrice + CONSTANTS.StockMarketCommission));
numeralWrapper.formatMoney(totalPrice));
}
return false;
@ -375,8 +377,8 @@ export function shortStock(stock, shares, workerScript=null) {
}
const origTotal = stock.playerShortShares * stock.playerAvgShortPx;
Player.loseMoney(totalPrice + CONSTANTS.StockMarketCommission);
const newTotal = origTotal + totalPrice;
Player.loseMoney(totalPrice);
const newTotal = origTotal + totalPrice - CONSTANTS.StockMarketCommission;
stock.playerShortShares = Math.round(stock.playerShortShares + shares);
stock.playerAvgShortPx = newTotal / stock.playerShortShares;
processBuyTransactionPriceMovement(stock, shares, PositionTypes.Short);
@ -454,16 +456,17 @@ export function sellShort(stock, shares, workerScript=null) {
return true;
}
// Stock prices updated every 6 seconds
const msPerStockUpdate = 6e3;
const cyclesPerStockUpdate = msPerStockUpdate / CONSTANTS.MilliPerCycle;
export function processStockPrices(numCycles=1) {
if (StockMarket.storedCycles == null || isNaN(StockMarket.storedCycles)) { StockMarket.storedCycles = 0; }
StockMarket.storedCycles += numCycles;
// Stock Prices updated every 6 seconds on average. But if there are stored
// cycles they update 50% faster, so every 4 seconds
const msPerStockUpdate = 6e3;
const cyclesPerStockUpdate = msPerStockUpdate / CONSTANTS.MilliPerCycle;
if (StockMarket.storedCycles < cyclesPerStockUpdate) { return; }
// We can process the update every 4 seconds as long as there are enough
// stored cycles. This lets us account for offline time
const timeNow = new Date().getTime();
if (timeNow - StockMarket.lastUpdate < 4e3) { return; }
@ -471,62 +474,62 @@ export function processStockPrices(numCycles=1) {
StockMarket.storedCycles -= cyclesPerStockUpdate;
var v = Math.random();
for (var name in StockMarket) {
if (StockMarket.hasOwnProperty(name)) {
var stock = StockMarket[name];
if (!(stock instanceof Stock)) { continue; }
var av = (v * stock.mv) / 100;
if (isNaN(av)) {av = .02;}
for (const name in StockMarket) {
const stock = StockMarket[name];
if (!(stock instanceof Stock)) { continue; }
let av = (v * stock.mv) / 100;
if (isNaN(av)) { av = .02; }
var chc = 50;
if (stock.b) {
chc = (chc + stock.otlkMag) / 100;
if (isNaN(chc)) {chc = 0.5;}
} else {
chc = (chc - stock.otlkMag) / 100;
if (isNaN(chc)) {chc = 0.5;}
}
if (stock.price >= stock.cap) {
chc = 0.1; // "Soft Limit" on stock price. It could still go up but its unlikely
stock.b = false;
}
var c = Math.random();
if (c < chc) {
stock.price *= (1 + av);
processOrders(stock, OrderTypes.LimitBuy, PositionTypes.Short);
processOrders(stock, OrderTypes.LimitSell, PositionTypes.Long);
processOrders(stock, OrderTypes.StopBuy, PositionTypes.Long);
processOrders(stock, OrderTypes.StopSell, PositionTypes.Short);
} else {
stock.price /= (1 + av);
processOrders(stock, OrderTypes.LimitBuy, PositionTypes.Long);
processOrders(stock, OrderTypes.LimitSell, PositionTypes.Short);
processOrders(stock, OrderTypes.StopBuy, PositionTypes.Short);
processOrders(stock, OrderTypes.StopSell, PositionTypes.Long);
}
var otlkMagChange = stock.otlkMag * av;
if (stock.otlkMag <= 0.1) {
otlkMagChange = 1;
}
if (c < 0.5) {
stock.otlkMag += otlkMagChange;
} else {
stock.otlkMag -= otlkMagChange;
}
if (stock.otlkMag > 50) { stock.otlkMag = 50; } // Cap so the "forecast" is between 0 and 100
if (stock.otlkMag < 0) {
stock.otlkMag *= -1;
stock.b = !stock.b;
}
let chc = 50;
if (stock.b) {
chc = (chc + stock.otlkMag) / 100;
} else {
chc = (chc - stock.otlkMag) / 100;
}
if (stock.price >= stock.cap) {
chc = 0.1; // "Soft Limit" on stock price. It could still go up but its unlikely
stock.b = false;
}
if (isNaN(chc)) { chc = 0.5; }
const c = Math.random();
if (c < chc) {
stock.price *= (1 + av);
processOrders(stock, OrderTypes.LimitBuy, PositionTypes.Short);
processOrders(stock, OrderTypes.LimitSell, PositionTypes.Long);
processOrders(stock, OrderTypes.StopBuy, PositionTypes.Long);
processOrders(stock, OrderTypes.StopSell, PositionTypes.Short);
} else {
stock.price /= (1 + av);
processOrders(stock, OrderTypes.LimitBuy, PositionTypes.Long);
processOrders(stock, OrderTypes.LimitSell, PositionTypes.Short);
processOrders(stock, OrderTypes.StopBuy, PositionTypes.Short);
processOrders(stock, OrderTypes.StopSell, PositionTypes.Long);
}
let otlkMagChange = stock.otlkMag * av;
if (stock.otlkMag <= 0.1) {
otlkMagChange = 1;
}
if (c < 0.5) {
stock.otlkMag += otlkMagChange;
} else {
stock.otlkMag -= otlkMagChange;
}
if (stock.otlkMag > 50) { stock.otlkMag = 50; } // Cap so the "forecast" is between 0 and 100
if (stock.otlkMag < 0) {
stock.otlkMag *= -1;
stock.b = !stock.b;
}
// Shares required for price movement gradually approaches max over time
stock.shareTxUntilMovement = Math.min(stock.shareTxUntilMovement + 5, stock.shareTxForMovement);
}
displayStockMarketContent();
}
//Checks and triggers any orders for the specified stock
// Checks and triggers any orders for the specified stock
function processOrders(stock, orderType, posType) {
var orderBook = StockMarket["Orders"];
if (orderBook == null) {

@ -59,17 +59,18 @@ 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);
const increasingMvmt = calculateIncreasingPriceMovement(stock)!;
const decreasingMvmt = calculateDecreasingPriceMovement(stock)!;
function processPriceMovement() {
if (isLong) {
currPrice *= calculateIncreasingPriceMovement(stock)!;
currPrice *= increasingMvmt;
} else {
currPrice *= calculateDecreasingPriceMovement(stock)!;
currPrice *= decreasingMvmt;
}
}
@ -228,3 +229,58 @@ export function processSellTransactionPriceMovement(stock: Stock, shares: number
stock.price = currPrice;
stock.shareTxUntilMovement = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovement) % stock.shareTxForMovement);
}
/**
* Calculate the maximum number of shares of a stock that can be purchased.
* Handles mid-transaction price movements, both L and S positions, etc.
* Used for the "Buy Max" button in the UI
* @param {Stock} stock - Stock being purchased
* @param {PositionTypes} posType - Long or short position
* @param {number} money - Amount of money player has
* @returns maximum number of shares that the player can purchase
*/
export function calculateBuyMaxAmount(stock: Stock, posType: PositionTypes, money: number): number {
if (!(stock instanceof Stock)) { return 0; }
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 firstIterationCost = stock.shareTxUntilMovement * 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 = stock.shareTxUntilMovement;
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);
}

@ -65,6 +65,11 @@ export class InfoAndPurchases extends React.Component<IProps, any> {
this.props.initStockMarket();
this.props.p.loseMoney(CONSTANTS.WSEAccountCost);
this.props.rerender();
const worldHeader = document.getElementById("world-menu-header");
if (worldHeader instanceof HTMLElement) {
worldHeader.click(); worldHeader.click();
}
}
purchaseTixApiAccess() {
@ -117,7 +122,7 @@ export class InfoAndPurchases extends React.Component<IProps, any> {
const cost = CONSTANTS.TIXAPICost;
return (
<StdButton
disabled={!this.props.p.canAfford(cost)}
disabled={!this.props.p.canAfford(cost) || !this.props.p.hasWseAccount}
onClick={this.purchaseTixApiAccess}
style={blockStyleMarkup}
text={`Buy Trade Information eXchange (TIX) API Access - ${numeralWrapper.formatMoney(cost)}`}
@ -138,7 +143,7 @@ export class InfoAndPurchases extends React.Component<IProps, any> {
const cost = getStockMarket4SDataCost();
return (
<StdButton
disabled={!this.props.p.canAfford(cost)}
disabled={!this.props.p.canAfford(cost) || !this.props.p.hasWseAccount}
onClick={this.purchase4SMarketData}
text={`Buy 4S Market Data Access - ${numeralWrapper.formatMoney(cost)}`}
tooltip={"Lets you view additional pricing and volatility information about stocks"}

@ -59,16 +59,19 @@ export class StockMarketRoot extends React.Component<IProps, IState> {
p={this.props.p}
rerender={this.rerender}
/>
<StockTickers
buyStockLong={this.props.buyStockLong}
buyStockShort={this.props.buyStockShort}
cancelOrder={this.props.cancelOrder}
p={this.props.p}
placeOrder={this.props.placeOrder}
sellStockLong={this.props.sellStockLong}
sellStockShort={this.props.sellStockShort}
stockMarket={this.props.stockMarket}
/>
{
this.props.p.hasWseAccount &&
<StockTickers
buyStockLong={this.props.buyStockLong}
buyStockShort={this.props.buyStockShort}
cancelOrder={this.props.cancelOrder}
p={this.props.p}
placeOrder={this.props.placeOrder}
sellStockLong={this.props.sellStockLong}
sellStockShort={this.props.sellStockShort}
stockMarket={this.props.stockMarket}
/>
}
</div>
)
}

@ -13,6 +13,7 @@ import { Stock } from "../Stock";
import {
getBuyTransactionCost,
getSellTransactionGain,
calculateBuyMaxAmount,
} from "../StockMarketHelpers";
import { OrderTypes } from "../data/OrderTypes";
import { PositionTypes } from "../data/PositionTypes";
@ -142,7 +143,7 @@ export class StockTicker extends React.Component<IProps, IState> {
return `You do not have this many shares in the Short position`;
}
}
const cost = getSellTransactionGain(stock, qty, this.state.position);
if (cost == null) { return ""; }
@ -202,7 +203,7 @@ export class StockTicker extends React.Component<IProps, IState> {
const playerMoney: number = this.props.p.money.toNumber();
const stock = this.props.stock;
let maxShares = Math.floor((playerMoney - CONSTANTS.StockMarketCommission) / this.props.stock.price);
let maxShares = calculateBuyMaxAmount(stock, this.state.position, playerMoney);
maxShares = Math.min(maxShares, Math.round(stock.maxShares - stock.playerShares - stock.playerShortShares));
switch (this.state.orderType) {

@ -37,6 +37,8 @@ type IState = {
}
export class StockTickers extends React.Component<IProps, IState> {
listRef: React.RefObject<HTMLUListElement>;
constructor(props: IProps) {
super(props);
@ -49,6 +51,10 @@ export class StockTickers extends React.Component<IProps, IState> {
this.changeDisplayMode = this.changeDisplayMode.bind(this);
this.changeWatchlistFilter = this.changeWatchlistFilter.bind(this);
this.collapseAllTickers = this.collapseAllTickers.bind(this);
this.expandAllTickers = this.expandAllTickers.bind(this);
this.listRef = React.createRef();
}
changeDisplayMode() {
@ -82,6 +88,38 @@ export class StockTickers extends React.Component<IProps, IState> {
}
}
collapseAllTickers() {
const ul = this.listRef.current;
if (ul == null) { return; }
const tickers = ul.getElementsByClassName("accordion-header");
for (let i = 0; i < tickers.length; ++i) {
const ticker = tickers[i];
if (!(ticker instanceof HTMLButtonElement)) {
continue;
}
if (ticker.classList.contains("active")) {
ticker.click();
}
}
}
expandAllTickers() {
const ul = this.listRef.current;
if (ul == null) { return; }
const tickers = ul.getElementsByClassName("accordion-header");
for (let i = 0; i < tickers.length; ++i) {
const ticker = tickers[i];
if (!(ticker instanceof HTMLButtonElement)) {
continue;
}
if (!ticker.classList.contains("active")) {
ticker.click();
}
}
}
rerender() {
this.setState((prevState) => {
return {
@ -134,10 +172,12 @@ export class StockTickers extends React.Component<IProps, IState> {
<StockTickersConfig
changeDisplayMode={this.changeDisplayMode}
changeWatchlistFilter={this.changeWatchlistFilter}
collapseAllTickers={this.collapseAllTickers}
expandAllTickers={this.expandAllTickers}
tickerDisplayMode={this.state.tickerDisplayMode}
/>
<ul id="stock-market-list">
<ul id="stock-market-list" ref={this.listRef}>
{tickers}
</ul>
</div>

@ -15,6 +15,8 @@ export enum TickerDisplayMode {
type IProps = {
changeDisplayMode: () => void;
changeWatchlistFilter: (e: React.ChangeEvent<HTMLInputElement>) => void;
collapseAllTickers: () => void;
expandAllTickers: () => void;
tickerDisplayMode: TickerDisplayMode;
}
@ -47,6 +49,14 @@ export class StockTickersConfig extends React.Component<IProps, any> {
return (
<div>
{this.renderDisplayModeButton()}
<StdButton
onClick={this.props.expandAllTickers}
text="Expand Tickers"
/>
<StdButton
onClick={this.props.collapseAllTickers}
text="Collapse Tickers"
/>
<input
className="text-input"

@ -6,6 +6,7 @@ import * as React from "react";
type IProps = {
headerContent: React.ReactElement;
panelContent: React.ReactElement;
panelInitiallyOpened?: boolean;
}
type IState = {
@ -19,7 +20,7 @@ export class Accordion extends React.Component<IProps, IState> {
this.handleHeaderClick = this.handleHeaderClick.bind(this);
this.state = {
panelOpened: false,
panelOpened: props.panelInitiallyOpened ? true : false,
}
}
@ -28,16 +29,17 @@ export class Accordion extends React.Component<IProps, IState> {
elem.classList.toggle("active");
const panel: HTMLElement = elem.nextElementSibling as HTMLElement;
if (panel!.style.display === "block") {
panel!.style.display = "none";
this.setState({
panelOpened: false,
});
} else {
const active = elem.classList.contains("active");
if (active) {
panel!.style.display = "block";
this.setState({
panelOpened: true,
});
} else {
panel!.style.display = "none";
this.setState({
panelOpened: false,
});
}
}