This commit is contained in:
danielyxie 2019-04-28 23:22:02 -07:00
commit 9df054dd0c
45 changed files with 4064 additions and 1983 deletions

@ -7,35 +7,44 @@
p {
font-size: $defaultFontSize * 0.8125;
}
a {
font-size: $defaultFontSize * 0.875;
}
h2 {
}
.stock-market-info-and-purchases {
> h2 {
display: block;
margin-top: 10px;
margin-left: 10px;
}
> p {
display: block;
margin-left: 10px;
width: 70%;
}
> a, > button {
margin: 10px;
}
}
#stock-market-list li {
button {
font-size: $defaultFontSize;
#stock-market-list {
list-style: none;
li {
button {
font-size: $defaultFontSize;
}
}
}
#stock-market-container p {
padding: 6px;
margin: 6px;
width: 70%;
}
#stock-market-container a {
margin: 10px;
}
#stock-market-watchlist-filter {
margin: 5px 5px 5px 10px;
padding: 4px;
width: 50%;
margin-left: 10px;
}
.stock-market-input {
@ -47,14 +56,36 @@
color: var(--my-font-color);
}
.stock-market-price-movement-warning {
border: 1px solid white;
color: red;
margin: 2px;
padding: 2px;
}
.stock-market-position-text {
color: #fff;
display: inline-block;
display: block;
p {
color: #fff;
display: inline-block;
margin: 4px;
}
h3 {
margin: 4px;
}
}
.stock-market-order-list {
overflow-y: auto;
max-height: 100px;
li {
color: #fff;
padding: 4px;
}
}
.stock-market-order-cancel-btn {

@ -7,10 +7,14 @@ buy and sell stocks in order to make money.
The WSE can be found in the 'City' tab, and is accessible in every city.
Automating the Stock Market
^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can write scripts to perform automatic and algorithmic trading on the Stock Market.
See :ref:`netscript_tixapi` for more details.
Fundamentals
------------
The Stock Market is not as simple as "buy at price X and sell at price Y". The following
are several fundamental concepts you need to understand about the stock market.
.. note:: For those that have experience with finance/trading/investing, please be aware
that the game's stock market does not function exactly like it does in the real
world. So these concepts below should seem similar, but won't be exactly the same.
Positions: Long vs Short
^^^^^^^^^^^^^^^^^^^^^^^^
@ -21,16 +25,61 @@ is the exact opposite. In a Short position you purchase shares of a stock and
earn a profit if the price of that stock decreases. This is also called 'shorting'
a stock.
NOTE: Shorting stocks is not available immediately, and must be unlocked later in the
game.
.. note:: Shorting stocks is not available immediately, and must be unlocked later in the
game.
.. _gameplay_stock_market_spread:
Spread (Bid Price & Ask Price)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The **bid price** is the maximum price at which someone will buy a stock on the
stock market.
The **ask price** is the minimum price that a seller is willing to receive for a stock
on the stock market
The ask price will always be higher than the bid price (This is because if a seller
is willing to receive less than the bid price, that transaction is guaranteed to
happen). The difference between the bid and ask price is known as the **spread**.
A stock's "price" will be the average of the bid and ask price.
The bid and ask price are important because these are the prices at which a
transaction actually occurs. If you purchase a stock in the long position, the cost
of your purchase depends on that stock's ask price. If you then try to sell that
stock (still in the long position), the price at which you sell is the stock's
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:
Transactions Influencing Stock Price
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Buying or selling a large number of shares of a stock will influence that stock's price.
Buying a stock in the long position will cause that stock's price to
increase. Selling a stock in the long position will cause the stock's price to decrease.
The reverse occurs for the short position. The effect of this depends on how many shares
are being transacted. More shares will have a bigger effect on the stock price. If
only a small number of shares are being transacted, the stock price may not be affected.
Note that this "influencing" of the stock price **can happen in the middle of a transaction**.
For example, assume you are selling 1m shares of a stock. That stock's bid price
is currently $100. However, selling 100k shares of the stock causes its price to drop
by 1%. This means that for your transaction of 1m shares, the first 100k shares will be
sold at $100. Then the next 100k shares will be sold at $99. Then the next 100k shares will
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**.
Order Types
^^^^^^^^^^^
There are three different types of orders you can make to buy or sell stocks on the exchange:
Market Order, Limit Order, and Stop Order.
Note that Limit Orders and Stop Orders are not available immediately, and must be unlocked
later in the game.
.. note:: Limit Orders and Stop Orders are not available immediately, and must be unlocked
later in the game.
When you place a Market Order to buy or sell a stock, the order executes immediately at
whatever the current price of the stock is. For example if you choose to short a stock
@ -71,3 +120,8 @@ A Limit Order to sell will execute if the stock's price <= order's price
A Stop Order to buy will execute if the stock's price <= order's price
A Stop Order to sell will execute if the stock's price >= order's price.
Automating the Stock Market
---------------------------
You can write scripts to perform automatic and algorithmic trading on the Stock Market.
See :ref:`netscript_tixapi` for more details.

@ -15,11 +15,15 @@ access even after you 'reset' by installing Augmentations
.. toctree::
:caption: API Functions:
getStockSymbols() <tixapi/getStockSymbols>
getStockPrice() <tixapi/getStockPrice>
getStockAskPrice() <tixapi/getStockAskPrice>
getStockBidPrice() <tixapi/getStockBidPrice>
getStockPosition() <tixapi/getStockPosition>
getStockMaxShares() <tixapi/getStockMaxShares>
getStockPurchaseCost() <tixapi/getStockPurchaseCost>
getStockSaleGain() <tixapi/getStockSaleGain>
buyStock() <tixapi/buyStock>
sellStock() <tixapi/sellStock>
shortStock() <tixapi/shortStock>

@ -0,0 +1,12 @@
getStockAskPrice() Netscript Function
=====================================
.. js:function:: getStockAskPrice(sym)
:param string sym: Stock symbol
:RAM cost: 2 GB
Given a stock's symbol, returns the ask price of that stock (the symbol is a sequence
of two to four capital letters, **not** the name of the company to which that stock belongs).
See :ref:`gameplay_stock_market_spread` for details on what the ask price is.

@ -0,0 +1,12 @@
getStockBidPrice() Netscript Function
=====================================
.. js:function:: getStockBidPrice(sym)
:param string sym: Stock symbol
:RAM cost: 2 GB
Given a stock's symbol, returns the bid price of that stock (the symbol is a sequence
of two to four capital letters, **not** the name of the company to which that stock belongs).
See :ref:`gameplay_stock_market_spread` for details on what the bid price is.

@ -6,9 +6,12 @@ getStockPrice() Netscript Function
:param string sym: Stock symbol
:RAM cost: 2 GB
Returns the price of a stock, given its symbol (NOT the company name). The symbol is a sequence
of two to four capital letters.
Given a stock's symbol, returns the price of that stock (the symbol is a sequence
of two to four capital letters, **not** the name of the company to which that stock belongs).
.. note:: The stock's price is the average of its bid and ask price.
See :ref:`gameplay_stock_market_spread` for details on what this means.
Example::
getStockPrice("FISG");
getStockPrice("FSIG");

@ -0,0 +1,15 @@
getStockPurchaseCost() Netscript Function
=========================================
.. js:function:: getStockPurchaseCost(sym, shares, posType)
:param string sym: Stock symbol
:param number shares: Number of shares to purchase
:param string posType: Specifies whether the order is a "Long" or "Short" position.
The values "L" or "S" can also be used.
:RAM cost: 2 GB
Calculates and returns how much it would cost to buy a given number of
shares of a stock. This takes into account :ref:`spread <gameplay_stock_market_spread>`,
:ref:`large transactions influencing the price of the stock <gameplay_stock_spread_price_movement>`
and commission fees.

