mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-09 17:23:53 +01:00
Merge branch 'dev' of https://github.com/danielyxie/bitburner into dev
This commit is contained in:
commit
9df054dd0c
@ -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>
|
||||
|
12
doc/source/netscript/tixapi/getStockAskPrice.rst
Normal file
12
doc/source/netscript/tixapi/getStockAskPrice.rst
Normal file
@ -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.
|
12
doc/source/netscript/tixapi/getStockBidPrice.rst
Normal file
12
doc/source/netscript/tixapi/getStockBidPrice.rst
Normal file
@ -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");
|
||||
|
15
doc/source/netscript/tixapi/getStockPurchaseCost.rst
Normal file
15
doc/source/netscript/tixapi/getStockPurchaseCost.rst
Normal file
@ -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.
|
15
doc/source/netscript/tixapi/getStockSaleGain.rst
Normal file
15
doc/source/netscript/tixapi/getStockSaleGain.rst
Normal file
@ -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
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.
|
||||
|
5
src/StockMarket/IOrderBook.ts
Normal file
5
src/StockMarket/IOrderBook.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Order } from "./Order";
|
||||
|
||||
export interface IOrderBook {
|
||||
[key: string]: Order[];
|
||||
}
|
10
src/StockMarket/IStockMarket.ts
Normal file
10
src/StockMarket/IStockMarket.ts
Normal file
@ -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
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
625
src/StockMarket/StockMarket.jsx
Normal file
625
src/StockMarket/StockMarket.jsx
Normal file
@ -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
|
||||
)
|
||||
}
|
||||
}
|
230
src/StockMarket/StockMarketHelpers.ts
Normal file
230
src/StockMarket/StockMarketHelpers.ts
Normal file
@ -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);
|
||||
}
|
873
src/StockMarket/data/InitStockMetadata.ts
Normal file
873
src/StockMarket/data/InitStockMetadata.ts
Normal file
@ -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"],
|
||||
},
|
||||
];
|
6
src/StockMarket/data/OrderTypes.ts
Normal file
6
src/StockMarket/data/OrderTypes.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum OrderTypes {
|
||||
LimitBuy = "Limit Buy Order",
|
||||
LimitSell = "Limit Sell Order",
|
||||
StopBuy = "Stop Buy Order",
|
||||
StopSell = "Stop Sell Order"
|
||||
}
|
4
src/StockMarket/data/PositionTypes.ts
Normal file
4
src/StockMarket/data/PositionTypes.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum PositionTypes {
|
||||
Long = "L",
|
||||
Short = "S"
|
||||
}
|
41
src/StockMarket/data/StockSymbols.ts
Normal file
41
src/StockMarket/data/StockSymbols.ts
Normal file
@ -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";
|
11
src/StockMarket/data/TickerHeaderFormatData.ts
Normal file
11
src/StockMarket/data/TickerHeaderFormatData.ts
Normal file
@ -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);
|
||||
}
|
226
src/StockMarket/ui/InfoAndPurchases.tsx
Normal file
226
src/StockMarket/ui/InfoAndPurchases.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
75
src/StockMarket/ui/Root.tsx
Normal file
75
src/StockMarket/ui/Root.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
435
src/StockMarket/ui/StockTicker.tsx
Normal file
435
src/StockMarket/ui/StockTicker.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
41
src/StockMarket/ui/StockTickerHeaderText.tsx
Normal file
41
src/StockMarket/ui/StockTickerHeaderText.tsx
Normal file
@ -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>;
|
||||
}
|
42
src/StockMarket/ui/StockTickerOrder.tsx
Normal file
42
src/StockMarket/ui/StockTickerOrder.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
33
src/StockMarket/ui/StockTickerOrderList.tsx
Normal file
33
src/StockMarket/ui/StockTickerOrderList.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
114
src/StockMarket/ui/StockTickerPositionText.tsx
Normal file
114
src/StockMarket/ui/StockTickerPositionText.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
41
src/StockMarket/ui/StockTickerTxButton.tsx
Normal file
41
src/StockMarket/ui/StockTickerTxButton.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
146
src/StockMarket/ui/StockTickers.tsx
Normal file
146
src/StockMarket/ui/StockTickers.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
61
src/StockMarket/ui/StockTickersConfig.tsx
Normal file
61
src/StockMarket/ui/StockTickersConfig.tsx
Normal file
@ -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 -->
|
||||
|
22
src/types.ts
22
src/types.ts
@ -44,3 +44,25 @@ export interface IReturnStatus {
|
||||
res: boolean;
|
||||
msg?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the minimum and maximum values for a range.
|
||||
* It is up to the consumer if these values are inclusive or exclusive.
|
||||
* It is up to the implementor to ensure max > min.
|
||||
*/
|
||||
export interface IMinMaxRange {
|
||||
/**
|
||||
* Value by which the bounds are to be divided for the final range
|
||||
*/
|
||||
divisor?: number;
|
||||
|
||||
/**
|
||||
* The maximum bound of the range.
|
||||
*/
|
||||
max: number;
|
||||
|
||||
/**
|
||||
* The minimum bound of the range.
|
||||
*/
|
||||
min: number;
|
||||
}
|
||||
|
73
src/ui/React/Accordion.tsx
Normal file
73
src/ui/React/Accordion.tsx
Normal file
@ -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
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";
|
||||
|
Loading…
Reference in New Issue
Block a user