@ -0,0 +1,15 @@
getStockSaleGain() Netscript Function
=====================================
.. js:function:: getStockSaleGain(sym, shares, posType)
:param string sym: Stock symbol
:param number shares: Number of shares to purchase
:param string posType: Specifies whether the order is a "Long" or "Short" position.
The values "L" or "S" can also be used.
:RAM cost: 2 GB
Calculates and returns how much 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>`
and commission fees.

123
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "bitburner",
"version": "0.45.0",
"version": "0.46.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -928,12 +928,6 @@
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true
},
"assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true
},
"assign-symbols": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@ -1319,12 +1313,6 @@
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true
},
"browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
"dev": true
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@ -1626,29 +1614,6 @@
"lazy-cache": "1.0.4"
}
},
"chai": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
"integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=",
"dev": true,
"requires": {
"assertion-error": "1.1.0",
"check-error": "1.0.2",
"deep-eql": "3.0.1",
"get-func-name": "2.0.0",
"pathval": "1.1.0",
"type-detect": "4.0.8"
}
},
"chai-as-promised": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
"integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
"dev": true,
"requires": {
"check-error": "1.0.2"
}
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
@ -1700,12 +1665,6 @@
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
"dev": true
},
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"chokidar": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz",
@ -2870,15 +2829,6 @@
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
},
"deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"requires": {
"type-detect": "4.0.8"
}
},
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
@ -4440,12 +4390,6 @@
"integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=",
"dev": true
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true
},
"get-stdin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
@ -4654,12 +4598,6 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
},
"growl": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
"dev": true
},
"handle-thing": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
@ -6852,59 +6790,6 @@
"minimist": "0.0.8"
}
},
"mocha": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
"integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
"dev": true,
"requires": {
"browser-stdout": "1.3.1",
"commander": "2.15.1",
"debug": "3.1.0",
"diff": "3.5.0",
"escape-string-regexp": "1.0.5",
"glob": "7.1.2",
"growl": "1.10.5",
"he": "1.1.1",
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"supports-color": "5.4.0"
},
"dependencies": {
"commander": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
"dev": true
},
"diff": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
}
}
},
"mocha-lcov-reporter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/mocha-lcov-reporter/-/mocha-lcov-reporter-1.3.0.tgz",
"integrity": "sha1-Rpve9PivyaEWBW8HnfYYLQr7A4Q=",
"dev": true
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -7734,12 +7619,6 @@
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
"dev": true
},
"pathval": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
"dev": true
},
"pbkdf2": {
"version": "3.0.16",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz",

@ -48,8 +48,6 @@
"beautify-lint": "^1.0.3",
"benchmark": "^2.1.1",
"bundle-loader": "~0.5.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"css-loader": "^0.28.11",
"es6-promise-polyfill": "^1.1.1",
"eslint": "^4.19.1",
@ -65,8 +63,6 @@
"lodash": "^4.17.10",
"mini-css-extract-plugin": "^0.4.1",
"mkdirp": "^0.5.1",
"mocha": "^5.2.0",
"mocha-lcov-reporter": "^1.0.0",
"node-sass": "^4.10.0",
"raw-loader": "~0.5.0",
"sass-loader": "^7.0.3",

@ -275,35 +275,15 @@ export let CONSTANTS: IMap<any> = {
LatestUpdate:
`
v0.46.3
* Added a new Augmentation: The Shadow's Simulacrum
* Improved tab autocompletion feature in Terminal so that it works better with directories
* Bug Fix: Tech vendor location UI now properly refreshed when purchasing a TOR router
* Bug Fix: Fixed UI issue with faction donations
* Bug Fix: The money statistics & breakdown should now properly track money earned from Hacknet Server (hashes -> money)
* Bug Fix: Fixed issue with changing input in 'Minimum Path Sum in a Triangle' coding contract problem
* Fixed several typos in various places
v0.47.0
* Stock Market changes:
** Implemented spread. Stock's now have bid and ask prices at which transactions occur
** Large transactions will now influence a stock's price.
** This "influencing" can take effect in the middle of a transaction
** See documentation for more details on these changes
** Added getStockAskPrice(), getStockBidPrice() Netscript functions to the TIX API
** Added getStockPurchaseCost(), getStockSaleGain() Netscript functions to the TIX API
v0.46.2
* Source-File 2 now allows you to form gangs in other BitNodes when your karma reaches a very large negative value
** (Karma is a hidden stat and is lowered by committing crimes)
* Gang changes:
** Bug Fix: Gangs can no longer clash with themselve
** Bug Fix: Winning against another gang should properly reduce their power
* Bug Fix: Terminal 'wget' command now works properly
* Bug Fix: Hacknet Server Hash upgrades now properly reset upon installing Augs/switching BitNodes
* Bug Fix: Fixed button for creating Corporations
v0.46.1
* Added a very rudimentary directory system to the Terminal
** Details here: https://bitburner.readthedocs.io/en/latest/basicgameplay/terminal.html#filesystem-directories
* Added numHashes(), hashCost(), and spendHashes() functions to the Netscript Hacknet Node API
* 'Generate Coding Contract' hash upgrade is now more expensive
* 'Generate Coding Contract' hash upgrade now generates the contract randomly on the server, rather than on home computer
* The cost of selling hashes for money no longer increases each time
* Selling hashes for money now costs 4 hashes (in exchange for $1m)
* Bug Fix: Hacknet Node earnings should work properly when game is inactive/offline
* Bug Fix: Duplicate Sleeve augmentations are now properly reset when switching to a new BitNode
`
}

@ -75,20 +75,22 @@ import { SpecialServerIps } from "./Server/SpecialServerIps";
import { Stock } from "./StockMarket/Stock";
import {
StockMarket,
StockSymbols,
SymbolToStockMap,
initStockMarket,
initSymbolToStockMap,
buyStock,
sellStock,
updateStockPlayerPosition,
shortStock,
sellShort,
OrderTypes,
PositionTypes,
placeOrder,
cancelOrder
} from "./StockMarket/StockMarket";
import {
getBuyTransactionCost,
getSellTransactionGain,
} from "./StockMarket/StockMarketHelpers";
import { OrderTypes } from "./StockMarket/data/OrderTypes";
import { PositionTypes } from "./StockMarket/data/PositionTypes";
import { StockSymbols } from "./StockMarket/data/StockSymbols";
import {
getStockmarket4SDataCost,
getStockMarket4STixApiCost
@ -282,6 +284,29 @@ function NetscriptFunctions(workerScript) {
return server;
}
/**
* Checks if the player has TIX API access. Throws an error if the player does not
*/
const checkTixApiAccess = function(callingFn="") {
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, `You don't have TIX API Access! Cannot use ${callingFn}()`);
}
}
/**
* Gets a stock, given its symbol. Throws an error if the symbol is invalid
* @param {string} symbol - Stock's symbol
* @returns {Stock} stock object
*/
const getStockFromSymbol = function(symbol, callingFn="") {
const stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, `Invalid stock symbol passed into ${callingFn}()`);
}
return stock;
}
/**
* Used to fail a function if the function's target is a Hacknet Server.
* This is used for functions that should run on normal Servers, but not Hacknet Servers
@ -930,7 +955,7 @@ function NetscriptFunctions(workerScript) {
if (scriptname === undefined) {
throw makeRuntimeRejectMsg(workerScript, "run() call has incorrect number of arguments. Usage: run(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 = [];
@ -1587,9 +1612,7 @@ function NetscriptFunctions(workerScript) {
return updateStaticRam("getStockSymbols", CONSTANTS.ScriptGetStockRamCost);
}
updateDynamicRam("getStockSymbols", CONSTANTS.ScriptGetStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use getStockSymbols()");
}
checkTixApiAccess("getStockSymbols");
return Object.values(StockSymbols);
},
getStockPrice : function(symbol) {
@ -1597,23 +1620,37 @@ function NetscriptFunctions(workerScript) {
return updateStaticRam("getStockPrice", CONSTANTS.ScriptGetStockRamCost);
}
updateDynamicRam("getStockPrice", CONSTANTS.ScriptGetStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use getStockPrice()");
checkTixApiAccess("getStockPrice");
const stock = getStockFromSymbol(symbol, "getStockPrice");
return stock.price;
},
getStockAskPrice : function(symbol) {
if (workerScript.checkingRam) {
return updateStaticRam("getStockAskPrice", CONSTANTS.ScriptGetStockRamCost);
}
var stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, "Invalid stock symbol passed into getStockPrice()");
updateDynamicRam("getStockAskPrice", CONSTANTS.ScriptGetStockRamCost);
checkTixApiAccess("getStockAskPrice");
const stock = getStockFromSymbol(symbol, "getStockAskPrice");
return stock.getAskPrice();
},
getStockBidPrice : function(symbol) {
if (workerScript.checkingRam) {
return updateStaticRam("getStockBidPrice", CONSTANTS.ScriptGetStockRamCost);
}
return parseFloat(stock.price.toFixed(3));
updateDynamicRam("getStockBidPrice", CONSTANTS.ScriptGetStockRamCost);
checkTixApiAccess("getStockBidPrice");
const stock = getStockFromSymbol(symbol, "getStockBidPrice");
return stock.getBidPrice();
},
getStockPosition : function(symbol) {
if (workerScript.checkingRam) {
return updateStaticRam("getStockPosition", CONSTANTS.ScriptGetStockRamCost);
}
updateDynamicRam("getStockPosition", CONSTANTS.ScriptGetStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use getStockPosition()");
}
checkTixApiAccess("getStockPosition");
var stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, "Invalid stock symbol passed into getStockPosition()");
@ -1625,166 +1662,126 @@ function NetscriptFunctions(workerScript) {
return updateStaticRam("getStockMaxShares", CONSTANTS.ScriptGetStockRamCost);
}
updateDynamicRam("getStockMaxShares", CONSTANTS.ScriptGetStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use getStockMaxShares()");
}
const stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, "Invalid stock symbol passed into getStockMaxShares()");
}
checkTixApiAccess("getStockMaxShares");
const stock = getStockFromSymbol(symbol, "getStockMaxShares");
return stock.maxShares;
},
getStockPurchaseCost : function(symbol, shares, posType) {
if (workerScript.checkingRam) {
return updateStaticRam("getStockPurchaseCost", CONSTANTS.ScriptGetStockRamCost);
}
updateDynamicRam("getStockPurchaseCost", CONSTANTS.ScriptGetStockRamCost);
checkTixApiAccess("getStockPurchaseCost");
const stock = getStockFromSymbol(symbol, "getStockPurchaseCost");
shares = Math.round(shares);
let pos;
const sanitizedPosType = posType.toLowerCase();
if (sanitizedPosType.includes("l")) {
pos = PositionTypes.Long;
} else if (sanitizedPosType.includes("s")) {
pos = PositionTypes.Short;
} else {
return Infinity;
}
const res = getBuyTransactionCost(stock, shares, pos);
if (res == null) { return Infinity; }
return res;
},
getStockSaleGain : function(symbol, shares, posType) {
if (workerScript.checkingRam) {
return updateStaticRam("getStockSaleGain", CONSTANTS.ScriptGetStockRamCost);
}
updateDynamicRam("getStockSaleGain", CONSTANTS.ScriptGetStockRamCost);
checkTixApiAccess("getStockSaleGain");
const stock = getStockFromSymbol(symbol, "getStockSaleGain");
shares = Math.round(shares);
let pos;
const sanitizedPosType = posType.toLowerCase();
if (sanitizedPosType.includes("l")) {
pos = PositionTypes.Long;
} else if (sanitizedPosType.includes("s")) {
pos = PositionTypes.Short;
} else {
return 0;
}
const res = getSellTransactionGain(stock, shares, pos);
if (res == null) { return 0; }
return res;
},
buyStock : function(symbol, shares) {
if (workerScript.checkingRam) {
return updateStaticRam("buyStock", CONSTANTS.ScriptBuySellStockRamCost);
}
updateDynamicRam("buyStock", CONSTANTS.ScriptBuySellStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use buyStock()");
}
var stock = SymbolToStockMap[symbol];
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;}
checkTixApiAccess("buyStock");
const stock = getStockFromSymbol(symbol, "buyStock");
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) {
return updateStaticRam("sellStock", CONSTANTS.ScriptBuySellStockRamCost);
}
updateDynamicRam("sellStock", CONSTANTS.ScriptBuySellStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use sellStock()");
}
var stock = SymbolToStockMap[symbol];
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);
checkTixApiAccess("sellStock");
const stock = getStockFromSymbol(symbol, "sellStock");
const res = sellStock(stock, shares, workerScript);
// 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");
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) {
shortStock : function(symbol, shares) {
if (workerScript.checkingRam) {
return updateStaticRam("shortStock", CONSTANTS.ScriptBuySellStockRamCost);
}
updateDynamicRam("shortStock", CONSTANTS.ScriptBuySellStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use shortStock()");
}
checkTixApiAccess("shortStock");
if (Player.bitNodeN !== 8) {
if (!(hasWallStreetSF && wallStreetSFLvl >= 2)) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Cannot use shortStock(). You must either be in BitNode-8 or you must have Level 2 of Source-File 8");
}
}
var stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid stock symbol passed into shortStock()");
}
var res = shortStock(stock, shares, workerScript);
const stock = getStockFromSymbol(symbol, "shortStock");
const res = shortStock(stock, shares, workerScript);
return res ? stock.price : 0;
},
sellShort(symbol, shares) {
sellShort : function(symbol, shares) {
if (workerScript.checkingRam) {
return updateStaticRam("sellShort", CONSTANTS.ScriptBuySellStockRamCost);
}
updateDynamicRam("sellShort", CONSTANTS.ScriptBuySellStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use sellShort()");
}
checkTixApiAccess("sellShort");
if (Player.bitNodeN !== 8) {
if (!(hasWallStreetSF && wallStreetSFLvl >= 2)) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Cannot use sellShort(). You must either be in BitNode-8 or you must have Level 2 of Source-File 8");
}
}
var stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid stock symbol passed into sellShort()");
}
var res = sellShort(stock, shares, workerScript);
const stock = getStockFromSymbol(symbol, "sellShort");
const res = sellShort(stock, shares, workerScript);
return res ? stock.price : 0;
},
placeOrder(symbol, shares, price, type, pos) {
placeOrder : function(symbol, shares, price, type, pos) {
if (workerScript.checkingRam) {
return updateStaticRam("placeOrder", CONSTANTS.ScriptBuySellStockRamCost);
}
updateDynamicRam("placeOrder", CONSTANTS.ScriptBuySellStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use placeOrder()");
}
checkTixApiAccess("placeOrder");
if (Player.bitNodeN !== 8) {
if (!(hasWallStreetSF && wallStreetSFLvl >= 3)) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Cannot use placeOrder(). You must either be in BitNode-8 or have Level 3 of Source-File 8");
}
}
var stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid stock symbol passed into placeOrder()");
}
const stock = getStockFromSymbol(symbol, "placeOrder");
var orderType, orderPos;
type = type.toLowerCase();
if (type.includes("limit") && type.includes("buy")) {
@ -1810,23 +1807,18 @@ function NetscriptFunctions(workerScript) {
return placeOrder(stock, shares, price, orderType, orderPos, workerScript);
},
cancelOrder(symbol, shares, price, type, pos) {
cancelOrder : function(symbol, shares, price, type, pos) {
if (workerScript.checkingRam) {
return updateStaticRam("cancelOrder", CONSTANTS.ScriptBuySellStockRamCost);
}
updateDynamicRam("cancelOrder", CONSTANTS.ScriptBuySellStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use cancelOrder()");
}
checkTixApiAccess("cancelOrder");
if (Player.bitNodeN !== 8) {
if (!(hasWallStreetSF && wallStreetSFLvl >= 3)) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Cannot use cancelOrder(). You must either be in BitNode-8 or have Level 3 of Source-File 8");
}
}
var stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid stock symbol passed into cancelOrder()");
}
const stock = getStockFrom(symbol, "cancelOrder");
if (isNaN(shares) || isNaN(price)) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid shares or price argument passed into cancelOrder(). Must be numeric");
}
@ -1903,10 +1895,8 @@ function NetscriptFunctions(workerScript) {
if (!Player.has4SDataTixApi) {
throw makeRuntimeRejectMsg(workerScript, "You don't have 4S Market Data TIX API Access! Cannot use getStockVolatility()");
}
var stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid stock symbol passed into getStockVolatility()");
}
const stock = getStockFromSymbol(symbol, "getStockVolatility");
return stock.mv / 100; // Convert from percentage to decimal
},
getStockForecast : function(symbol) {
@ -1917,10 +1907,8 @@ function NetscriptFunctions(workerScript) {
if (!Player.has4SDataTixApi) {
throw makeRuntimeRejectMsg(workerScript, "You don't have 4S Market Data TIX API Access! Cannot use getStockForecast()");
}
var stock = SymbolToStockMap[symbol];
if (stock == null) {
throw makeRuntimeRejectMsg(workerScript, "ERROR: Invalid stock symbol passed into getStockForecast()");
}
const stock = getStockFromSymbol(symbol, "getStockForecast");
var forecast = 50;
stock.b ? forecast += stock.otlkMag : forecast -= stock.otlkMag;
return forecast / 100; // Convert from percentage to decimal
@ -1930,10 +1918,7 @@ function NetscriptFunctions(workerScript) {
return updateStaticRam("purchase4SMarketData", CONSTANTS.ScriptBuySellStockRamCost);
}
updateDynamicRam("purchase4SMarketData", CONSTANTS.ScriptBuySellStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use purchase4SMarketData()");
}
checkTixApiAccess("purchase4SMarketData");
if (Player.has4SData) {
if (workerScript.shouldLog("purchase4SMarketData")) {
@ -1961,10 +1946,7 @@ function NetscriptFunctions(workerScript) {
return updateStaticRam("purchase4SMarketDataTixApi", CONSTANTS.ScriptBuySellStockRamCost);
}
updateDynamicRam("purchase4SMarketDataTixApi", CONSTANTS.ScriptBuySellStockRamCost);
if (!Player.hasTixApiAccess) {
throw makeRuntimeRejectMsg(workerScript, "You don't have TIX API Access! Cannot use purchase4SMarketDataTixApi()");
}
checkTixApiAccess("purchase4SMarketDataTixApi");
if (Player.has4SDataTixApi) {
if (workerScript.shouldLog("purchase4SMarketDataTixApi")) {

@ -32,7 +32,10 @@ export interface IPlayer {
factions: string[];
firstTimeTraveled: boolean;
hacknetNodes: (HacknetNode | string)[]; // HacknetNode object or IP of Hacknet Server
has4SData: boolean;
has4SDataTixApi: boolean;
hashManager: HashManager;
hasTixApiAccess: boolean;
hasWseAccount: boolean;
homeComputer: string;
hp: number;

@ -47,8 +47,6 @@ import {
import {
initStockMarket,
initSymbolToStockMap,
stockMarketContentCreated,
setStockMarketContentCreated
} from "./StockMarket/StockMarket";
import { Terminal, postNetburnerText } from "./Terminal";
@ -163,13 +161,6 @@ function prestigeAugmentation() {
initStockMarket();
initSymbolToStockMap();
}
setStockMarketContentCreated(false);
var stockMarketList = document.getElementById("stock-market-list");
while(stockMarketList.firstChild) {
stockMarketList.removeChild(stockMarketList.firstChild);
}
var watchlist = document.getElementById("stock-market-watchlist-filter");
watchlist.value = ""; // Reset watchlist filter
// Refresh Main Menu (the 'World' menu, specifically)
document.getElementById("world-menu-header").click();
@ -336,11 +327,6 @@ function prestigeSourceFile() {
initStockMarket();
initSymbolToStockMap();
}
setStockMarketContentCreated(false);
var stockMarketList = document.getElementById("stock-market-list");
while(stockMarketList.firstChild) {
stockMarketList.removeChild(stockMarketList.firstChild);
}
if (Player.inGang()) { Player.gang.clearUI(); }
Player.gang = null;

@ -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.

@ -0,0 +1,5 @@
import { Order } from "./Order";
export interface IOrderBook {
[key: string]: Order[];
}

@ -0,0 +1,10 @@
import { IOrderBook } from "./IOrderBook";
import { Stock } from "./Stock";
export type IStockMarket = {
[key: string]: Stock;
} & {
lastUpdate: number;
storedCycles: number;
Orders: IOrderBook;
}

45
src/StockMarket/Order.ts Normal file

@ -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;

@ -1,6 +1,58 @@
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 <number>n;
}
case "object": {
const range = <IMinMaxRange>n;
value = getRandomInt(range.min, range.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.
*/
@ -22,6 +74,11 @@ export class Stock {
*/
readonly cap: number;
/**
* Stocks previous share price
*/
lastPrice: number;
/**
* Maximum number of shares that player can own (both long and short combined)
*/
@ -63,16 +120,33 @@ export class Stock {
*/
playerShortShares: number;
/**
* The HTML element that displays the stock's info in the UI
*/
posTxtEl: HTMLElement | null;
/**
* Stock's share price
*/
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,34 +159,50 @@ 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.lastPrice = this.price;
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;
this.maxShares = Math.round((this.totalShares * outstandingSharePercentage) / 1e5) * 1e5;
}
this.posTxtEl = null;
changePrice(newPrice: number): void {
this.lastPrice = this.price;
this.price = newPrice;
}
/**
* 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));
}
/**

File diff suppressed because it is too large Load Diff

@ -0,0 +1,625 @@
import { Order } from "./Order";
import { Stock } from "./Stock";
import {
getBuyTransactionCost,
getSellTransactionGain,
processBuyTransactionPriceMovement,
processSellTransactionPriceMovement
} from "./StockMarketHelpers";
import {
getStockMarket4SDataCost,
getStockMarket4STixApiCost
} from "./StockMarketCosts";
import { InitStockMetadata } from "./data/InitStockMetadata";
import { OrderTypes } from "./data/OrderTypes";
import { PositionTypes } from "./data/PositionTypes";
import { StockSymbols } from "./data/StockSymbols";
import { StockMarketRoot } from "./ui/Root";
import { CONSTANTS } from "../Constants";
import { WorkerScript } from "../NetscriptWorker";
import { Player } from "../Player";
import { Page, routing } from ".././ui/navigationTracking";
import { numeralWrapper } from ".././ui/numeralFormat";
import { dialogBoxCreate } from "../../utils/DialogBox";
import { Reviver } from "../../utils/JSONReviver";
import React from "react";
import ReactDOM from "react-dom";
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)) {
if (tixApi) {
workerScript.scriptRef.log("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument");
} else {
dialogBoxCreate("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument");
}
return false;
}
if (StockMarket["Orders"] == null) {
const orders = {};
for (const name in StockMarket) {
if (StockMarket.hasOwnProperty(name)) {
const stk = StockMarket[name];
if (!(stk instanceof Stock)) { continue; }
orders[stk.symbol] = [];
}
}
StockMarket["Orders"] = orders;
}
StockMarket["Orders"][stock.symbol].push(order);
//Process to see if it should be executed immediately
processOrders(order.stock, order.type, order.pos);
displayStockMarketContent();
return true;
}
// 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) {
var order = params.order;
// An 'Order' object is passed in
var stockOrders = StockMarket["Orders"][order.stock.symbol];
for (var i = 0; i < stockOrders.length; ++i) {
if (order == stockOrders[i]) {
stockOrders.splice(i, 1);
displayStockMarketContent();
return true;
}
}
return false;
} else if (params.stock && params.shares && params.price && params.type &&
params.pos && params.stock instanceof Stock) {
// Order properties are passed in. Need to look for the order
var stockOrders = StockMarket["Orders"][params.stock.symbol];
var orderTxt = params.stock.symbol + " - " + params.shares + " @ " +
numeralWrapper.format(params.price, '$0.000a');
for (var i = 0; i < stockOrders.length; ++i) {
var order = stockOrders[i];
if (params.shares === order.shares &&
params.price === order.price &&
params.type === order.type &&
params.pos === order.pos) {
stockOrders.splice(i, 1);
displayStockMarketContent();
if (tixApi) {
workerScript.scriptRef.log("Successfully cancelled order: " + orderTxt);
}
return true;
}
}
if (tixApi) {
workerScript.scriptRef.log("Failed to cancel order: " + orderTxt);
}
return false;
}
return false;
}
function executeOrder(order) {
var stock = order.stock;
var orderBook = StockMarket["Orders"];
var stockOrders = orderBook[stock.symbol];
var res = true;
console.log("Executing the following order:");
console.log(order);
switch (order.type) {
case OrderTypes.LimitBuy:
case OrderTypes.StopBuy:
if (order.pos === PositionTypes.Long) {
res = buyStock(order.stock, order.shares) && res;
} else if (order.pos === PositionTypes.Short) {
res = shortStock(order.stock, order.shares) && res;
}
break;
case OrderTypes.LimitSell:
case OrderTypes.StopSell:
if (order.pos === PositionTypes.Long) {
res = sellStock(order.stock, order.shares) && res;
} else if (order.pos === PositionTypes.Short) {
res = sellShort(order.stock, order.shares) && res;
}
break;
}
if (res) {
// Remove order from order book
for (var i = 0; i < stockOrders.length; ++i) {
if (order == stockOrders[i]) {
stockOrders.splice(i, 1);
displayStockMarketContent();
return;
}
}
console.log("ERROR: Could not find the following Order in Order Book: ");
console.log(order);
} else {
console.log("Order failed to execute");
}
}
export let StockMarket = {}; // Maps full stock name -> Stock object
export let SymbolToStockMap = {}; // Maps symbol -> Stock object
export function loadStockMarket(saveString) {
if (saveString === "") {
StockMarket = {};
} else {
StockMarket = JSON.parse(saveString, Reviver);
}
}
export function initStockMarket() {
for (const stk in StockMarket) {
if (StockMarket.hasOwnProperty(stk)) {
delete StockMarket[stk];
}
}
for (const metadata of InitStockMetadata) {
const name = metadata.name;
StockMarket[name] = new Stock(metadata);
}
const orders = {};
for (const name in StockMarket) {
if (StockMarket.hasOwnProperty(name)) {
const stock = StockMarket[name];
if (!(stock instanceof Stock)) { continue; }
orders[stock.symbol] = [];
}
}
StockMarket["Orders"] = orders;
StockMarket.storedCycles = 0;
StockMarket.lastUpdate = 0;
}
export function initSymbolToStockMap() {
for (const name in StockSymbols) {
if (StockSymbols.hasOwnProperty(name)) {
const stock = StockMarket[name];
if (stock == null) {
console.error(`Could not find Stock for ${name}`);
continue;
}
const symbol = StockSymbols[name];
SymbolToStockMap[symbol] = stock;
}
}
}
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;
}
}
}
}
/**
* 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)) {
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?
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)}`);
} 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) {
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;
}
const origTotal = stock.playerShares * stock.playerAvgPx;
Player.loseMoney(totalPrice);
const newTotal = origTotal + totalPrice;
stock.playerShares = Math.round(stock.playerShares + shares);
stock.playerAvgPx = newTotal / stock.playerShares;
processBuyTransactionPriceMovement(stock, shares, PositionTypes.Long);
displayStockMarketContent();
const resultTxt = `Bought ${numeralWrapper.format(shares, '0,0')} shares of ${stock.symbol} for ${numeralWrapper.formatMoney(totalPrice)}. ` +
`Paid ${numeralWrapper.formatMoney(CONSTANTS.StockMarketCommission)} in commission fees.`
if (tixApi) {
if (workerScript.shouldLog("buyStock")) { workerScript.log(resultTxt); }
} else {
dialogBoxCreate(resultTxt);
}
return true;
}
/**
* 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)) {
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 = getSellTransactionGain(stock, shares, PositionTypes.Long);
let netProfit = gains - (stock.playerAvgPx * shares);
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) {
stock.playerAvgPx = 0;
}
processSellTransactionPriceMovement(stock, shares, PositionTypes.Long);
displayStockMarketContent();
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); }
} else {
dialogBoxCreate(resultTxt);
}
return true;
}
/**
* 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.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");
}
return false;
}
// Does the player have enough money?
const totalPrice = getBuyTransactionCost(stock, shares, PositionTypes.Short);
if (Player.money.lt(totalPrice + CONSTANTS.StockMarketCommission)) {
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));
} else {
dialogBoxCreate("You do not have enough money to purchase this short position. 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) {
if (tixApi) {
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.`);
}
return false;
}
const origTotal = stock.playerShortShares * stock.playerAvgShortPx;
Player.loseMoney(totalPrice + CONSTANTS.StockMarketCommission);
const newTotal = origTotal + totalPrice;
stock.playerShortShares = Math.round(stock.playerShortShares + shares);
stock.playerAvgShortPx = newTotal / stock.playerShortShares;
processBuyTransactionPriceMovement(stock, shares, PositionTypes.Short);
displayStockMarketContent();
const resultTxt = `Bought a short position of ${numeralWrapper.format(shares, '0,0')} shares of ${stock.symbol} ` +
`for ${numeralWrapper.formatMoney(totalPrice)}. Paid ${numeralWrapper.formatMoney(CONSTANTS.StockMarketCommission)} ` +
`in commission fees.`;
if (tixApi) {
if (workerScript.shouldLog("shortStock")) { workerScript.log(resultTxt); }
} else {
dialogBoxCreate(resultTxt);
}
return true;
}
/**
* 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.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;}
const origCost = shares * stock.playerAvgShortPx;
const totalGain = getSellTransactionGain(stock, shares, PositionTypes.Short);
if (totalGain == null || isNaN(totalGain) || origCost == null) {
if (tixApi) {
workerScript.log(`Failed to sell short position in a stock. This is probably either due to invalid arguments, or a bug`);
} else {
dialogBoxCreate(`Failed to sell short position in a stock. This is probably either due to invalid arguments, or a bug`);
}
return false;
}
let profit = totalGain - origCost;
if (isNaN(profit)) { profit = 0; }
Player.gainMoney(totalGain);
Player.recordMoneySource(profit, "stock");
if (tixApi) {
workerScript.scriptRef.onlineMoneyMade += profit;
Player.scriptProdSinceLastAug += profit;
}
stock.playerShortShares = Math.round(stock.playerShortShares - shares);
if (stock.playerShortShares === 0) {
stock.playerAvgShortPx = 0;
}
processSellTransactionPriceMovement(stock, shares, PositionTypes.Short);
displayStockMarketContent();
const resultTxt = `Sold your short position of ${numeralWrapper.format(shares, '0,0')} shares of ${stock.symbol}. ` +
`After commissions, you gained a total of ${numeralWrapper.formatMoney(totalGain)}`;
if (tixApi) {
if (workerScript.shouldLog("sellShort")) { workerScript.log(resultTxt); }
} else {
dialogBoxCreate(resultTxt);
}
return true;
}
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; }
const timeNow = new Date().getTime();
if (timeNow - StockMarket.lastUpdate < 4e3) { return; }
StockMarket.lastUpdate = timeNow;
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;}
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;
}
}
}
displayStockMarketContent();
}
//Checks and triggers any orders for the specified stock
function processOrders(stock, orderType, posType) {
var orderBook = StockMarket["Orders"];
if (orderBook == null) {
var orders = {};
for (var name in StockMarket) {
if (StockMarket.hasOwnProperty(name)) {
var stock = StockMarket[name];
if (!(stock instanceof Stock)) {continue;}
orders[stock.symbol] = [];
}
}
StockMarket["Orders"] = orders;
return; //Newly created, so no orders to process
}
var stockOrders = orderBook[stock.symbol];
if (stockOrders == null || !(stockOrders.constructor === Array)) {
console.log("ERROR: Invalid Order book for " + stock.symbol + " in processOrders()");
stockOrders = [];
return;
}
for (var i = 0; i < stockOrders.length; ++i) {
var order = stockOrders[i];
if (order.type === orderType && order.pos === posType) {
switch(order.type) {
case OrderTypes.LimitBuy:
if (order.pos === PositionTypes.Long && stock.price <= order.price) {
executeOrder/*66*/(order);
} else if (order.pos === PositionTypes.Short && stock.price >= order.price) {
executeOrder/*66*/(order);
}
break;
case OrderTypes.LimitSell:
if (order.pos === PositionTypes.Long && stock.price >= order.price) {
executeOrder/*66*/(order);
} else if (order.pos === PositionTypes.Short && stock.price <= order.price) {
executeOrder/*66*/(order);
}
break;
case OrderTypes.StopBuy:
if (order.pos === PositionTypes.Long && stock.price >= order.price) {
executeOrder/*66*/(order);
} else if (order.pos === PositionTypes.Short && stock.price <= order.price) {
executeOrder/*66*/(order);
}
break;
case OrderTypes.StopSell:
if (order.pos === PositionTypes.Long && stock.price <= order.price) {
executeOrder/*66*/(order);
} else if (order.pos === PositionTypes.Short && stock.price >= order.price) {
executeOrder/*66*/(order);
}
break;
default:
console.log("Invalid order type: " + order.type);
return;
}
}
}
}
let stockMarketContainer = null;
function setStockMarketContainer() {
stockMarketContainer = document.getElementById("stock-market-container");
document.removeEventListener("DOMContentLoaded", setStockMarketContainer);
}
document.addEventListener("DOMContentLoaded", setStockMarketContainer);
function initStockMarketFnForReact() {
initStockMarket();
initSymbolToStockMap();
}
export function displayStockMarketContent() {
if (!routing.isOn(Page.StockMarket)) {
return;
}
if (stockMarketContainer instanceof HTMLElement) {
ReactDOM.render(
<StockMarketRoot
buyStockLong={buyStock}
buyStockShort={shortStock}
cancelOrder={cancelOrder}
initStockMarket={initStockMarketFnForReact}
p={Player}
placeOrder={placeOrder}
sellStockLong={sellStock}
sellStockShort={sellShort}
stockMarket={StockMarket}
/>,
stockMarketContainer
)
}
}

@ -0,0 +1,230 @@
import { Stock } from "./Stock";
import { PositionTypes } from "./data/PositionTypes";
import { CONSTANTS } from "../Constants";
/**
* Given a stock, calculates the amount by which the stock price is multiplied
* for an 'upward' price movement. This does not actually increase the stock's price,
* just calculates the multiplier
* @param {Stock} stock - Stock for price movement
* @returns {number | null} Number by which stock's price should be multiplied. Null for invalid args
*/
export function calculateIncreasingPriceMovement(stock: Stock): number | null {
if (!(stock instanceof Stock)) { return null; }
return (1 + (stock.priceMovementPerc / 100));
}
/**
* Given a stock, calculates the amount by which the stock price is multiplied
* for a "downward" price movement. This does not actually increase the stock's price,
* just calculates the multiplier
* @param {Stock} stock - Stock for price movement
* @returns {number | null} Number by which stock's price should be multiplied. Null for invalid args
*/
export function calculateDecreasingPriceMovement(stock: Stock): number | null {
if (!(stock instanceof Stock)) { return null; }
return (1 - (stock.priceMovementPerc / 100));
}
/**
* Calculate the total cost of a "buy" transaction. This accounts for spread,
* price movements, and commission.
* @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; }
// Cap the 'shares' arg at the stock's maximum shares. This'll prevent
// hanging in the case when a really big number is passed in
shares = Math.min(shares, stock.maxShares);
const isLong = (posType === PositionTypes.Long);
// 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);
function processPriceMovement() {
if (isLong) {
currPrice *= calculateIncreasingPriceMovement(stock)!;
} else {
currPrice *= calculateDecreasingPriceMovement(stock)!;
}
}
for (let i = 1; i < numIterations; ++i) {
processPriceMovement();
const amt = Math.min(stock.shareTxForMovement, remainingShares);
totalCost += (amt * currPrice);
remainingShares -= amt;
}
return totalCost + CONSTANTS.StockMarketCommission;
}
/**
* Processes a buy transaction's resulting price movement.
* @param {Stock} stock - Stock being purchased
* @param {number} shares - Number of shares being transacted
* @param {PositionTypes} posType - Long or short position
*/
export function processBuyTransactionPriceMovement(stock: Stock, shares: number, posType: PositionTypes): void {
if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return; }
// Cap the 'shares' arg at the stock's maximum shares. This'll prevent
// hanging in the case when a really big number is passed in
shares = Math.min(shares, stock.maxShares);
const isLong = (posType === PositionTypes.Long);
// If the number of shares doesn't trigger a price movement, just return
if (shares <= stock.shareTxUntilMovement) {
stock.shareTxUntilMovement -= shares;
return;
}
// 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);
let currPrice = stock.price;
function processPriceMovement() {
if (isLong) {
currPrice *= calculateIncreasingPriceMovement(stock)!;
} else {
currPrice *= calculateDecreasingPriceMovement(stock)!;
}
}
for (let i = 1; i < numIterations; ++i) {
processPriceMovement();
}
stock.price = currPrice;
stock.shareTxUntilMovement = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovement) % stock.shareTxForMovement);
}
/**
* 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; }
// Cap the 'shares' arg at the stock's maximum shares. This'll prevent
// hanging in the case when a really big number is passed in
shares = Math.min(shares, stock.maxShares);
const isLong = (posType === PositionTypes.Long);
// 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 account 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) {
// Price movement
if (isLong) {
currPrice *= calculateDecreasingPriceMovement(stock)!;
} else {
currPrice *= calculateIncreasingPriceMovement(stock)!;
}
const amt = Math.min(stock.shareTxForMovement, remainingShares);
totalGain += calculateGain(currPrice, amt);
remainingShares -= amt;
}
return totalGain - CONSTANTS.StockMarketCommission;
}
/**
* Processes a sell transaction's resulting price movement
* @param {Stock} stock - Stock being sold
* @param {number} shares - Number of sharse being transacted
* @param {PositionTypes} posType - Long or short position
*/
export function processSellTransactionPriceMovement(stock: Stock, shares: number, posType: PositionTypes): void {
if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return; }
// Cap the 'shares' arg at the stock's maximum shares. This'll prevent
// hanging in the case when a really big number is passed in
shares = Math.min(shares, stock.maxShares);
const isLong = (posType === PositionTypes.Long);
if (shares <= stock.shareTxUntilMovement) {
stock.shareTxUntilMovement -= shares;
return;
}
// 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);
let currPrice = stock.price;
for (let i = 1; i < numIterations; ++i) {
// Price movement
if (isLong) {
currPrice *= calculateDecreasingPriceMovement(stock)!;
} else {
currPrice *= calculateIncreasingPriceMovement(stock)!;
}
}
stock.price = currPrice;
stock.shareTxUntilMovement = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovement) % stock.shareTxForMovement);
}

@ -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: 30e3,
min: 10e3,
},
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: 30e3,
min: 10e3,
},
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: 30e3,
min: 10e3,
},
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: 30e3,
min: 10e3,
},
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: 30e3,
min: 10e3,
},
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: 30e3,
min: 10e3,
},
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: 30e3,
min: 10e3,
},
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: 30e3,
min: 10e3,
},
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: 36e3,
min: 12e3,
},
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: 36e3,
min: 12e3,
},
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: 36e3,
min: 12e3,
},
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: 36e3,
min: 12e3,
},
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: 36e3,
min: 12e3,
},
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: 36e3,
min: 12e3,
},
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: 42e3,
min: 14e3,
},
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: 42e3,
min: 14e3,
},
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: 42e3,
min: 14e3,
},
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: 42e3,
min: 14e3,
},
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: 42e3,
min: 14e3,
},
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: 18e3,
min: 4e3,
},
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: 36e3,
min: 12e3,
},
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: 42e3,
min: 20e3,
},
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: 30e3,
min: 10e3,
},
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: 30e3,
min: 10e3,
},
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: 42e3,
min: 20e3,
},
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: 18e3,
min: 6e3,
},
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: 30e3,
min: 10e3,
},
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: 60e3,
min: 20e3,
},
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: 28e3,
min: 8e3,
},
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: 21e3,
min: 6e3,
},
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: 24e3,
min: 8e3,
},
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: 72e3,
min: 30e3,
},
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: 72e3,
min: 30e3,
},
symbol: StockSymbols["Titan Laboratories"],
},
];

@ -0,0 +1,6 @@
export enum OrderTypes {
LimitBuy = "Limit Buy Order",
LimitSell = "Limit Sell Order",
StopBuy = "Stop Buy Order",
StopSell = "Stop Sell Order"
}

@ -0,0 +1,4 @@
export enum PositionTypes {
Long = "L",
Short = "S"
}

@ -0,0 +1,41 @@
import { IMap } from "../../types";
import { LocationName } from "../../Locations/data/LocationNames";
export const StockSymbols: IMap<string> = {};
// 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";

@ -0,0 +1,11 @@
import { StockSymbols } from "./StockSymbols";
export const TickerHeaderFormatData = {
longestName: 0,
longestSymbol: 0,
}
for (const key in StockSymbols) {
TickerHeaderFormatData.longestName = Math.max(key.length, TickerHeaderFormatData.longestName);
TickerHeaderFormatData.longestSymbol = Math.max(StockSymbols[key].length, TickerHeaderFormatData.longestSymbol);
}

@ -0,0 +1,226 @@
/**
* React component for the Stock Market UI. This component displays
* general information about the stock market, buttons for the various purchases,
* and a link to the documentation (Investopedia)
*/
import * as React from "react";
import {
getStockMarket4SDataCost,
getStockMarket4STixApiCost
} from "../StockMarketCosts";
import { CONSTANTS } from "../../Constants";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { numeralWrapper } from "../../ui/numeralFormat";
import { StdButton } from "../../ui/React/StdButton";
import { StdButtonPurchased } from "../../ui/React/StdButtonPurchased";
import { dialogBoxCreate } from "../../../utils/DialogBox";
type IProps = {
initStockMarket: () => void;
p: IPlayer;
rerender: () => void;
}
const blockStyleMarkup = {
display: "block",
}
export class InfoAndPurchases extends React.Component<IProps, any> {
constructor(props: IProps) {
super(props);
this.handleClick4SMarketDataHelpTip = this.handleClick4SMarketDataHelpTip.bind(this);
this.purchaseWseAccount = this.purchaseWseAccount.bind(this);
this.purchaseTixApiAccess = this.purchaseTixApiAccess.bind(this);
this.purchase4SMarketData = this.purchase4SMarketData.bind(this);
this.purchase4SMarketDataTixApiAccess = this.purchase4SMarketDataTixApiAccess.bind(this);
}
handleClick4SMarketDataHelpTip() {
dialogBoxCreate(
"Access to the 4S Market Data feed will display two additional pieces " +
"of information about each stock: Price Forecast & Volatility<br><br>" +
"Price Forecast indicates the probability the stock has of increasing or " +
"decreasing. A '+' forecast means the stock has a higher chance of increasing " +
"than decreasing, and a '-' means the opposite. The number of '+/-' symbols " +
"is used to illustrate the magnitude of these probabilities. For example, " +
"'+++' means that the stock has a significantly higher chance of increasing " +
"than decreasing, while '+' means that the stock only has a slightly higher chance " +
"of increasing than decreasing.<br><br>" +
"Volatility represents the maximum percentage by which a stock's price " +
"can change every tick (a tick occurs every few seconds while the game " +
"is running).<br><br>" +
"A stock's price forecast can change over time. This is also affected by volatility. " +
"The more volatile a stock is, the more its price forecast will change."
);
}
purchaseWseAccount() {
if (this.props.p.hasWseAccount) { return; }
if (!this.props.p.canAfford(CONSTANTS.WSEAccountCost)) { return; }
this.props.p.hasWseAccount = true;
this.props.initStockMarket();
this.props.p.loseMoney(CONSTANTS.WSEAccountCost);
this.props.rerender();
}
purchaseTixApiAccess() {
if (this.props.p.hasTixApiAccess) { return; }
if (!this.props.p.canAfford(CONSTANTS.TIXAPICost)) { return; }
this.props.p.hasTixApiAccess = true;
this.props.p.loseMoney(CONSTANTS.TIXAPICost);
this.props.rerender();
}
purchase4SMarketData() {
if (this.props.p.has4SData) { return; }
if (!this.props.p.canAfford(getStockMarket4SDataCost())) { return; }
this.props.p.has4SData = true;
this.props.p.loseMoney(getStockMarket4SDataCost());
this.props.rerender();
}
purchase4SMarketDataTixApiAccess() {
if (this.props.p.has4SDataTixApi) { return; }
if (!this.props.p.canAfford(getStockMarket4STixApiCost())) { return; }
this.props.p.has4SDataTixApi = true;
this.props.p.loseMoney(getStockMarket4STixApiCost());
this.props.rerender();
}
renderPurchaseWseAccountButton(): React.ReactElement {
if (this.props.p.hasWseAccount) {
return (
<StdButtonPurchased text={"WSE Account - Purchased"} />
)
} else {
const cost = CONSTANTS.WSEAccountCost;
return (
<StdButton
disabled={!this.props.p.canAfford(cost)}
onClick={this.purchaseWseAccount}
text={`Buy WSE Account - ${numeralWrapper.formatMoney(cost)}`}
/>
)
}
}
renderPurchaseTixApiAccessButton(): React.ReactElement {
if (this.props.p.hasTixApiAccess) {
return (
<StdButtonPurchased text={"TIX API Access - Purchased"} />
)
} else {
const cost = CONSTANTS.TIXAPICost;
return (
<StdButton
disabled={!this.props.p.canAfford(cost)}
onClick={this.purchaseTixApiAccess}
style={blockStyleMarkup}
text={`Buy Trade Information eXchange (TIX) API Access - ${numeralWrapper.formatMoney(cost)}`}
/>
)
}
}
renderPurchase4SMarketDataButton(): React.ReactElement {
if (this.props.p.has4SData) {
return (
<StdButtonPurchased
text={"4S Market Data - Purchased"}
tooltip={"Lets you view additional pricing and volatility information about stocks"}
/>
)
} else {
const cost = getStockMarket4SDataCost();
return (
<StdButton
disabled={!this.props.p.canAfford(cost)}
onClick={this.purchase4SMarketData}
text={`Buy 4S Market Data Access - ${numeralWrapper.formatMoney(cost)}`}
tooltip={"Lets you view additional pricing and volatility information about stocks"}
/>
)
}
}
renderPurchase4SMarketDataTixApiAccessButton(): React.ReactElement {
if (!this.props.p.hasTixApiAccess) {
return (
<StdButton
disabled={true}
text={`Buy 4S Market Data TIX API Access`}
tooltip={"Requires TIX API Access"}
/>
)
} else if (this.props.p.has4SDataTixApi) {
return (
<StdButtonPurchased
text={"4S Market Data TIX API - Purchased"}
tooltip={"Let you access 4S Market Data through Netscript"}
/>
)
} else {
const cost = getStockMarket4STixApiCost();
return (
<StdButton
disabled={!this.props.p.canAfford(cost)}
onClick={this.purchase4SMarketDataTixApiAccess}
text={`Buy 4S Market Data TIX API Access - ${numeralWrapper.formatMoney(cost)}`}
tooltip={"Let you access 4S Market Data through Netscript"}
/>
)
}
}
render() {
const documentationLink = "https://bitburner.readthedocs.io/en/latest/basicgameplay/stockmarket.html";
return (
<div className={"stock-market-info-and-purchases"}>
<p>Welcome to the World Stock Exchange (WSE)!</p>
<button className={"std-button"}>
<a href={documentationLink} target={"_blank"}>
Investopedia
</a>
</button>
<br />
<p>
To begin trading, you must first purchase an account:
</p>
{this.renderPurchaseWseAccountButton()}
<h2>Trade Information eXchange (TIX) API</h2>
<p>
TIX, short for Trade Information eXchange, is the communications protocol
used by the WSE. Purchasing access to the TIX API lets you write code to create
your own algorithmic/automated trading strategies.
</p>
{this.renderPurchaseTixApiAccessButton()}
<h2>Four Sigma (4S) Market Data Feed</h2>
<p>
Four Sigma's (4S) Market Data Feed provides information about stocks that will help
your trading strategies.
</p>
{this.renderPurchase4SMarketDataButton()}
<button className={"help-tip-big"} onClick={this.handleClick4SMarketDataHelpTip}>
?
</button>
{this.renderPurchase4SMarketDataTixApiAccessButton()}
<p>
Commission Fees: Every transaction you make has
a {numeralWrapper.formatMoney(CONSTANTS.StockMarketCommission)} commission fee.
</p><br />
<p>
WARNING: When you reset after installing Augmentations, the Stock
Market is reset. You will retain your WSE Account, access to the
TIX API, and 4S Market Data access. However, all of your stock
positions are lost, so make sure to sell your stocks before
installing Augmentations!
</p>
</div>
)
}
}

@ -0,0 +1,75 @@
/**
* Root React component for the Stock Market UI
*/
import * as React from "react";
import { InfoAndPurchases } from "./InfoAndPurchases";
import { StockTickers } from "./StockTickers";
import { IStockMarket } from "../IStockMarket";
import { Stock } from "../Stock";
import { OrderTypes } from "../data/OrderTypes";
import { PositionTypes } from "../data/PositionTypes";
import { IPlayer } from "../../PersonObjects/IPlayer";
type txFn = (stock: Stock, shares: number) => boolean;
export type placeOrderFn = (stock: Stock, shares: number, price: number, ordType: OrderTypes, posType: PositionTypes) => boolean;
type IProps = {
buyStockLong: txFn;
buyStockShort: txFn;
cancelOrder: (params: object) => void;
initStockMarket: () => void;
p: IPlayer;
placeOrder: placeOrderFn;
sellStockLong: txFn;
sellStockShort: txFn;
stockMarket: IStockMarket;
}
type IState = {
rerenderFlag: boolean;
}
export class StockMarketRoot extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
rerenderFlag: false,
}
this.rerender = this.rerender.bind(this);
}
rerender(): void {
this.setState((prevState) => {
return {
rerenderFlag: !prevState.rerenderFlag,
}
});
}
render() {
return (
<div>
<InfoAndPurchases
initStockMarket={this.props.initStockMarket}
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}
/>
</div>
)
}
}

@ -0,0 +1,435 @@
/**
* React Component for a single stock ticker in the Stock Market UI
*/
import * as React from "react";
import { StockTickerHeaderText } from "./StockTickerHeaderText";
import { StockTickerOrderList } from "./StockTickerOrderList";
import { StockTickerPositionText } from "./StockTickerPositionText";
import { StockTickerTxButton } from "./StockTickerTxButton";
import { Order } from "../Order";
import { Stock } from "../Stock";
import {
getBuyTransactionCost,
getSellTransactionGain,
} from "../StockMarketHelpers";
import { OrderTypes } from "../data/OrderTypes";
import { PositionTypes } from "../data/PositionTypes";
import { CONSTANTS } from "../../Constants";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { SourceFileFlags } from "../../SourceFile/SourceFileFlags";
import { numeralWrapper } from "../../ui/numeralFormat";
import { Accordion } from "../../ui/React/Accordion";
import { dialogBoxCreate } from "../../../utils/DialogBox";
import {
yesNoTxtInpBoxClose,
yesNoTxtInpBoxCreate,
yesNoTxtInpBoxGetInput,
yesNoTxtInpBoxGetNoButton,
yesNoTxtInpBoxGetYesButton,
} from "../../../utils/YesNoBox";
enum SelectorOrderType {
Market = "Market Order",
Limit = "Limit Order",
Stop = "Stop Order",
}
export type txFn = (stock: Stock, shares: number) => boolean;
export type placeOrderFn = (stock: Stock, shares: number, price: number, ordType: OrderTypes, posType: PositionTypes) => boolean;
type IProps = {
buyStockLong: txFn;
buyStockShort: txFn;
cancelOrder: (params: object) => void;
orders: Order[];
p: IPlayer;
placeOrder: placeOrderFn;
sellStockLong: txFn;
sellStockShort: txFn;
stock: Stock;
}
type IState = {
orderType: SelectorOrderType;
position: PositionTypes;
qty: string;
}
export class StockTicker extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
orderType: SelectorOrderType.Market,
position: PositionTypes.Long,
qty: "",
}
this.getBuyTransactionCostText = this.getBuyTransactionCostText.bind(this);
this.getSellTransactionCostText = this.getSellTransactionCostText.bind(this);
this.handleBuyButtonClick = this.handleBuyButtonClick.bind(this);
this.handleBuyMaxButtonClick = this.handleBuyMaxButtonClick.bind(this);
this.handleHeaderClick = this.handleHeaderClick.bind(this);
this.handleOrderTypeChange = this.handleOrderTypeChange.bind(this);
this.handlePositionTypeChange = this.handlePositionTypeChange.bind(this);
this.handleQuantityChange = this.handleQuantityChange.bind(this);
this.handleSellButtonClick = this.handleSellButtonClick.bind(this);
this.handleSellAllButtonClick = this.handleSellAllButtonClick.bind(this);
}
createPlaceOrderPopupBox(yesTxt: string, popupTxt: string, yesBtnCb: (price: number) => void) {
const yesBtn = yesNoTxtInpBoxGetYesButton();
const noBtn = yesNoTxtInpBoxGetNoButton();
yesBtn!.innerText = yesTxt;
yesBtn!.addEventListener("click", () => {
const price = parseFloat(yesNoTxtInpBoxGetInput());
if (isNaN(price)) {
dialogBoxCreate(`Invalid input for price: ${yesNoTxtInpBoxGetInput()}`);
return false;
}
yesBtnCb(price);
yesNoTxtInpBoxClose();
});
noBtn!.innerText = "Cancel Order";
noBtn!.addEventListener("click", () => {
yesNoTxtInpBoxClose();
});
yesNoTxtInpBoxCreate(popupTxt);
}
getBuyTransactionCostText(): string {
const stock = this.props.stock;
const qty: number = this.getQuantity();
if (isNaN(qty)) { return ""; }
const cost = getBuyTransactionCost(stock, qty, this.state.position);
if (cost == null) { return ""; }
let costTxt = `Purchasing ${numeralWrapper.formatBigNumber(qty)} shares (${this.state.position === PositionTypes.Long ? "Long" : "Short"}) ` +
`will cost ${numeralWrapper.formatMoney(cost)}. `;
const causesMovement = qty > stock.shareTxUntilMovement;
if (causesMovement) {
costTxt += `WARNING: Purchasing this many shares will influence the stock price`;
}
return costTxt;
}
getQuantity(): number {
return Math.round(parseFloat(this.state.qty));
}
getSellTransactionCostText(): string {
const stock = this.props.stock;
const qty: number = this.getQuantity();
if (isNaN(qty)) { return ""; }
if (this.state.position === PositionTypes.Long) {
if (qty > stock.playerShares) {
return `You do not have this many shares in the Long position`;
}
} else {
if (qty > stock.playerShortShares) {
return `You do not have this many shares in the Short position`;
}
}
const cost = getSellTransactionGain(stock, qty, this.state.position);
if (cost == null) { return ""; }
let costTxt = `Selling ${numeralWrapper.formatBigNumber(qty)} shares (${this.state.position === PositionTypes.Long ? "Long" : "Short"}) ` +
`will result in a gain of ${numeralWrapper.formatMoney(cost)}. `;
const causesMovement = qty > stock.shareTxUntilMovement;
if (causesMovement) {
costTxt += `WARNING: Selling this many shares will influence the stock price`;
}
return costTxt;
}
handleBuyButtonClick() {
const shares = this.getQuantity();
if (isNaN(shares)) {
dialogBoxCreate(`Invalid input for quantity (number of shares): ${this.state.qty}`);
return;
}
switch (this.state.orderType) {
case SelectorOrderType.Market: {
if (this.state.position === PositionTypes.Short) {
this.props.buyStockShort(this.props.stock, shares);
} else {
this.props.buyStockLong(this.props.stock, shares);
}
break;
}
case SelectorOrderType.Limit: {
this.createPlaceOrderPopupBox(
"Place Buy Limit Order",
"Enter the price for your Limit Order",
(price: number) => {
this.props.placeOrder(this.props.stock, shares, price, OrderTypes.LimitBuy, this.state.position);
}
);
break;
}
case SelectorOrderType.Stop: {
this.createPlaceOrderPopupBox(
"Place Buy Stop Order",
"Enter the price for your Stop Order",
(price: number) => {
this.props.placeOrder(this.props.stock, shares, price, OrderTypes.StopBuy, this.state.position);
}
);
break;
}
default:
break;
}
}
handleBuyMaxButtonClick() {
const playerMoney: number = this.props.p.money.toNumber();
const stock = this.props.stock;
let maxShares = Math.floor((playerMoney - CONSTANTS.StockMarketCommission) / this.props.stock.price);
maxShares = Math.min(maxShares, Math.round(stock.maxShares - stock.playerShares - stock.playerShortShares));
switch (this.state.orderType) {
case SelectorOrderType.Market: {
if (this.state.position === PositionTypes.Short) {
this.props.buyStockShort(stock, maxShares);
} else {
this.props.buyStockLong(stock, maxShares);
}
break;
}
case SelectorOrderType.Limit: {
this.createPlaceOrderPopupBox(
"Place Buy Limit Order",
"Enter the price for your Limit Order",
(price: number) => {
this.props.placeOrder(stock, maxShares, price, OrderTypes.LimitBuy, this.state.position);
}
);
break;
}
case SelectorOrderType.Stop: {
this.createPlaceOrderPopupBox(
"Place Buy Stop Order",
"Enter the price for your Stop Order",
(price: number) => {
this.props.placeOrder(stock, maxShares, price, OrderTypes.StopBuy, this.state.position);
}
)
break;
}
default:
break;
}
}
handleHeaderClick(e: React.MouseEvent<HTMLButtonElement>) {
const elem = e.currentTarget;
elem.classList.toggle("active");
const panel: HTMLElement = elem.nextElementSibling as HTMLElement;
if (panel!.style.display === "block") {
panel!.style.display = "none";
} else {
panel.style.display = "block";
}
}
handleOrderTypeChange(e: React.ChangeEvent<HTMLSelectElement>) {
const val = e.target.value;
// The select value returns a string. Afaik TypeScript doesnt make it easy
// to convert that string back to an enum type so we'll just do this for now
switch (val) {
case SelectorOrderType.Limit:
this.setState({
orderType: SelectorOrderType.Limit,
});
break;
case SelectorOrderType.Stop:
this.setState({
orderType: SelectorOrderType.Stop,
});
break;
case SelectorOrderType.Market:
default:
this.setState({
orderType: SelectorOrderType.Market,
});
}
}
handlePositionTypeChange(e: React.ChangeEvent<HTMLSelectElement>) {
const val = e.target.value;
if (val === PositionTypes.Short) {
this.setState({
position: PositionTypes.Short,
});
} else {
this.setState({
position: PositionTypes.Long,
});
}
}
handleQuantityChange(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({
qty: e.target.value,
});
}
handleSellButtonClick() {
const shares = this.getQuantity();
if (isNaN(shares)) {
dialogBoxCreate(`Invalid input for quantity (number of shares): ${this.state.qty}`);
return;
}
switch (this.state.orderType) {
case SelectorOrderType.Market: {
if (this.state.position === PositionTypes.Short) {
this.props.sellStockShort(this.props.stock, shares);
} else {
this.props.sellStockLong(this.props.stock, shares);
}
break;
}
case SelectorOrderType.Limit: {
this.createPlaceOrderPopupBox(
"Place Sell Limit Order",
"Enter the price for your Limit Order",
(price: number) => {
this.props.placeOrder(this.props.stock, shares, price, OrderTypes.LimitSell, this.state.position);
}
);
break;
}
case SelectorOrderType.Stop: {
this.createPlaceOrderPopupBox(
"Place Sell Stop Order",
"Enter the price for your Stop Order",
(price: number) => {
this.props.placeOrder(this.props.stock, shares, price, OrderTypes.StopSell, this.state.position);
}
)
break;
}
default:
break;
}
}
handleSellAllButtonClick() {
const stock = this.props.stock;
switch (this.state.orderType) {
case SelectorOrderType.Market: {
if (this.state.position === PositionTypes.Short) {
this.props.sellStockShort(stock, stock.playerShortShares);
} else {
this.props.sellStockLong(stock, stock.playerShares);
}
break;
}
default: {
dialogBoxCreate(`ERROR: 'Sell All' only works for Market Orders`);
break;
}
}
}
// Whether the player has access to orders besides market orders (limit/stop)
hasOrderAccess(): boolean {
return (this.props.p.bitNodeN === 8 || (SourceFileFlags[8] >= 3));
}
// Whether the player has access to shorting stocks
hasShortAccess(): boolean {
return (this.props.p.bitNodeN === 8 || (SourceFileFlags[8] >= 2));
}
render() {
// Determine if the player's intended transaction will cause a price movement
let causesMovement: boolean = false;
const qty = this.getQuantity();
if (!isNaN(qty)) {
causesMovement = qty > this.props.stock.shareTxUntilMovement;
}
return (
<li>
<Accordion
headerContent={
<StockTickerHeaderText p={this.props.p} stock={this.props.stock} />
}
panelContent={
<div>
<input
className="stock-market-input"
onChange={this.handleQuantityChange}
placeholder="Quantity (Shares)"
value={this.state.qty}
/>
<select className="stock-market-input dropdown" onChange={this.handlePositionTypeChange} value={this.state.position}>
<option value={PositionTypes.Long}>Long</option>
{
this.hasShortAccess() &&
<option value={PositionTypes.Short}>Short</option>
}
</select>
<select className="stock-market-input dropdown" onChange={this.handleOrderTypeChange} value={this.state.orderType}>
<option value={SelectorOrderType.Market}>{SelectorOrderType.Market}</option>
{
this.hasOrderAccess() &&
<option value={SelectorOrderType.Limit}>{SelectorOrderType.Limit}</option>
}
{
this.hasOrderAccess() &&
<option value={SelectorOrderType.Stop}>{SelectorOrderType.Stop}</option>
}
</select>
<StockTickerTxButton onClick={this.handleBuyButtonClick} text={"Buy"} tooltip={this.getBuyTransactionCostText()} />
<StockTickerTxButton onClick={this.handleSellButtonClick} text={"Sell"} tooltip={this.getSellTransactionCostText()} />
<StockTickerTxButton onClick={this.handleBuyMaxButtonClick} text={"Buy MAX"} />
<StockTickerTxButton onClick={this.handleSellAllButtonClick} text={"Sell ALL"} />
{
causesMovement &&
<p className="stock-market-price-movement-warning">
WARNING: Buying/Selling {numeralWrapper.formatBigNumber(qty)} shares will affect
the stock's price. This applies during the transaction itself as well. See Investopedia
for more details.
</p>
}
<StockTickerPositionText p={this.props.p} stock={this.props.stock} />
<StockTickerOrderList
cancelOrder={this.props.cancelOrder}
orders={this.props.orders}
p={this.props.p}
stock={this.props.stock}
/>
</div>
}
/>
</li>
)
}
}

@ -0,0 +1,41 @@
/**
* React Component for the text on a stock ticker's header. This text displays
* general information on the stock such as the name, symbol, price, and
* 4S Market Data
*/
import * as React from "react";
import { Stock } from "../Stock";
import { TickerHeaderFormatData } from "../data/TickerHeaderFormatData";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { numeralWrapper } from "../../ui/numeralFormat";
type IProps = {
p: IPlayer;
stock: Stock;
}
export function StockTickerHeaderText(props: IProps): React.ReactElement {
const stock = props.stock;
const p = props.p;
const stockPriceFormat = numeralWrapper.formatMoney(stock.price);
let hdrText = `${stock.name}${" ".repeat(1 + TickerHeaderFormatData.longestName - stock.name.length + (TickerHeaderFormatData.longestSymbol - stock.symbol.length))}${stock.symbol} -${" ".repeat(10 - stockPriceFormat.length)}${stockPriceFormat}`;
if (props.p.has4SData) {
hdrText += ` - Volatility: ${numeralWrapper.format(stock.mv, '0,0.00')}% - Price Forecast: `;
hdrText += (stock.b ? "+" : "-").repeat(Math.floor(stock.otlkMag / 10) + 1);
}
let styleMarkup = {
color: "#66ff33"
};
if (stock.lastPrice === stock.price) {
styleMarkup.color = "white";
} else if (stock.lastPrice > stock.price) {
styleMarkup.color = "red";
}
return <pre style={styleMarkup}>{hdrText}</pre>;
}

@ -0,0 +1,42 @@
/**
* React component for displaying a single order in a stock's order book
*/
import * as React from "react";
import { Order } from "../Order";
import { PositionTypes } from "../data/PositionTypes";
import { numeralWrapper } from "../../ui/numeralFormat";
type IProps = {
cancelOrder: (params: object) => void;
order: Order;
}
export class StockTickerOrder extends React.Component<IProps, any> {
constructor(props: IProps) {
super(props);
this.handleCancelOrderClick = this.handleCancelOrderClick.bind(this);
}
handleCancelOrderClick() {
this.props.cancelOrder({ order: this.props.order });
}
render() {
const order = this.props.order;
const posTxt = order.pos === PositionTypes.Long ? "Long Position" : "Short Position";
const txt = `${order.type} - ${posTxt} - ${order.shares} @ ${numeralWrapper.formatMoney(order.price)}`
return (
<li>
{txt}
<button className={"std-button stock-market-order-cancel-btn"} onClick={this.handleCancelOrderClick}>
Cancel Order
</button>
</li>
)
}
}

@ -0,0 +1,33 @@
/**
* React component for displaying a stock's order list in the Stock Market UI.
* This component resides in the stock ticker
*/
import * as React from "react";
import { StockTickerOrder } from "./StockTickerOrder";
import { Order } from "../Order";
import { Stock } from "../Stock";
import { IPlayer } from "../../PersonObjects/IPlayer";
type IProps = {
cancelOrder: (params: object) => void;
orders: Order[];
p: IPlayer;
stock: Stock;
}
export class StockTickerOrderList extends React.Component<IProps, any> {
render() {
const orders: React.ReactElement[] = [];
for (let i = 0; i < this.props.orders.length; ++i) {
const o = this.props.orders[i];
orders.push(<StockTickerOrder cancelOrder={this.props.cancelOrder} order={o} key={i} />);
}
return (
<ul className={"stock-market-order-list"}>{orders}</ul>
)
}
}

@ -0,0 +1,114 @@
/**
* React Component for the text on a stock ticker that display's information
* about the player's position in that stock
*/
import * as React from "react";
import { Stock } from "../Stock";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { numeralWrapper } from "../../ui/numeralFormat";
import { SourceFileFlags } from "../../SourceFile/SourceFileFlags";
type IProps = {
p: IPlayer;
stock: Stock;
}
const blockStyleMarkup = {
display: "block",
}
export class StockTickerPositionText extends React.Component<IProps, any> {
renderLongPosition(): React.ReactElement {
const stock = this.props.stock;
// Caculate total returns
const totalCost = stock.playerShares * stock.playerAvgPx;
const gains = (stock.price - stock.playerAvgPx) * stock.playerShares;
let percentageGains = gains / totalCost;
if (isNaN(percentageGains)) { percentageGains = 0; }
return (
<div>
<h3 className={"tooltip"}>
Long Position:
<span className={"tooltiptext"}>
Shares in the long position will increase in value if the price
of the corresponding stock increases
</span>
</h3><br />
<p>
Shares: {numeralWrapper.format(stock.playerShares, "0,0")}
</p><br />
<p>
Average Price: {numeralWrapper.formatMoney(stock.playerAvgPx)} (Total Cost: {numeralWrapper.formatMoney(totalCost)})
</p><br />
<p>
Profit: {numeralWrapper.formatMoney(gains)} ({numeralWrapper.formatPercentage(percentageGains)})
</p><br />
</div>
)
}
renderShortPosition(): React.ReactElement | null {
const stock = this.props.stock;
// Caculate total returns
const totalCost = stock.playerShortShares * stock.playerAvgShortPx;
const gains = (stock.playerAvgShortPx - stock.price) * stock.playerShortShares;
let percentageGains = gains / totalCost;
if (isNaN(percentageGains)) { percentageGains = 0; }
if (this.props.p.bitNodeN === 8 || (SourceFileFlags[8] >= 2)) {
return (
<div>
<h3 className={"tooltip"}>
Short Position:
<span className={"tooltiptext"}>
Shares in the short position will increase in value if the
price of the corresponding stock decreases
</span>
</h3><br />
<p>
Shares: {numeralWrapper.format(stock.playerShortShares, "0,0")}
</p><br />
<p>
Average Price: {numeralWrapper.formatMoney(stock.playerAvgShortPx)} (Total Cost: {numeralWrapper.formatMoney(totalCost)})
</p><br />
<p>
Profit: {numeralWrapper.formatMoney(gains)} ({numeralWrapper.formatPercentage(percentageGains)})
</p><br />
</div>
)
} else {
return null;
}
}
render() {
const stock = this.props.stock;
return (
<div className={"stock-market-position-text"}>
<p style={blockStyleMarkup}>
Max Shares: {numeralWrapper.formatBigNumber(stock.maxShares)}
</p>
<p className={"tooltip"} >
Ask Price: {numeralWrapper.formatMoney(stock.getAskPrice())}
<span className={"tooltiptext"}>
See Investopedia for details on what this is
</span>
</p><br />
<p className={"tooltip"} >
Bid Price: {numeralWrapper.formatMoney(stock.getBidPrice())}
<span className={"tooltiptext"}>
See Investopedia for details on what this is
</span>
</p>
{this.renderLongPosition()}
{this.renderShortPosition()}
</div>
)
}
}

@ -0,0 +1,41 @@
/**
* React Component for a button that initiates a transaction on the Stock Market UI
* (Buy, Sell, Buy Max, etc.)
*/
import * as React from "react";
type IProps = {
onClick: () => void;
text: string;
tooltip?: string;
}
type IInnerHTMLMarkup = {
__html: string;
}
export function StockTickerTxButton(props: IProps): React.ReactElement {
let className = "stock-market-input std-button";
const hasTooltip = (typeof props.tooltip === "string" && props.tooltip !== "");
if (hasTooltip) {
className += " tooltip";
}
let tooltipMarkup: IInnerHTMLMarkup | null;
if (hasTooltip) {
tooltipMarkup = {
__html: props.tooltip!
}
}
return (
<button className={className} onClick={props.onClick}>
{props.text}
{
hasTooltip &&
<span className={"tooltiptext"} dangerouslySetInnerHTML={tooltipMarkup!}></span>
}
</button>
)
}

@ -0,0 +1,146 @@
/**
* React Component for the Stock Market UI. This is the container for all
* of the stock tickers. It also contains the configuration for the
* stock ticker UI (watchlist filter, portfolio vs all mode, etc.)
*/
import * as React from "react";
import { StockTicker } from "./StockTicker";
import { StockTickersConfig, TickerDisplayMode } from "./StockTickersConfig";
import { IStockMarket } from "../IStockMarket";
import { Stock } from "../Stock";
import { OrderTypes } from "../data/OrderTypes";
import { PositionTypes } from "../data/PositionTypes";
import { IPlayer } from "../../PersonObjects/IPlayer";
export type txFn = (stock: Stock, shares: number) => boolean;
export type placeOrderFn = (stock: Stock, shares: number, price: number, ordType: OrderTypes, posType: PositionTypes) => boolean;
type IProps = {
buyStockLong: txFn;
buyStockShort: txFn;
cancelOrder: (params: object) => void;
p: IPlayer;
placeOrder: placeOrderFn;
sellStockLong: txFn;
sellStockShort: txFn;
stockMarket: IStockMarket;
}
type IState = {
rerenderFlag: boolean;
tickerDisplayMode: TickerDisplayMode;
watchlistFilter: string;
watchlistSymbols: string[];
}
export class StockTickers extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
rerenderFlag: false,
tickerDisplayMode: TickerDisplayMode.AllStocks,
watchlistFilter: "",
watchlistSymbols: [],
}
this.changeDisplayMode = this.changeDisplayMode.bind(this);
this.changeWatchlistFilter = this.changeWatchlistFilter.bind(this);
}
changeDisplayMode() {
if (this.state.tickerDisplayMode === TickerDisplayMode.AllStocks) {
this.setState({
tickerDisplayMode: TickerDisplayMode.Portfolio,
});
} else {
this.setState({
tickerDisplayMode: TickerDisplayMode.AllStocks,
});
}
}
changeWatchlistFilter(e: React.ChangeEvent<HTMLInputElement>) {
const watchlist = e.target.value;
const sanitizedWatchlist = watchlist.replace(/\s/g, '');
this.setState({
watchlistFilter: watchlist,
});
if (sanitizedWatchlist !== "") {
this.setState({
watchlistSymbols: sanitizedWatchlist.split(","),
});
} else {
this.setState({
watchlistSymbols: [],
});
}
}
rerender() {
this.setState((prevState) => {
return {
rerenderFlag: !prevState.rerenderFlag,
}
});
}
render() {
const tickers: React.ReactElement[] = [];
for (const stockMarketProp in this.props.stockMarket) {
const val = this.props.stockMarket[stockMarketProp];
if (val instanceof Stock) {
// Skip if there's a filter and the stock isnt in that filter
if (this.state.watchlistSymbols.length > 0 && !this.state.watchlistSymbols.includes(val.symbol)) {
continue;
}
let orders = this.props.stockMarket.Orders[val.symbol];
if (orders == null) {
orders = [];
}
// Skip if we're in portfolio mode and the player doesnt own this or have any active orders
if (this.state.tickerDisplayMode === TickerDisplayMode.Portfolio) {
if (val.playerShares === 0 && val.playerShortShares === 0 && orders.length === 0) {
continue;
}
}
tickers.push(
<StockTicker
buyStockLong={this.props.buyStockLong}
buyStockShort={this.props.buyStockShort}
cancelOrder={this.props.cancelOrder}
key={val.symbol}
orders={orders}
p={this.props.p}
placeOrder={this.props.placeOrder}
sellStockLong={this.props.sellStockLong}
sellStockShort={this.props.sellStockShort}
stock={val}
/>
)
}
}
return (
<div>
<StockTickersConfig
changeDisplayMode={this.changeDisplayMode}
changeWatchlistFilter={this.changeWatchlistFilter}
tickerDisplayMode={this.state.tickerDisplayMode}
/>
<ul id="stock-market-list">
{tickers}
</ul>
</div>
)
}
}

@ -0,0 +1,61 @@
/**
* React component for the tickers configuration section of the Stock Market UI.
* This config lets you change the way stock tickers are displayed (watchlist,
* all/portoflio mode, etc)
*/
import * as React from "react";
import { StdButton } from "../../ui/React/StdButton";
export enum TickerDisplayMode {
AllStocks,
Portfolio,
}
type IProps = {
changeDisplayMode: () => void;
changeWatchlistFilter: (e: React.ChangeEvent<HTMLInputElement>) => void;
tickerDisplayMode: TickerDisplayMode;
}
export class StockTickersConfig extends React.Component<IProps, any> {
constructor(props: IProps) {
super(props);
}
renderDisplayModeButton() {
let txt: string = "";
let tooltip: string = "";
if (this.props.tickerDisplayMode === TickerDisplayMode.Portfolio) {
txt = "Switch to 'All Stocks' Mode";
tooltip = "Displays all stocks on the WSE";
} else {
txt = "Switch to 'Portfolio' Mode";
tooltip = "Displays only the stocks for which you have shares or orders";
}
return (
<StdButton
onClick={this.props.changeDisplayMode}
text={txt}
tooltip={tooltip}
/>
)
}
render() {
return (
<div>
{this.renderDisplayModeButton()}
<input
className="text-input"
id="stock-market-watchlist-filter"
onChange={this.props.changeWatchlistFilter}
placeholder="Filter Stocks by symbol (comma-separated list)"
type="text"
/>
</div>
)
}
}

@ -82,9 +82,7 @@ import {
} from "./Server/SpecialServerIps";
import {
StockMarket,
StockSymbols,
SymbolToStockMap,
initStockSymbols,
initSymbolToStockMap,
stockMarketCycle,
processStockPrices,
@ -1085,7 +1083,6 @@ const Engine = {
Engine.init(); // Initialize buttons, work, etc.
initAugmentations(); // Also calls Player.reapplyAllAugmentations()
Player.reapplyAllSourceFiles();
initStockSymbols();
if (Player.hasWseAccount) {
initSymbolToStockMap();
}
@ -1215,7 +1212,6 @@ const Engine = {
initFactions();
initAugmentations();
initMessages();
initStockSymbols();
initLiterature();
initSingularitySFFlags();

@ -290,53 +290,7 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
</div>
<div id="stock-market-container" class="generic-menupage-container">
<p>
Welcome to the World Stock Exchange (WSE)! <br /><br />
To begin trading, you must first purchase an account. WSE accounts will persist
after you 'reset' by installing Augmentations.
</p>
<a id="stock-market-buy-account" class="a-link-button-inactive"> Buy WSE Account </a>
<a id="stock-market-investopedia" class="a-link-button">Investopedia</a>
<h2> Trade Information eXchange (TIX) API </h2>
<p>
TIX, short for Trade Information eXchange, is the communications protocol supported by the WSE.
Purchasing access to the TIX API lets you write code to create your own algorithmic/automated
trading strategies.
<br /><br />
If you purchase access to the TIX API, you will retain that access even after
you 'reset' by installing Augmentations.
</p>
<a id="stock-market-buy-tix-api" class="a-link-button-inactive">Buy Trade Information eXchange (TIX) API Access</a>
<h2> Four Sigma (4S) Market Data Feed </h2>
<p>
Four Sigma's (4S) Market Data Feed provides information about stocks
that will help your trading strategies.
<br /><br />
If you purchase access to 4S Market Data and/or the 4S TIX API, you will
retain that access even after you 'reset' by installing Augmentations.
</p>
<a id="stock-market-buy-4s-data" class="a-link-button-inactive tooltip">
Buy 4S Market Data Feed
</a>
<div class="help-tip-big" id="stock-market-4s-data-help-tip">?</div>
<a id="stock-market-buy-4s-tix-api" class="a-link-button-inactive tooltip">
Buy 4S Market Data TIX API Access
</a>
<p id="stock-market-commission"> </p>
<a id="stock-market-mode" class="a-link-button tooltip"></a>
<a id="stock-market-expand-tickers" class="a-link-button tooltip">Expand tickers</a>
<a id="stock-market-collapse-tickers" class="a-link-button tooltip">Collapse tickers</a>
<br /><br />
<input id="stock-market-watchlist-filter" class="text-input" type="text" placeholder="Filter Stocks by symbol (comma-separated list)"/>
<a id="stock-market-watchlist-filter-update" class="a-link-button"> Update Watchlist </a>
<ul id="stock-market-list" style="list-style:none;">
</ul>
<!-- React Component -->
</div>
<!-- Log Box -->

@ -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;
}

@ -0,0 +1,73 @@
/**
* React component to create an accordion element
*/
import * as React from "react";
type IProps = {
headerContent: React.ReactElement;
panelContent: React.ReactElement;
}
type IState = {
panelOpened: boolean;
}
export class Accordion extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.handleHeaderClick = this.handleHeaderClick.bind(this);
this.state = {
panelOpened: false,
}
}
handleHeaderClick(e: React.MouseEvent<HTMLButtonElement>) {
const elem = e.currentTarget;
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 {
panel!.style.display = "block";
this.setState({
panelOpened: true,
});
}
}
render() {
return (
<div>
<button className={"accordion-header"} onClick={this.handleHeaderClick}>
{this.props.headerContent}
</button>
<AccordionPanel opened={this.state.panelOpened} panelContent={this.props.panelContent} />
</div>
)
}
}
type IPanelProps = {
opened: boolean;
panelContent: React.ReactElement;
}
class AccordionPanel extends React.Component<IPanelProps, any> {
shouldComponentUpdate(nextProps: IPanelProps) {
return this.props.opened || nextProps.opened;
}
render() {
return (
<div className={"accordion-panel"}>
{this.props.panelContent}
</div>
)
}
}

@ -7,13 +7,36 @@ interface IStdButtonPurchasedProps {
onClick?: (e: React.MouseEvent<HTMLElement>) => any;
style?: object;
text: string;
tooltip?: string;
}
type IInnerHTMLMarkup = {
__html: string;
}
export class StdButtonPurchased extends React.Component<IStdButtonPurchasedProps, any> {
render() {
const hasTooltip = this.props.tooltip != null && this.props.tooltip !== "";
let className = "std-button-bought";
if (hasTooltip) {
className += " tooltip";
}
// Tooltip will be set using inner HTML
let tooltipMarkup: IInnerHTMLMarkup | null;
if (hasTooltip) {
tooltipMarkup = {
__html: this.props.tooltip!
}
}
return (
<button className={"std-button-bought"} onClick={this.props.onClick} style={this.props.style}>
<button className={className} onClick={this.props.onClick} style={this.props.style}>
{this.props.text}
{
hasTooltip &&
<span className={"tooltiptext"} dangerouslySetInnerHTML={tooltipMarkup!}></span>
}
</button>
)
}

432
tests/StockMarketTests.js Normal file

@ -0,0 +1,432 @@
import { CONSTANTS } from "../src/Constants";
import { Stock } from "../src/StockMarket/Stock";
import {
calculateIncreasingPriceMovement,
calculateDecreasingPriceMovement,
getBuyTransactionCost,
getSellTransactionGain,
processBuyTransactionPriceMovement,
processSellTransactionPriceMovement,
} from "../src/StockMarket/StockMarketHelpers";
import { PositionTypes } from "../src/StockMarket/data/PositionTypes";
const assert = chai.assert;
const expect = chai.expect;
console.log("Beginning Stock Market Tests");
describe("Stock Market Tests", function() {
const commission = CONSTANTS.StockMarketCommission;
// Generic Stock object that can be used by each test
let stock;
const ctorParams = {
b: true,
initPrice: 10e3,
marketCap: 5e9,
mv: 1,
name: "MockStock",
otlkMag: 10,
spreadPerc: 1,
shareTxForMovement: 5e3,
symbol: "mock",
};
beforeEach(function() {
function construct() {
stock = new Stock(ctorParams);
}
expect(construct).to.not.throw();
});
describe("Stock Class", function() {
describe("constructor", function() {
it("should have default parameters", function() {
let defaultStock;
function construct() {
defaultStock = new Stock();
}
expect(construct).to.not.throw();
expect(defaultStock.name).to.equal("");
});
it("should properly initialize props from parameters", function() {
expect(stock.name).to.equal(ctorParams.name);
expect(stock.symbol).to.equal(ctorParams.symbol);
expect(stock.price).to.equal(ctorParams.initPrice);
expect(stock.lastPrice).to.equal(ctorParams.initPrice);
expect(stock.b).to.equal(ctorParams.b);
expect(stock.mv).to.equal(ctorParams.mv);
expect(stock.shareTxForMovement).to.equal(ctorParams.shareTxForMovement);
expect(stock.shareTxUntilMovement).to.equal(ctorParams.shareTxForMovement);
expect(stock.maxShares).to.be.below(stock.totalShares);
expect(stock.spreadPerc).to.equal(ctorParams.spreadPerc);
expect(stock.priceMovementPerc).to.be.a("number");
expect(stock.priceMovementPerc).to.be.at.most(stock.spreadPerc);
expect(stock.priceMovementPerc).to.be.at.least(0);
});
it ("should properly initialize props from range-values", function() {
let stock;
const params = {
b: true,
initPrice: {
max: 10e3,
min: 1e3,
},
marketCap: 5e9,
mv: {
divisor: 100,
max: 150,
min: 50,
},
name: "MockStock",
otlkMag: 10,
spreadPerc: {
divisor: 10,
max: 10,
min: 1,
},
shareTxForMovement: {
max: 10e3,
min: 5e3,
},
symbol: "mock",
};
function construct() {
stock = new Stock(params);
}
expect(construct).to.not.throw();
expect(stock.price).to.be.within(params.initPrice.min, params.initPrice.max);
expect(stock.mv).to.be.within(params.mv.min / params.mv.divisor, params.mv.max / params.mv.divisor);
expect(stock.spreadPerc).to.be.within(params.spreadPerc.min / params.spreadPerc.divisor, params.spreadPerc.max / params.spreadPerc.divisor);
expect(stock.shareTxForMovement).to.be.within(params.shareTxForMovement.min, params.shareTxForMovement.max);
});
it("should round the 'totalShare' prop to the nearest 100k", function() {
expect(stock.totalShares % 100e3).to.equal(0);
});
});
describe("#changePrice()", function() {
it("should set both the last price and current price properties", function() {
const newPrice = 20e3;
stock.changePrice(newPrice);
expect(stock.lastPrice).to.equal(ctorParams.initPrice);
expect(stock.price).to.equal(newPrice);
});
});
describe("#getAskPrice()", function() {
it("should return the price increased by spread percentage", function() {
const perc = stock.spreadPerc / 100;
expect(perc).to.be.at.most(1);
expect(perc).to.be.at.least(0);
const expected = stock.price * (1 + perc);
expect(stock.getAskPrice()).to.equal(expected);
});
});
describe("#getBidPrice()", function() {
it("should return the price decreased by spread percentage", function() {
const perc = stock.spreadPerc / 100;
expect(perc).to.be.at.most(1);
expect(perc).to.be.at.least(0);
const expected = stock.price * (1 - perc);
expect(stock.getBidPrice()).to.equal(expected);
});
});
});
describe("Transaction Cost Calculator Functions", function() {
describe("getBuyTransactionCost()", function() {
it("should fail on invalid 'stock' argument", function() {
const res = getBuyTransactionCost({}, 10, PositionTypes.Long);
expect(res).to.equal(null);
});
it("should fail on invalid 'shares' arg", function() {
let res = getBuyTransactionCost(stock, NaN, PositionTypes.Long);
expect(res).to.equal(null);
res = getBuyTransactionCost(stock, -1, PositionTypes.Long);
expect(res).to.equal(null);
});
it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() {
const shares = ctorParams.shareTxForMovement / 2;
const res = getBuyTransactionCost(stock, shares, PositionTypes.Long);
expect(res).to.equal(shares * stock.getAskPrice() + commission);
});
it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() {
const shares = ctorParams.shareTxForMovement / 2;
const res = getBuyTransactionCost(stock, shares, PositionTypes.Short);
expect(res).to.equal(shares * stock.getBidPrice() + commission);
});
it("should properly evaluate LONG transactions that trigger price movements", function() {
const sharesPerMvmt = ctorParams.shareTxForMovement;
const shares = sharesPerMvmt * 3;
const res = getBuyTransactionCost(stock, shares, PositionTypes.Long);
// Calculate expected cost
const secondPrice = stock.getAskPrice() * calculateIncreasingPriceMovement(stock);
const thirdPrice = secondPrice * calculateIncreasingPriceMovement(stock);
let expected = (sharesPerMvmt * stock.getAskPrice()) + (sharesPerMvmt * secondPrice) + (sharesPerMvmt * thirdPrice);
expect(res).to.equal(expected + commission);
});
it("should properly evaluate SHORT transactions that trigger price movements", function() {
const sharesPerMvmt = ctorParams.shareTxForMovement;
const shares = sharesPerMvmt * 3;
const res = getBuyTransactionCost(stock, shares, PositionTypes.Short);
// Calculate expected cost
const secondPrice = stock.getBidPrice() * calculateDecreasingPriceMovement(stock);
const thirdPrice = secondPrice * calculateDecreasingPriceMovement(stock);
let expected = (sharesPerMvmt * stock.getBidPrice()) + (sharesPerMvmt * secondPrice) + (sharesPerMvmt * thirdPrice);
expect(res).to.equal(expected + commission);
});
it("should cap the 'shares' argument at the stock's maximum number of shares", function() {
const maxRes = getBuyTransactionCost(stock, stock.maxShares, PositionTypes.Long);
const exceedRes = getBuyTransactionCost(stock, stock.maxShares * 10, PositionTypes.Long);
expect(maxRes).to.equal(exceedRes);
});
});
describe("getSellTransactionGain()", function() {
it("should fail on invalid 'stock' argument", function() {
const res = getSellTransactionGain({}, 10, PositionTypes.Long);
expect(res).to.equal(null);
});
it("should fail on invalid 'shares' arg", function() {
let res = getSellTransactionGain(stock, NaN, PositionTypes.Long);
expect(res).to.equal(null);
res = getSellTransactionGain(stock, -1, PositionTypes.Long);
expect(res).to.equal(null);
});
it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() {
const shares = ctorParams.shareTxForMovement / 2;
const res = getSellTransactionGain(stock, shares, PositionTypes.Long);
const expected = shares * stock.getBidPrice() - commission;
expect(res).to.equal(expected);
});
it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() {
// We need to set this property in order to calculate gains from short position
stock.playerAvgShortPx = stock.price * 2;
const shares = ctorParams.shareTxForMovement / 2;
const res = getSellTransactionGain(stock, shares, PositionTypes.Short);
const expected = (shares * stock.playerAvgShortPx) + (shares * (stock.playerAvgShortPx - stock.getAskPrice())) - commission;
expect(res).to.equal(expected);
});
it("should properly evaluate LONG transactions that trigger price movements", function() {
const sharesPerMvmt = ctorParams.shareTxForMovement;
const shares = sharesPerMvmt * 3;
const res = getSellTransactionGain(stock, shares, PositionTypes.Long);
// Calculated expected gain
const mvmt = calculateDecreasingPriceMovement(stock);
const secondPrice = stock.getBidPrice() * mvmt;
const thirdPrice = secondPrice * mvmt;
const expected = (sharesPerMvmt * stock.getBidPrice()) + (sharesPerMvmt * secondPrice) + (sharesPerMvmt * thirdPrice);
expect(res).to.equal(expected - commission);
});
it("should properly evaluate SHORT transactions that trigger price movements", function() {
// We need to set this property in order to calculate gains from short position
stock.playerAvgShortPx = stock.price * 2;
const sharesPerMvmt = ctorParams.shareTxForMovement;
const shares = sharesPerMvmt * 3;
const res = getSellTransactionGain(stock, shares, PositionTypes.Short);
// Calculate expected gain
const mvmt = calculateIncreasingPriceMovement(stock);
const secondPrice = stock.getAskPrice() * mvmt;
const thirdPrice = secondPrice * mvmt;
function getGainForPrice(thisPrice) {
const origCost = sharesPerMvmt * stock.playerAvgShortPx;
return origCost + ((stock.playerAvgShortPx - thisPrice) * sharesPerMvmt);
}
const expected = getGainForPrice(stock.getAskPrice()) + getGainForPrice(secondPrice) + getGainForPrice(thirdPrice);
expect(res).to.equal(expected - commission);
});
it("should cap the 'shares' argument at the stock's maximum number of shares", function() {
const maxRes = getSellTransactionGain(stock, stock.maxShares, PositionTypes.Long);
const exceedRes = getSellTransactionGain(stock, stock.maxShares * 10, PositionTypes.Long);
expect(maxRes).to.equal(exceedRes);
});
});
});
describe("Price Movement Processor Functions", function() {
describe("processBuyTransactionPriceMovement()", function() {
const noMvmtShares = Math.round(ctorParams.shareTxForMovement / 2.2);
const mvmtShares = ctorParams.shareTxForMovement * 3 + noMvmtShares;
it("should do nothing on invalid 'stock' argument", function() {
const oldPrice = stock.price;
const oldTracker = stock.shareTxUntilMovement;
processBuyTransactionPriceMovement({}, mvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(oldTracker);
});
it("should do nothing on invalid 'shares' arg", function() {
const oldPrice = stock.price;
const oldTracker = stock.shareTxUntilMovement;
processBuyTransactionPriceMovement(stock, NaN, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(oldTracker);
processBuyTransactionPriceMovement(stock, -1, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(oldTracker);
});
it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() {
const oldPrice = stock.price;
processBuyTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares);
});
it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() {
const oldPrice = stock.price;
processBuyTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Short);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares);
});
it("should properly evaluate LONG transactions that trigger price movements", function() {
const oldPrice = stock.price;
function getNthPrice(n) {
let price = oldPrice;
for (let i = 1; i < n; ++i) {
price *= calculateIncreasingPriceMovement(stock);
}
return price;
}
processBuyTransactionPriceMovement(stock, mvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(getNthPrice(4));
expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares);
});
it("should properly evaluate SHORT transactions that trigger price movements", function() {
const oldPrice = stock.price;
function getNthPrice(n) {
let price = oldPrice;
for (let i = 1; i < n; ++i) {
price *= calculateDecreasingPriceMovement(stock);
}
return price;
}
processBuyTransactionPriceMovement(stock, mvmtShares, PositionTypes.Short);
expect(stock.price).to.equal(getNthPrice(4));
expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares);
});
});
describe("processSellTransactionPriceMovement()", function() {
const noMvmtShares = Math.round(ctorParams.shareTxForMovement / 2.2);
const mvmtShares = ctorParams.shareTxForMovement * 3 + noMvmtShares;
it("should do nothing on invalid 'stock' argument", function() {
const oldPrice = stock.price;
const oldTracker = stock.shareTxUntilMovement;
processSellTransactionPriceMovement({}, mvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(oldTracker);
});
it("should do nothing on invalid 'shares' arg", function() {
const oldPrice = stock.price;
const oldTracker = stock.shareTxUntilMovement;
processSellTransactionPriceMovement(stock, NaN, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(oldTracker);
processSellTransactionPriceMovement(stock, -1, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(oldTracker);
});
it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() {
const oldPrice = stock.price;
processSellTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares);
});
it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() {
const oldPrice = stock.price;
processSellTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Short);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares);
});
it("should properly evaluate LONG transactions that trigger price movements", function() {
const oldPrice = stock.price;
function getNthPrice(n) {
let price = oldPrice;
for (let i = 1; i < n; ++i) {
price *= calculateDecreasingPriceMovement(stock);
}
return price;
}
processSellTransactionPriceMovement(stock, mvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(getNthPrice(4));
expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares);
});
it("should properly evaluate SHORT transactions that trigger price movements", function() {
const oldPrice = stock.price;
function getNthPrice(n) {
let price = oldPrice;
for (let i = 1; i < n; ++i) {
price *= calculateIncreasingPriceMovement(stock);
}
return price;
}
processSellTransactionPriceMovement(stock, mvmtShares, PositionTypes.Short);
expect(stock.price).to.equal(getNthPrice(4));
expect(stock.shareTxUntilMovement).to.equal(stock.shareTxForMovement - noMvmtShares);
});
});
});
});

@ -3,17 +3,20 @@
<head>
<meta charset="utf-8">
<title>Mocha Tests</title>
<link href="https://unpkg.com/mocha@4.0.1/mocha.css" rel="stylesheet" />
<link href="https://unpkg.com/mocha@6.1.4/mocha.css" rel="stylesheet" />
</head>
<body>
<div id="mocha"></div>
<script src="https://unpkg.com/mocha@4.0.1/mocha.js"></script>
<script src="https://unpkg.com/chai/chai.js"></script>
<script src="https://unpkg.com/mocha/mocha.js"></script>
<script>mocha.setup('bdd')</script>
<script type="module" src="tests.bundle.js"></script>
<script type="module">
<script class="mocha-init">
mocha.setup('bdd');
mocha.checkLeaks();
</script>
<script type="module" src="tests.bundle.js"></script>
<script class="mocha-exec" type="module">
mocha.run();
</script>
</body>

@ -1,3 +1 @@
//require("babel-core/register");
//require("babel-polyfill");
module.exports = require("./NetscriptJSTest.js");
export * from "./StockMarketTests";