Last active 1697523181

My bitburner scripts. adapted from alain mostly

_firstboot.js Raw
1/** @param {NS} ns */
2// script to be run after each augment to recover hack level and money
3// 1. run scan.js, backdoor n00dles and foodnstuff using the links provided
4// 2. run this script, starts hacking those servers
5// 3. once you have 200k, go to aevum, then run alain/casino.js
6// 3a. go to tech store, buy darkweb, connect darkweb, buy -a
7// 4. run scan.js, nuke and backdoor all available servers
8// 5. run purchase.js, update with higher ram allowance (32>2048) if you have enough money
9// * run delete-pserv.js if there's an issue with buying the servers
10// 6. run init.js, this will boot up stockmaster and stats, as well as starting the hack scripts running across all pservs and home
11export async function main(ns) {
12 const scripts = [
13 {name: "/alain/stats.js", args:[]},
14 {name: "/local.js", args:[]},
15 ];
16 for(const {name,args,waitFor=false} of scripts) {
17 const pid = ns.run(name,1,...args);
18 if(pid) {
19 ns.tprint(`Started ${name} with [${args}]`);
20 if(waitFor) {
21 ns.tprint("Waiting for it to exit...");
22 await awaitByPid(ns,pid);
23 }
24 }
25 else
26 ns.tprint(`ERROR: Failed to start ${name} with ${args}!`);
27 await ns.sleep(100);
28 }
29}
_init.js Raw
1/** @param {NS} ns */
2export async function main(ns) {
3 const scripts = [
4 {name: "/alain/stats.js", args:[]},
5 {name: "/stockmaster.js", args:["--toastStyle", ""], tail:true},
6 {name: "/pserv.js", args:[], tail:true},
7 {name: "/local.js", args:[], tail:true},
8 {name: "/alain/hacknet-upgrade-manager.js", args:["-c", "--reserve", "5000000000", "--time", "1000h", "--interval", "100"], tail:true},
9 ];
10 for(const {name,args,waitFor=false,tail=false} of scripts) {
11 const pid = ns.run(name,1,...args);
12 if(pid) {
13 if (tail) {
14 ns.tail(pid);
15 await ns.sleep(0);
16 ns.resizeTail(425, 203, pid);
17 }
18 ns.tprint(`Started ${name} with [${args}]`);
19 if(waitFor) {
20 ns.tprint("Waiting for it to exit...");
21 await awaitByPid(ns,pid);
22 }
23 }
24 else
25 ns.tprint(`ERROR: Failed to start ${name} with ${args}!`);
26 await ns.sleep(100);
27 }
28}
_shutdown.js Raw
1/** @param {NS} ns */
2export async function main(ns) {
3 const scripts = [
4 {name: "/alain/stockmaster.js", args:["-l"]},
5 {name: "/alain/kill-all-scripts.js", args:[]},
6 ];
7 for(const {name,args,waitFor=false} of scripts) {
8 const pid = ns.run(name,1,...args);
9 if(pid) {
10 ns.tprint(`Started ${name} with [${args}]`);
11 if(waitFor) {
12 ns.tprint("Waiting for it to exit...");
13 await awaitByPid(ns,pid);
14 }
15 }
16 else
17 ns.tprint(`ERROR: Failed to start ${name} with ${args}!`);
18 await ns.sleep(100);
19 }
20}
delete-pserv.js Raw
1export async function main(ns) {
2 // Iterator we'll use for our loop
3 let i = 0;
4
5 while (i < ns.getPurchasedServerLimit()) {
6 let hostname = "pserv-" + i;
7 ns.deleteServer(hostname)
8 ++i;
9 await ns.sleep(100);
10 }
11}
early-hack-template.js Raw
1import { targets } from "/targets.js";
2
3export async function main(ns) {
4 const args = ns.flags([['help', false]]);
5 let hostname = args._[0];
6 if(!hostname) {
7 hostname = targets[Math.floor(Math.random()*targets.length)];
8 }
9 while (true) {
10 if (ns.getServerSecurityLevel(hostname) > (ns.getServerMinSecurityLevel(hostname)+1)) {
11 await ns.weaken(hostname);
12 } else if (ns.getServerMoneyAvailable(hostname) < (ns.getServerMaxMoney(hostname)*0.75)) {
13 await ns.grow(hostname);
14 } else {
15 await ns.hack(hostname);
16 }
17 }
18}
local.js Raw
1export async function main(ns) {
2 var i = 0;
3 var ram = ns.getScriptRam("early-hack-template.js")*100,
4 total_ram = ns.getServerMaxRam("home")-1024,
5 used_ram = ns.getServerUsedRam("home");
6
7 while (i < ((total_ram-used_ram)/ram)) {
8 ns.run("early-hack-template.js", 100);
9 ++i;
10 await ns.sleep(100);
11 }
12}
open.js Raw
1/** @param {NS} ns **/
2export async function main(ns) {
3 ns.tail()
4 const target = ns.args[0];
5 try { ns.brutessh(target); } catch { }
6 try { ns.ftpcrack(target); } catch { }
7 try { ns.relaysmtp(target); } catch { }
8 try { ns.httpworm(target); } catch { }
9 try { ns.sqlinject(target); } catch { }
10 try { ns.nuke(target); } catch { }
11}
pserv.js Raw
1import { targets } from "/targets.js";
2
3export async function main(ns) {
4 // Iterator we'll use for our loop
5 let i = 0;
6
7 while (i < ns.getPurchasedServerLimit()) {
8 let hostname = "pserv-" + i;
9// ns.deleteServer(hostname)
10// ns.purchaseServer(hostname, 1024)
11 ns.scp("targets.js", hostname);
12 ns.scp("early-hack-template.js", hostname);
13 for (var z = 0; z < Math.floor(ns.getServerMaxRam(hostname)/2.4/10); z++)
14 ns.exec("early-hack-template.js", hostname, 10, targets[z % targets.length]);
15 ++i;
16 await ns.sleep(100);
17 }
18}
purchase.js Raw
1export async function main(ns) {
2 // Iterator we'll use for our loop
3 let i = 0;
4 ns.tail();
5
6 while (i < ns.getPurchasedServerLimit()) {
7 let hostname = "pserv-" + i;
8 ns.purchaseServer(hostname, 32) // update this by powers of 2 as you get more money. i usually have it at 2048
9 ++i;
10 await ns.sleep(100);
11 }
12}
scan.js Raw
1/**
2 * @param {NS} ns
3 * @returns interactive server map
4 */
5export function main(ns) {
6 var targets_outp = [];
7 const factionServers = ["CSEC", "avmnite-02h", "I.I.I.I", "run4theh111z", "w0r1d_d43m0n", "fulcrumassets"],
8 css = ` <style id="scanCSS">
9 .serverscan {white-space:pre; color:#ccc; font:14px monospace; line-height: 16px; }
10 .serverscan .server {color:#080;cursor:pointer;text-decoration:underline}
11 .serverscan .faction {color:#088}
12 .serverscan .rooted {color:#6f3}
13 .serverscan .rooted.faction {color:#0ff}
14 .serverscan .rooted::before {color:#6f3}
15 .serverscan .hack {display:inline-block; font:12px monospace}
16 .serverscan .red {color:red;}
17 .serverscan .green {color:green;}
18 .serverscan .nuke {color:#6f3}
19 .serverscan .backdoor {color:#6f3}
20 .serverscan .linky {font:12px monospace}
21 .serverscan .linky > a {cursor:pointer; text-decoration:underline;}
22 .serverscan .cct {color:#0ff;}
23 </style>`,
24 doc = eval("document"),
25 terminalInsert = html => doc.getElementById("terminal").insertAdjacentHTML('beforeend', `<li>${html}</li>`),
26 terminalInput = doc.getElementById("terminal-input"),
27 terminalEventHandlerKey = Object.keys(terminalInput)[1],
28 setNavCommand = async inputValue => {
29 terminalInput.value = inputValue
30 terminalInput[terminalEventHandlerKey].onChange({ target: terminalInput })
31 terminalInput.focus()
32 await terminalInput[terminalEventHandlerKey].onKeyDown({ key: 'Enter', preventDefault: () => 0 })
33 },
34 myHackLevel = ns.getHackingLevel(),
35 serverInfo = (serverName) => {
36 // Costs 2 GB. If you can't don't need backdoor links, uncomment and use the alternate implementations below
37 return ns.getServer(serverName)
38 /* return {
39 requiredHackingSkill: ns.getServerRequiredHackingLevel(serverName),
40 hasAdminRights: ns.hasRootAccess(serverName),
41 purchasedByPlayer: serverName.includes('daemon') || serverName.includes('hacknet'),
42 backdoorInstalled: true // No way of knowing without ns.getServer
43 } */
44 },
45 createServerEntry = serverName => {
46 let server = serverInfo(serverName),
47 requiredHackLevel = server.requiredHackingSkill,
48 rooted = server.hasAdminRights,
49 canHack = requiredHackLevel <= myHackLevel,
50 shouldBackdoor = !server?.backdoorInstalled && canHack && serverName != 'home' && rooted && !server.purchasedByPlayer,
51 contracts = ns.ls(serverName, ".cct")
52 if (canHack &&
53 serverName != 'home' &&
54 rooted &&
55 !server.purchasedByPlayer &&
56 !factionServers.includes(serverName)) targets_outp.push(serverName);
57 if (!canHack && ns.args[0] != "-a") return "";
58
59 return `<span id="${serverName}">`
60 + `<a class="server${factionServers.includes(serverName) ? " faction" : ""}`
61 + `${rooted ? " rooted" : ""}">${serverName}</a>`
62 + (server.purchasedByPlayer ? '' : ` <span class="hack ${(canHack ? 'green' : 'red')} monitor linky">(<a>${requiredHackLevel}</a>)</span>`)
63 + `${((canHack && !rooted) ? ' <span class="nuke linky">[<a>nuke</a>]</span>' : '')}`
64 + `${((canHack && !rooted) || shouldBackdoor ? ' <span class="backdoor linky">[<a>backdoor</a>]</span>' : '')}`
65 + ` ${contracts.map(c => `<span class="cct" title="${c}">@</span>`)}`
66 + "</span>"
67 },
68 buildOutput = (parent = servers[0], prefix = ["\n"]) => {
69 let serverEntry = createServerEntry(parent);
70 if (serverEntry == "") return "";
71 let output = prefix.join("") + serverEntry
72 for (let i = 0; i < servers.length; i++) {
73 if (parentByIndex[i] != parent) continue
74 let newPrefix = prefix.slice()
75 const appearsAgain = parentByIndex.slice(i + 1).includes(parentByIndex[i]),
76 lastElementIndex = newPrefix.length - 1
77
78 newPrefix.push(appearsAgain ? "├╴" : "└╴")
79
80 newPrefix[lastElementIndex] = newPrefix[lastElementIndex].replace("├╴", "│ ").replace("└╴", " ")
81 output += buildOutput(servers[i], newPrefix)
82 }
83
84 return output
85 },
86 ordering = (serverA, serverB) => {
87 // Sort servers with fewer connections towards the top.
88 let orderNumber = ns.scan(serverA).length - ns.scan(serverB).length
89 // Purchased servers to the very top
90 orderNumber = orderNumber != 0 ? orderNumber
91 : serverInfo(serverB).purchasedByPlayer - serverInfo(serverA).purchasedByPlayer
92 // Hack: compare just the first 2 chars to keep purchased servers in order purchased
93 orderNumber = orderNumber != 0 ? orderNumber
94 : serverA.slice(0, 2).toLowerCase().localeCompare(serverB.slice(0, 2).toLowerCase())
95
96 return orderNumber
97 }
98
99 // refresh css (in case it changed)
100 doc.getElementById("scanCSS")?.remove()
101 doc.head.insertAdjacentHTML('beforeend', css)
102 let servers = ["home"],
103 parentByIndex = [""],
104 routes = { home: "home" }
105 for (let server of servers)
106 for (let oneScanResult of ns.scan(server).sort(ordering))
107 if (!servers.includes(oneScanResult)) {
108 const backdoored = serverInfo(oneScanResult)?.backdoorInstalled
109 servers.push(oneScanResult)
110 parentByIndex.push(server)
111 routes[oneScanResult] = backdoored ? "connect " + oneScanResult : routes[server] + ";connect " + oneScanResult
112 }
113
114 terminalInsert(`<div class="serverscan new">${buildOutput()}</div>`)
115 doc.querySelectorAll(".serverscan.new .server").forEach(serverEntry => serverEntry
116 .addEventListener('click', setNavCommand.bind(null, routes[serverEntry.childNodes[0].nodeValue])))
117 doc.querySelectorAll(".serverscan.new .monitor").forEach(monitorButton => monitorButton
118 .addEventListener('click', setNavCommand.bind(null, "run monitor.js " + monitorButton.parentNode.childNodes[0].childNodes[0].nodeValue)))
119 doc.querySelectorAll(".serverscan.new .nuke").forEach(nukeButton => nukeButton
120 .addEventListener('click', setNavCommand.bind(null, "home;run open.js " + nukeButton.parentNode.childNodes[0].childNodes[0].nodeValue)))
121 doc.querySelectorAll(".serverscan.new .backdoor").forEach(backdoorButton => backdoorButton
122 .addEventListener('click', setNavCommand.bind(null, routes[backdoorButton.parentNode.childNodes[0].childNodes[0].nodeValue] + ";backdoor")))
123 doc.querySelector(".serverscan.new").classList.remove("new")
124 ns.write("/targets.js", "export var targets = [", "w");
125 for (let name of targets_outp) {
126 ns.write("/targets.js", `"${name}",`, "a");
127 }
128 ns.write("/targets.js", "]", "a")
129}
130
stockmaster.js Raw
1// this is in here just to be able to disable toasts
2
3import {
4 instanceCount, getConfiguration, getNsDataThroughFile, runCommand, getActiveSourceFiles, tryGetBitNodeMultipliers,
5 formatMoney, formatNumberShort, formatDuration, getStockSymbols
6} from 'alain/helpers.js'
7
8let disableShorts = false;
9let commission = 100000; // Buy/sell commission. Expected profit must exceed this to buy anything.
10let totalProfit = 0.0; // We can keep track of how much we've earned since start.
11let lastLog = ""; // We update faster than the stock-market ticks, but we don't log anything unless there's been a change
12let allStockSymbols = null; // Stores the set of all symbols collected at start
13let mock = false; // If set to true, will "mock" buy/sell but not actually buy/sell anythingorecast
14let noisy = false; // If set to true, tprints and announces each time stocks are bought/sold
15let toastStyle = "info";
16let dictSourceFiles; // Populated at init, a dictionary of source-files the user has access to, and their level
17// Pre-4S configuration (influences how we play the stock market before we have 4S data, after which everything's fool-proof)
18let showMarketSummary = false; // If set to true, will always generate and display the pre-4s forecast table in a separate tail window
19let minTickHistory; // This much history must be gathered before we will offer a stock forecast.
20let longTermForecastWindowLength; // This much history will be used to determine the historical probability of the stock (so long as no inversions are detected)
21let nearTermForecastWindowLength; // This much history will be used to detect recent negative trends and act on them immediately.
22// The following pre-4s constants are hard-coded (not configurable via command line) but may require tweaking
23const marketCycleLength = 75; // Every this many ticks, all stocks have a 45% chance of "reversing" their probability. Something we must detect and act on quick to not lose profits.
24const maxTickHistory = 151; // This much history will be kept for purposes of detemining volatility and perhaps one day pinpointing the market cycle tick
25const inversionDetectionTolerance = 0.10; // If the near-term forecast is within this distance of (1 - long-term forecast), consider it a potential "inversion"
26const inversionLagTolerance = 5; // An inversion is "trusted" up to this many ticks after the normal nearTermForecastWindowLength expected detection time
27// (Note: 33 total stocks * 45% inversion chance each cycle = ~15 expected inversions per cycle)
28// The following pre-4s values are set during the lifetime of the program
29let marketCycleDetected = false; // We should not make risky purchasing decisions until the stock market cycle is detected. This can take a long time, but we'll be thanked
30let detectedCycleTick = 0; // This will be reset to zero once we've detected the market cycle point.
31let inversionAgreementThreshold = 6; // If this many stocks are detected as being in an "inversion", consider this the stock market cycle point
32const expectedTickTime = 6000;
33const catchUpTickTime = 4000;
34let lastTick = 0;
35let sleepInterval = 1000;
36let resetInfo = (/**@returns{ResetInfo}*/() => undefined)(); // Information about the current bitnode
37
38let options;
39const argsSchema = [
40 ['l', false], // Stop any other running stockmaster.js instances and sell all stocks
41 ['liquidate', false], // Long-form alias for the above flag.
42 ['mock', false], // If set to true, will "mock" buy/sell but not actually buy/sell anything
43 ['noisy', false], // If set to true, tprints and announces each time stocks are bought/sold
44 ['toastStyle', toastStyle],
45 ['disable-shorts', false], // If set to true, will not short any stocks. Will be set depending on having SF8.2 by default.
46 ['reserve', null], // A fixed amount of money to not spend
47 ['fracB', 0.4], // Fraction of assets to have as liquid before we consider buying more stock
48 ['fracH', 0.2], // Fraction of assets to retain as cash in hand when buying
49 ['buy-threshold', 0.0001], // Buy only stocks forecasted to earn better than a 0.01% return (1 Basis Point)
50 ['sell-threshold', 0], // Sell stocks forecasted to earn less than this return (default 0% - which happens when prob hits 50% or worse)
51 ['diversification', 0.34], // Before we have 4S data, we will not hold more than this fraction of our portfolio as a single stock
52 ['disableHud', false], // Disable showing stock value in the HUD panel
53 ['disable-purchase-tix-api', false], // Disable purchasing the TIX API if you do not already have it.
54 // The following settings are related only to tweaking pre-4s stock-market logic
55 ['show-pre-4s-forecast', false], // If set to true, will always generate and display the pre-4s forecast (if false, it's only shown while we hold no stocks)
56 ['show-market-summary', false], // Same effect as "show-pre-4s-forecast", this market summary has become so informative, it's valuable even with 4s
57 ['pre-4s-buy-threshold-probability', 0.15], // Before we have 4S data, only buy stocks whose probability is more than this far away from 0.5, to account for imprecision
58 ['pre-4s-buy-threshold-return', 0.0015], // Before we have 4S data, Buy only stocks forecasted to earn better than this return (default 0.25% or 25 Basis Points)
59 ['pre-4s-sell-threshold-return', 0.0005], // Before we have 4S data, Sell stocks forecasted to earn less than this return (default 0.15% or 15 Basis Points)
60 ['pre-4s-min-tick-history', 21], // This much history must be gathered before we will use pre-4s stock forecasts to make buy/sell decisions. (Default 21)
61 ['pre-4s-forecast-window', 51], // This much history will be used to determine the historical probability of the stock (so long as no inversions are detected) (Default 76)
62 ['pre-4s-inversion-detection-window', 10], // This much history will be used to detect recent negative trends and act on them immediately. (Default 10)
63 ['pre-4s-min-blackout-window', 10], // Do not make any new purchases this many ticks before the detected stock market cycle tick, to avoid buying a position that reverses soon after
64 ['pre-4s-minimum-hold-time', 10], // A recently bought position must be held for this long before selling, to avoid rash decisions due to noise after a fresh market cycle. (Default 10)
65 ['buy-4s-budget', 0.8], // Maximum corpus value we will sacrifice in order to buy 4S. Setting to 0 will never buy 4s.
66];
67
68export function autocomplete(data, args) {
69 data.flags(argsSchema);
70 return [];
71}
72
73/** Requires access to the TIX API. Purchases access to the 4S Mkt Data API as soon as it can
74 * @param {NS} ns */
75export async function main(ns) {
76 const runOptions = getConfiguration(ns, argsSchema);
77 if (!runOptions) return; // Invalid options, or ran in --help mode.
78
79 // If given the "liquidate" command, try to kill any versions of this script trading in stocks
80 // NOTE: We must do this immediately before we start resetting / overwriting global state below (which is shared between script instances)
81 const hasTixApiAccess = await getNsDataThroughFile(ns, 'ns.stock.hasTIXAPIAccess()');
82 if (runOptions.l || runOptions.liquidate) {
83 if (!hasTixApiAccess) return log(ns, 'ERROR: Cannot liquidate stocks because we do not have Tix Api Access', true, 'error');
84 log(ns, 'INFO: Killing any other stockmaster processes...', false, toastStyle);
85 await runCommand(ns, `ns.ps().filter(proc => proc.filename == '${ns.getScriptName()}' && !proc.args.includes('-l') && !proc.args.includes('--liquidate'))` +
86 `.forEach(proc => ns.kill(proc.pid))`, '/Temp/kill-stockmarket-scripts.js');
87 log(ns, 'INFO: Checking for and liquidating any stocks...', false, toastStyle);
88 await liquidate(ns); // Sell all stocks
89 return;
90 } // Otherwise, prevent multiple instances of this script from being started, even with different args.
91 if (await instanceCount(ns) > 1) return;
92
93 ns.disableLog("ALL");
94 // Extract various options from the args (globals, purchasing decision factors, pre-4s factors)
95 options = runOptions; // We don't set the global "options" until we're sure this is the only running instance
96 mock = options.mock;
97 noisy = options.noisy;
98 const fracB = options.fracB;
99 const fracH = options.fracH;
100 const diversification = options.diversification;
101 const disableHud = options.disableHud || options.liquidate || options.mock;
102 disableShorts = options['disable-shorts'];
103 const pre4sBuyThresholdProbability = options['pre-4s-buy-threshold-probability'];
104 const pre4sMinBlackoutWindow = options['pre-4s-min-blackout-window'] || 1;
105 const pre4sMinHoldTime = options['pre-4s-minimum-hold-time'] || 0;
106 minTickHistory = options['pre-4s-min-tick-history'] || 21;
107 nearTermForecastWindowLength = options['pre-4s-inversion-detection-window'] || 10;
108 longTermForecastWindowLength = options['pre-4s-forecast-window'] || (marketCycleLength + 1);
109 showMarketSummary = options['show-pre-4s-forecast'] || options['show-market-summary'];
110 // Other global values must be reset at start lest they be left in memory from a prior run
111 lastTick = 0, totalProfit = 0, lastLog = "", marketCycleDetected = false, detectedCycleTick = 0, inversionAgreementThreshold = 6;
112 let myStocks = [], allStocks = [];
113 let player = await getPlayerInfo(ns);
114 resetInfo = await getNsDataThroughFile(ns, 'ns.getResetInfo()');
115
116 if (!hasTixApiAccess) { // You cannot use the stockmaster until you have API access
117 if (options['disable-purchase-tix-api'])
118 return log(ns, "ERROR: You do not have stock market API access, and --disable-purchase-tix-api is set.", true);
119 let success = false;
120 log(ns, `INFO: You are missing stock market API access. (NOTE: This is granted for free once you have SF8). ` +
121 `Waiting until we can have the 5b needed to buy it. (Run with --disable-purchase-tix-api to disable this feature.)`, true);
122 do {
123 await ns.sleep(sleepInterval);
124 try {
125 const reserve = options['reserve'] != null ? options['reserve'] : Number(ns.read("reserve.txt") || 0);
126 success = await tryGetStockMarketAccess(ns, player.money - reserve);
127 } catch (err) {
128 log(ns, `WARNING: stockmaster.js Caught (and suppressed) an unexpected error while waiting to buy stock market access:\n` +
129 (typeof err === 'string' ? err : err.message || JSON.stringify(err)), false, 'warning');
130 }
131 } while (!success);
132 }
133
134 dictSourceFiles = await getActiveSourceFiles(ns); // Find out what source files the user has unlocked
135 if (!disableShorts && (!(8 in dictSourceFiles) || dictSourceFiles[8] < 2)) {
136 log(ns, "INFO: Shorting stocks has been disabled (you have not yet unlocked access to shorting)");
137 disableShorts = true;
138 }
139
140 allStockSymbols = await getStockSymbols(ns);
141 allStocks = await initAllStocks(ns);
142
143 let bitnodeMults;
144 if (5 in dictSourceFiles) bitnodeMults = await tryGetBitNodeMultipliers(ns);
145 // Assume bitnode mults are 1 if user doesn't have this API access yet
146 if (!bitnodeMults) bitnodeMults = { FourSigmaMarketDataCost: 1, FourSigmaMarketDataApiCost: 1 };
147
148 if (showMarketSummary) await launchSummaryTail(ns); // Opens a separate script / window to continuously display the Pre4S forecast
149
150 let hudElement = null;
151 if (!disableHud) {
152 hudElement = initializeHud();
153 ns.atExit(() => hudElement.parentElement.parentElement.parentElement.removeChild(hudElement.parentElement.parentElement));
154 }
155
156 log(ns, `Welcome! Please note: all stock purchases will initially result in a Net (unrealized) Loss. This is not only due to commission, but because each stock has a 'spread' (difference in buy price and sell price). ` +
157 `This script is designed to buy stocks that are most likely to surpass that loss and turn a profit, but it will take a few minutes to see the progress.\n\n` +
158 `If you choose to stop the script, make sure you SELL all your stocks (can go 'run ${ns.getScriptName()} --liquidate') to get your money back.\n\nGood luck!\n~ Insight\n\n`)
159
160 let pre4s = true;
161 while (true) {
162 try {
163 const playerStats = await getPlayerInfo(ns);
164 const reserve = options['reserve'] != null ? options['reserve'] : Number(ns.read("reserve.txt") || 0);
165 // Check whether we have 4s access yes (once we do, we can stop checking)
166 if (pre4s) pre4s = !(await checkAccess(ns, 'has4SDataTIXAPI'));
167 const holdings = await refresh(ns, !pre4s, allStocks, myStocks); // Returns total stock value
168 const corpus = holdings + playerStats.money; // Corpus means total stocks + cash
169 const maxHoldings = (1 - fracH) * corpus; // The largest value of stock we could hold without violiating fracH (Fraction to keep as cash)
170 if (pre4s && !mock && await tryGet4SApi(ns, playerStats, bitnodeMults, corpus * (options['buy-4s-budget'] - fracH) - reserve))
171 continue; // Start the loop over if we just bought 4S API access
172 // Be more conservative with our decisions if we don't have 4S data
173 const thresholdToBuy = pre4s ? options['pre-4s-buy-threshold-return'] : options['buy-threshold'];
174 const thresholdToSell = pre4s ? options['pre-4s-sell-threshold-return'] : options['sell-threshold'];
175 if (myStocks.length > 0)
176 doStatusUpdate(ns, allStocks, myStocks, hudElement);
177 else if (hudElement) hudElement.innerText = "$0.000 ";
178 if (pre4s && allStocks[0].priceHistory.length < minTickHistory) {
179 log(ns, `Building a history of stock prices (${allStocks[0].priceHistory.length}/${minTickHistory})...`);
180 await ns.sleep(sleepInterval);
181 continue;
182 }
183
184 // Sell forecasted-to-underperform shares (worse than some expected return threshold)
185 let sales = 0;
186 for (let stk of myStocks) {
187 if (stk.absReturn() <= thresholdToSell || stk.bullish() && stk.sharesShort > 0 || stk.bearish() && stk.sharesLong > 0) {
188 if (pre4s && stk.ticksHeld < pre4sMinHoldTime) {
189 if (!stk.warnedBadPurchase) log(ns, `WARNING: Thinking of selling ${stk.sym} with ER ${formatBP(stk.absReturn())}, but holding out as it was purchased just ${stk.ticksHeld} ticks ago...`);
190 stk.warnedBadPurchase = true; // Hack to ensure we don't spam this warning
191 } else {
192 sales += await doSellAll(ns, stk);
193 stk.warnedBadPurchase = false;
194 }
195 }
196 }
197 if (sales > 0) continue; // If we sold anything, loop immediately (no need to sleep) and refresh stats immediately before making purchasing decisions.
198
199 // If we haven't gone above a certain liquidity threshold, don't attempt to buy more stock
200 // Avoids death-by-a-thousand-commissions before we get super-rich, stocks are capped, and this is no longer an issue
201 // BUT may mean we miss striking while the iron is hot while waiting to build up more funds.
202 if (playerStats.money / corpus > fracB) {
203 // Compute the cash we have to spend (such that spending it all on stock would bring us down to a liquidity of fracH)
204 let cash = Math.min(playerStats.money - reserve, maxHoldings - holdings);
205 // If we haven't detected the market cycle (or haven't detected it reliably), assume it might be quite soon and restrict bets to those that can turn a profit in the very-near term.
206 const estTick = Math.max(detectedCycleTick, marketCycleLength - (!marketCycleDetected ? 10 : inversionAgreementThreshold <= 8 ? 20 : inversionAgreementThreshold <= 10 ? 30 : marketCycleLength));
207 // Buy shares with cash remaining in hand if exceeding some buy threshold. Prioritize targets whose expected return will cover the ask/bit spread the soonest
208 for (const stk of allStocks.sort(purchaseOrder)) {
209 if (cash <= 0) break; // Break if we are out of money (i.e. from prior purchases)
210 // Do not purchase a stock if it is not forecasted to recover from the ask/bid spread before the next market cycle and potential probability inversion
211 if (stk.blackoutWindow() >= marketCycleLength - estTick) continue;
212 if (pre4s && (Math.max(pre4sMinHoldTime, pre4sMinBlackoutWindow) >= marketCycleLength - estTick)) continue;
213 // Skip if we already own all possible shares in this stock, or if the expected return is below our threshold, or if shorts are disabled and stock is bearish
214 if (stk.ownedShares() == stk.maxShares || stk.absReturn() <= thresholdToBuy || (disableShorts && stk.bearish())) continue;
215 // If pre-4s, do not purchase any stock whose last inversion was too recent, or whose probability is too close to 0.5
216 if (pre4s && (stk.lastInversion < minTickHistory || Math.abs(stk.prob - 0.5) < pre4sBuyThresholdProbability)) continue;
217
218 // Enforce diversification: Don't hold more than x% of our portfolio as a single stock (as corpus increases, this naturally stops being a limiter)
219 // Inflate our budget / current position value by a factor of stk.spread_pct to avoid repeated micro-buys of a stock due to the buy/ask spread making holdings appear more diversified after purchase
220 let budget = Math.min(cash, maxHoldings * (diversification + stk.spread_pct) - stk.positionValue() * (1.01 + stk.spread_pct))
221 let purchasePrice = stk.bullish() ? stk.ask_price : stk.bid_price; // Depends on whether we will be buying a long or short position
222 let affordableShares = Math.floor((budget - commission) / purchasePrice);
223 let numShares = Math.min(stk.maxShares - stk.ownedShares(), affordableShares);
224 if (numShares <= 0) continue;
225 // Don't buy fewer shares than can beat the comission before the next stock market cycle (after covering the spread), lest the position reverse before we break-even.
226 let ticksBeforeCycleEnd = marketCycleLength - estTick - stk.timeToCoverTheSpread();
227 if (ticksBeforeCycleEnd < 1) continue; // We're cutting it too close to the market cycle, position might reverse before we break-even on commission
228 let estEndOfCycleValue = numShares * purchasePrice * ((stk.absReturn() + 1) ** ticksBeforeCycleEnd - 1); // Expected difference in purchase price and value at next market cycle end
229 let owned = stk.ownedShares() > 0;
230 if (estEndOfCycleValue <= 2 * commission)
231 log(ns, (owned ? '' : `We currently have ${formatNumberShort(stk.ownedShares(), 3, 1)} shares in ${stk.sym} valued at ${formatMoney(stk.positionValue())} ` +
232 `(${(100 * stk.positionValue() / maxHoldings).toFixed(1)}% of corpus, capped at ${(diversification * 100).toFixed(1)}% by --diversification).\n`) +
233 `Despite attractive ER of ${formatBP(stk.absReturn())}, ${owned ? 'more ' : ''}${stk.sym} was not bought. ` +
234 `\nBudget: ${formatMoney(budget)} can only buy ${numShares.toLocaleString('en')} ${owned ? 'more ' : ''}shares @ ${formatMoney(purchasePrice)}. ` +
235 `\nGiven an estimated ${marketCycleLength - estTick} ticks left in market cycle, less ${stk.timeToCoverTheSpread().toFixed(1)} ticks to cover the spread (${(stk.spread_pct * 100).toFixed(2)}%), ` +
236 `remaining ${ticksBeforeCycleEnd.toFixed(1)} ticks would only generate ${formatMoney(estEndOfCycleValue)}, which is less than 2x commission (${formatMoney(2 * commission, 3)})`);
237 else
238 cash -= await doBuy(ns, stk, numShares);
239 }
240 }
241 } catch (err) {
242 log(ns, `WARNING: stockmaster.js Caught (and suppressed) an unexpected error in the main loop:\n` +
243 (typeof err === 'string' ? err : err.message || JSON.stringify(err)), false, 'warning');
244 }
245 await ns.sleep(sleepInterval);
246 }
247}
248
249/** Ram-dodge getting updated player info. Note that this is the only async routine called in the main loop.
250 * If latency or ram instability is an issue, you may wish to try uncommenting the direct request.
251 * @param {NS} ns
252 * @returns {Promise<Player>} */
253async function getPlayerInfo(ns) {
254 return await getNsDataThroughFile(ns, `ns.getPlayer()`);
255}
256
257function getTimeInBitnode() { return Date.now() - resetInfo.lastNodeReset; }
258
259/* A sorting function to put stocks in the order we should prioritize investing in them */
260let purchaseOrder = (a, b) => (Math.ceil(a.timeToCoverTheSpread()) - Math.ceil(b.timeToCoverTheSpread())) || (b.absReturn() - a.absReturn());
261
262/** @param {NS} ns
263 * Generic helper for dodging the hefty RAM requirements of stock functions by spawning a temporary script to collect info for us. */
264async function getStockInfoDict(ns, stockFunction) {
265 allStockSymbols ??= await getStockSymbols(ns);
266 if (allStockSymbols == null) throw new Error(`No WSE API Access yet, this call to ns.stock.${stockFunction} is premature.`);
267 return await getNsDataThroughFile(ns,
268 `Object.fromEntries(ns.args.map(sym => [sym, ns.stock.${stockFunction}(sym)]))`,
269 `/Temp/stock-${stockFunction}.txt`, allStockSymbols);
270};
271
272/** @param {NS} ns **/
273async function initAllStocks(ns) {
274 let dictMaxShares = await getStockInfoDict(ns, 'getMaxShares'); // Only need to get this once, it never changes
275 return allStockSymbols.map(s => ({
276 sym: s,
277 maxShares: dictMaxShares[s], // Value never changes once retrieved
278 expectedReturn: function () { // How much holdings are expected to appreciate (or depreciate) in the future
279 // To add conservatism to pre-4s estimates, we reduce the probability by 1 standard deviation without crossing the midpoint
280 let normalizedProb = (this.prob - 0.5);
281 let conservativeProb = normalizedProb < 0 ? Math.min(0, normalizedProb + this.probStdDev) : Math.max(0, normalizedProb - this.probStdDev);
282 return this.vol * conservativeProb;
283 },
284 absReturn: function () { return Math.abs(this.expectedReturn()); }, // Appropriate to use when can just as well buy a short position as a long position
285 bullish: function () { return this.prob > 0.5 },
286 bearish: function () { return !this.bullish(); },
287 ownedShares: function () { return this.sharesLong + this.sharesShort; },
288 owned: function () { return this.ownedShares() > 0; },
289 positionValueLong: function () { return this.sharesLong * this.bid_price; },
290 positionValueShort: function () { return this.sharesShort * (2 * this.boughtPriceShort - this.ask_price); }, // Shorts work a bit weird
291 positionValue: function () { return this.positionValueLong() + this.positionValueShort(); },
292 // How many stock market ticks must occur at the current expected return before we regain the value lost by the spread between buy and sell prices.
293 // This can be derived by taking the compound interest formula (future = current * (1 + expected_return) ^ n) and solving for n
294 timeToCoverTheSpread: function () { return Math.log(this.ask_price / this.bid_price) / Math.log(1 + this.absReturn()); },
295 // We should not buy this stock within this many ticks of a Market cycle, or we risk being forced to sell due to a probability inversion, and losing money due to the spread
296 blackoutWindow: function () { return Math.ceil(this.timeToCoverTheSpread()); },
297 // Pre-4s properties used for forecasting
298 priceHistory: [],
299 lastInversion: 0,
300 }));
301}
302
303/** @param {NS} ns **/
304async function refresh(ns, has4s, allStocks, myStocks) {
305 let holdings = 0;
306
307 // Dodge hefty RAM requirements by spawning a sequence of temporary scripts to collect info for us one function at a time
308 const dictAskPrices = await getStockInfoDict(ns, 'getAskPrice');
309 const dictBidPrices = await getStockInfoDict(ns, 'getBidPrice');
310 const dictVolatilities = !has4s ? null : await getStockInfoDict(ns, 'getVolatility');
311 const dictForecasts = !has4s ? null : await getStockInfoDict(ns, 'getForecast');
312 const dictPositions = mock ? null : await getStockInfoDict(ns, 'getPosition');
313 const ticked = allStocks.some(stk => stk.ask_price != dictAskPrices[stk.sym]); // If any price has changed since our last update, the stock market has "ticked"
314
315 if (ticked) {
316 if (Date.now() - lastTick < expectedTickTime - sleepInterval) {
317 if (Date.now() - lastTick < catchUpTickTime - sleepInterval) {
318 let changedPrices = allStocks.filter(stk => stk.ask_price != dictAskPrices[stk.sym]);
319 log(ns, `WARNING: Detected a stock market tick after only ${formatDuration(Date.now() - lastTick)}, but expected ~${formatDuration(expectedTickTime)}. ` +
320 (changedPrices.length >= 33 ? '(All stocks updated)' : `The following ${changedPrices.length} stock prices changed: ${changedPrices.map(stk =>
321 `${stk.sym} ${formatMoney(stk.ask_price)} -> ${formatMoney(dictAskPrices[stk.sym])}`).join(", ")}`), false, 'warning');
322 } else
323 log(ns, `INFO: Detected a rapid stock market tick (${formatDuration(Date.now() - lastTick)}), likely to make up for lag / offline time.`)
324 }
325 lastTick = Date.now()
326 }
327
328 myStocks.length = 0;
329 for (const stk of allStocks) {
330 const sym = stk.sym;
331 stk.ask_price = dictAskPrices[sym]; // The amount we would pay if we bought the stock (higher than 'price')
332 stk.bid_price = dictBidPrices[sym]; // The amount we would recieve if we sold the stock (lower than 'price')
333 stk.spread = stk.ask_price - stk.bid_price;
334 stk.spread_pct = stk.spread / stk.ask_price; // The percentage of value we lose just by buying the stock
335 stk.price = (stk.ask_price + stk.bid_price) / 2; // = ns.stock.getPrice(sym);
336 stk.vol = has4s ? dictVolatilities[sym] : stk.vol;
337 stk.prob = has4s ? dictForecasts[sym] : stk.prob;
338 stk.probStdDev = has4s ? 0 : stk.probStdDev; // Standard deviation around the est. probability
339 // Update our current portfolio of owned stock
340 let [priorLong, priorShort] = [stk.sharesLong, stk.sharesShort];
341 stk.position = mock ? null : dictPositions[sym];
342 stk.sharesLong = mock ? (stk.sharesLong || 0) : stk.position[0];
343 stk.boughtPrice = mock ? (stk.boughtPrice || 0) : stk.position[1];
344 stk.sharesShort = mock ? (stk.shares_short || 0) : stk.position[2];
345 stk.boughtPriceShort = mock ? (stk.boughtPrice_short || 0) : stk.position[3];
346 holdings += stk.positionValue();
347 if (stk.owned()) myStocks.push(stk); else stk.ticksHeld = 0;
348 if (ticked) // Increment ticksHeld, or reset it if we have no position in this stock or reversed our position last tick.
349 stk.ticksHeld = !stk.owned() || (priorLong > 0 && stk.sharesLong == 0) || (priorShort > 0 && stk.sharesShort == 0) ? 0 : 1 + (stk.ticksHeld || 0);
350 }
351 if (ticked) await updateForecast(ns, allStocks, has4s); // Logic below only required on market tick
352 return holdings;
353}
354
355// Historical probability can be inferred from the number of times the stock was recently observed increasing over the total number of observations
356const forecast = history => history.reduce((ups, price, idx) => idx == 0 ? 0 : (history[idx - 1] > price ? ups + 1 : ups), 0) / (history.length - 1);
357// An "inversion" can be detected if two probabilities are far enough apart and are within "tolerance" of p1 being equal to 1-p2
358const tol2 = inversionDetectionTolerance / 2;
359const detectInversion = (p1, p2) => ((p1 >= 0.5 + tol2) && (p2 <= 0.5 - tol2) && p2 <= (1 - p1) + inversionDetectionTolerance)
360 /* Reverse Condition: */ || ((p1 <= 0.5 - tol2) && (p2 >= 0.5 + tol2) && p2 >= (1 - p1) - inversionDetectionTolerance);
361
362/** @param {NS} ns **/
363async function updateForecast(ns, allStocks, has4s) {
364 const currentHistory = allStocks[0].priceHistory.length;
365 const prepSummary = showMarketSummary || mock || (!has4s && (currentHistory < minTickHistory || allStocks.filter(stk => stk.owned()).length == 0)); // Decide whether to display the market summary table.
366 const inversionsDetected = []; // Keep track of individual stocks whose probability has inverted (45% chance of happening each "cycle")
367 detectedCycleTick = (detectedCycleTick + 1) % marketCycleLength; // Keep track of stock market cycle (which occurs every 75 ticks)
368 for (const stk of allStocks) {
369 stk.priceHistory.unshift(stk.price);
370 if (stk.priceHistory.length > maxTickHistory) // Limit the rolling window size
371 stk.priceHistory.splice(maxTickHistory, 1);
372 // Volatility is easy - the largest observed % movement in a single tick
373 if (!has4s) stk.vol = stk.priceHistory.reduce((max, price, idx) => Math.max(max, idx == 0 ? 0 : Math.abs(stk.priceHistory[idx - 1] - price) / price), 0);
374 // We want stocks that have the best expected return, averaged over a long window for greater precision, but the game will occasionally invert probabilities
375 // (45% chance every 75 updates), so we also compute a near-term forecast window to allow for early-detection of inversions so we can ditch our position.
376 stk.nearTermForecast = forecast(stk.priceHistory.slice(0, nearTermForecastWindowLength));
377 let preNearTermWindowProb = forecast(stk.priceHistory.slice(nearTermForecastWindowLength, nearTermForecastWindowLength + marketCycleLength)); // Used to detect the probability before the potential inversion event.
378 // Detect whether it appears as though the probability of this stock has recently undergone an inversion (i.e. prob => 1 - prob)
379 stk.possibleInversionDetected = has4s ? detectInversion(stk.prob, stk.lastTickProbability || stk.prob) : detectInversion(preNearTermWindowProb, stk.nearTermForecast);
380 stk.lastTickProbability = stk.prob;
381 if (stk.possibleInversionDetected) inversionsDetected.push(stk);
382 }
383 // Detect whether our auto-detected "stock market cycle" timing should be adjusted based on the number of potential inversions observed
384 let summary = "";
385 if (inversionsDetected.length > 0) {
386 summary += `${inversionsDetected.length} Stocks appear to be reversing their outlook: ${inversionsDetected.map(s => s.sym).join(', ')} (threshold: ${inversionAgreementThreshold})\n`;
387 if (inversionsDetected.length >= inversionAgreementThreshold && (has4s || currentHistory >= minTickHistory)) { // We believe we have detected the stock market cycle!
388 const newPredictedCycleTick = has4s ? 0 : nearTermForecastWindowLength; // By the time we've detected it, we're this many ticks past the cycle start
389 if (detectedCycleTick != newPredictedCycleTick)
390 log(ns, `Threshold for changing predicted market cycle met (${inversionsDetected.length} >= ${inversionAgreementThreshold}). ` +
391 `Changing current market tick from ${detectedCycleTick} to ${newPredictedCycleTick}.`);
392 marketCycleDetected = true;
393 detectedCycleTick = newPredictedCycleTick;
394 // Don't adjust this in the future unless we see another day with as much or even more agreement (capped at 14, it seems sometimes our cycles get out of sync with
395 // actual cycles and we need to reset our clock even after previously determining the cycle with great certainty.)
396 inversionAgreementThreshold = Math.max(14, inversionsDetected.length);
397 }
398 }
399 // Act on any inversions (if trusted), compute the probability, and prepare the stock summary
400 for (const stk of allStocks) {
401 // Don't "trust" (act on) a detected inversion unless it's near the time when we're capable of detecting market cycle start. Avoids most false-positives.
402 if (stk.possibleInversionDetected && (has4s && detectedCycleTick == 0 ||
403 (!has4s && (detectedCycleTick >= nearTermForecastWindowLength / 2) && (detectedCycleTick <= nearTermForecastWindowLength + inversionLagTolerance))))
404 stk.lastInversion = detectedCycleTick; // If we "trust" a probability inversion has occurred, probability will be calculated based on only history since the last inversion.
405 else
406 stk.lastInversion++;
407 // Only take the stock history since after the last inversion to compute the probability of the stock.
408 const probWindowLength = Math.min(longTermForecastWindowLength, stk.lastInversion);
409 stk.longTermForecast = forecast(stk.priceHistory.slice(0, probWindowLength));
410 if (!has4s) {
411 stk.prob = stk.longTermForecast;
412 stk.probStdDev = Math.sqrt((stk.prob * (1 - stk.prob)) / probWindowLength);
413 }
414 const signalStrength = 1 + (stk.bullish() ? (stk.nearTermForecast > stk.prob ? 1 : 0) + (stk.prob > 0.8 ? 1 : 0) : (stk.nearTermForecast < stk.prob ? 1 : 0) + (stk.prob < 0.2 ? 1 : 0));
415 if (prepSummary) { // Example: AERO ++ Prob: 54% (t51: 54%, t10: 67%) tLast⇄:190 Vol:0.640% ER: 2.778BP Spread:1.784% ttProfit: 65 Pos: 14.7M long (held 189 ticks)
416 stk.debugLog = `${stk.sym.padEnd(5, ' ')} ${(stk.bullish() ? '+' : '-').repeat(signalStrength).padEnd(3)} ` +
417 `Prob:${(stk.prob * 100).toFixed(0).padStart(3)}% (t${probWindowLength.toFixed(0).padStart(2)}:${(stk.longTermForecast * 100).toFixed(0).padStart(3)}%, ` +
418 `t${Math.min(stk.priceHistory.length, nearTermForecastWindowLength).toFixed(0).padStart(2)}:${(stk.nearTermForecast * 100).toFixed(0).padStart(3)}%) ` +
419 `tLast⇄:${(stk.lastInversion + 1).toFixed(0).padStart(3)} Vol:${(stk.vol * 100).toFixed(2)}% ER:${formatBP(stk.expectedReturn()).padStart(8)} ` +
420 `Spread:${(stk.spread_pct * 100).toFixed(2)}% ttProfit:${stk.blackoutWindow().toFixed(0).padStart(3)}`;
421 if (stk.owned()) stk.debugLog += ` Pos: ${formatNumberShort(stk.ownedShares(), 3, 1)} (${stk.ownedShares() == stk.maxShares ? 'max' :
422 ((100 * stk.ownedShares() / stk.maxShares).toFixed(0).padStart(2) + '%')}) ${stk.sharesLong > 0 ? 'long ' : 'short'} (held ${stk.ticksHeld} ticks)`;
423 if (stk.possibleInversionDetected) stk.debugLog += ' ⇄⇄⇄';
424 }
425 }
426 // Print a summary of stocks as of this most recent tick (if enabled)
427 if (prepSummary) {
428 summary += `Market day ${detectedCycleTick + 1}${marketCycleDetected ? '' : '?'} of ${marketCycleLength} (${marketCycleDetected ? (100 * inversionAgreementThreshold / 19).toPrecision(2) : '0'}% certain) ` +
429 `Current Stock Summary and Pre-4S Forecasts (by best payoff-time):\n` + allStocks.sort(purchaseOrder).map(s => s.debugLog).join("\n")
430 if (showMarketSummary) await updateForecastFile(ns, summary); else log(ns, summary);
431 }
432 // Write out a file of stock probabilities so that other scripts can make use of this (e.g. hack orchestrator can manipulate the stock market)
433 await ns.write('/Temp/stock-probabilities.txt', JSON.stringify(Object.fromEntries(
434 allStocks.map(stk => [stk.sym, { prob: stk.prob, sharesLong: stk.sharesLong, sharesShort: stk.sharesShort }]))), "w");
435}
436
437// Helpers to display the stock market summary in a separate window.
438let summaryFile = '/Temp/stockmarket-summary.txt';
439let updateForecastFile = async (ns, summary) => await ns.write(summaryFile, summary, 'w');
440let launchSummaryTail = async ns => {
441 let summaryTailScript = summaryFile.replace('.txt', '-tail.js');
442 if (await getNsDataThroughFile(ns, `ns.scriptRunning('${summaryTailScript}', ns.getHostname())`, '/Temp/stockmarket-summary-is-running.txt'))
443 return;
444 //await getNsDataThroughFile(ns, `ns.scriptKill('${summaryTailScript}', ns.getHostname())`, summaryTailScript.replace('.js', '-kill.js')); // Only needed if we're changing the script below
445 await runCommand(ns, `ns.disableLog('sleep'); ns.tail(); let lastRead = '';
446 while (true) {
447 let read = ns.read('${summaryFile}');
448 if (lastRead != read) ns.print(lastRead = read);
449 await ns.sleep(1000);
450 }`, summaryTailScript);
451}
452
453// Ram-dodging helpers that spawn temporary scripts to buy/sell rather than pay 2.5GB ram per variant
454let buyStockWrapper = async (ns, sym, numShares) => await transactStock(ns, sym, numShares, 'buyStock'); // ns.stock.buyStock(sym, numShares);
455let buyShortWrapper = async (ns, sym, numShares) => await transactStock(ns, sym, numShares, 'buyShort'); // ns.stock.buyShort(sym, numShares);
456let sellStockWrapper = async (ns, sym, numShares) => await transactStock(ns, sym, numShares, 'sellStock'); // ns.stock.sellStock(sym, numShares);
457let sellShortWrapper = async (ns, sym, numShares) => await transactStock(ns, sym, numShares, 'sellShort'); // ns.stock.sellShort(sym, numShares);
458let transactStock = async (ns, sym, numShares, action) =>
459 await getNsDataThroughFile(ns, `ns.stock.${action}(ns.args[0], ns.args[1])`, null, [sym, numShares]);
460
461/** @param {NS} ns
462 * Automatically buys either a short or long position depending on the outlook of the stock. */
463async function doBuy(ns, stk, sharesToBuy) {
464 // We include -2*commission in the "holdings value" of our stock, but if we make repeated purchases of the same stock, we have to track
465 // the additional commission somewhere. So only subtract it from our running profit if this isn't our first purchase of this symbol
466 let price = 0; //price wasn't defined yet.
467 if (stk.owned())
468 totalProfit -= commission;
469 let long = stk.bullish();
470 let expectedPrice = long ? stk.ask_price : stk.bid_price; // Depends on whether we will be buying a long or short position
471 log(ns, `INFO: ${long ? 'Buying ' : 'Shorting'} ${formatNumberShort(sharesToBuy, 3, 3).padStart(5)} (` +
472 `${stk.maxShares == sharesToBuy + stk.ownedShares() ? '@max shares' : `${formatNumberShort(sharesToBuy + stk.ownedShares(), 3, 3).padStart(5)}/${formatNumberShort(stk.maxShares, 3, 3).padStart(5)}`}) ` +
473 `${stk.sym.padEnd(5)} @ ${formatMoney(expectedPrice).padStart(9)} for ${formatMoney(sharesToBuy * expectedPrice).padStart(9)} (Spread:${(stk.spread_pct * 100).toFixed(2)}% ` +
474 `ER:${formatBP(stk.expectedReturn()).padStart(8)}) Ticks to Profit: ${stk.timeToCoverTheSpread().toFixed(2)}`, noisy, toastStyle);
475 try {
476 price = mock ? expectedPrice : Number(await transactStock(ns, stk.sym, sharesToBuy, long ? 'buyStock' : 'buyShort'));
477 } catch (err) {
478 if (long) throw err;
479 disableShorts = true;
480 log(ns, `WARN: Failed to short ${stk.sym} (Shorts not available?). Disabling shorts...`, true, 'warning');
481 return 0;
482 }
483 // The rest of this work is for troubleshooting / mock-mode purposes
484 if (price == 0) {
485 const playerMoney = (await getPlayerInfo(ns)).money;
486 if (playerMoney < sharesToBuy * expectedPrice)
487 log(ns, `WARN: Failed to ${long ? 'buy' : 'short'} ${stk.sym} because money just recently dropped to ${formatMoney(playerMoney)} and we can no longer afford it.`, noisy);
488 else
489 log(ns, `ERROR: Failed to ${long ? 'buy' : 'short'} ${stk.sym} @ ${formatMoney(expectedPrice)} (0 was returned) despite having ${formatMoney(playerMoney)}.`, true, 'error');
490 return 0;
491 } else if (price != expectedPrice) {
492 log(ns, `WARNING: ${long ? 'Bought' : 'Shorted'} ${stk.sym} @ ${formatMoney(price)} but expected ${formatMoney(expectedPrice)} (spread: ${formatMoney(stk.spread)})`, false, 'warning');
493 price = expectedPrice; // Known Bitburner bug for now, short returns "price" instead of "bid_price". Correct this so running profit calcs are correct.
494 }
495 if (mock && long) stk.boughtPrice = (stk.boughtPrice * stk.sharesLong + price * sharesToBuy) / (stk.sharesLong + sharesToBuy);
496 if (mock && !long) stk.boughtPriceShort = (stk.boughtPriceShort * stk.sharesShort + price * sharesToBuy) / (stk.sharesShort + sharesToBuy);
497 if (long) stk.sharesLong += sharesToBuy; else stk.sharesShort += sharesToBuy; // Maintained for mock mode, otherwise, redundant (overwritten at next refresh)
498 return sharesToBuy * price + commission; // Return the amount spent on the transaction so it can be subtracted from our cash on hand
499}
500
501/** @param {NS} ns
502 * Sell our current position in this stock. */
503async function doSellAll(ns, stk) {
504 let long = stk.sharesLong > 0;
505 if (long && stk.sharesShort > 0) // Detect any issues here - we should always sell one before buying the other.
506 log(ns, `ERROR: Somehow ended up both ${stk.sharesShort} short and ${stk.sharesLong} long on ${stk.sym}`, true, 'error');
507 let expectedPrice = long ? stk.bid_price : stk.ask_price; // Depends on whether we will be selling a long or short position
508 let sharesSold = long ? stk.sharesLong : stk.sharesShort;
509 let price = mock ? expectedPrice : await transactStock(ns, stk.sym, sharesSold, long ? 'sellStock' : 'sellShort');
510 const profit = (long ? stk.sharesLong * (price - stk.boughtPrice) : stk.sharesShort * (stk.boughtPriceShort - price)) - 2 * commission;
511 log(ns, `${profit > 0 ? 'SUCCESS' : 'WARNING'}: Sold all ${formatNumberShort(sharesSold, 3, 3).padStart(5)} ${stk.sym.padEnd(5)} ${long ? ' long' : 'short'} positions ` +
512 `@ ${formatMoney(price).padStart(9)} for a ` + (profit > 0 ? `PROFIT of ${formatMoney(profit).padStart(9)}` : ` LOSS of ${formatMoney(-profit).padStart(9)}`) + ` after ${stk.ticksHeld} ticks`,
513 noisy, noisy ? (profit > 0 ? 'success' : 'error') : undefined);
514 if (price == 0) {
515 log(ns, `ERROR: Failed to sell ${sharesSold} ${stk.sym} ${long ? 'shares' : 'shorts'} @ ${formatMoney(expectedPrice)} - 0 was returned.`, true, 'error');
516 return 0;
517 } else if (price != expectedPrice) {
518 log(ns, `WARNING: Sold ${stk.sym} ${long ? 'shares' : 'shorts'} @ ${formatMoney(price)} but expected ${formatMoney(expectedPrice)} (spread: ${formatMoney(stk.spread)})`, false, 'warning');
519 price = expectedPrice; // Known Bitburner bug for now, sellSort returns "price" instead of "ask_price". Correct this so running profit calcs are correct.
520 }
521 if (long) stk.sharesLong -= sharesSold; else stk.sharesShort -= sharesSold; // Maintained for mock mode, otherwise, redundant (overwritten at next refresh)
522 totalProfit += profit;
523 return price * sharesSold - commission; // Return the amount of money recieved from the transaction
524}
525
526let formatBP = fraction => formatNumberShort(fraction * 100 * 100, 3, 2) + " BP";
527
528/** Log / tprint / toast helper.
529 * @param {NS} ns */
530let log = (ns, message, tprint = false, toastStyle = "") => {
531 if (message == lastLog) return;
532 ns.print(message);
533 if (tprint) ns.tprint(message);
534 if (toastStyle) ns.toast(message, toastStyle);
535 return lastLog = message;
536}
537
538function doStatusUpdate(ns, stocks, myStocks, hudElement = null) {
539 let maxReturnBP = 10000 * Math.max(...myStocks.map(s => s.absReturn())); // The largest return (in basis points) in our portfolio
540 let minReturnBP = 10000 * Math.min(...myStocks.map(s => s.absReturn())); // The smallest return (in basis points) in our portfolio
541 let est_holdings_cost = myStocks.reduce((sum, stk) => sum + (stk.owned() ? commission : 0) +
542 stk.sharesLong * stk.boughtPrice + stk.sharesShort * stk.boughtPriceShort, 0);
543 let liquidation_value = myStocks.reduce((sum, stk) => sum - (stk.owned() ? commission : 0) + stk.positionValue(), 0);
544 let status = `Long ${myStocks.filter(s => s.sharesLong > 0).length}, Short ${myStocks.filter(s => s.sharesShort > 0).length} of ${stocks.length} stocks ` +
545 (myStocks.length == 0 ? '' : `(ER ${minReturnBP.toFixed(1)}-${maxReturnBP.toFixed(1)} BP) `) +
546 `Profit: ${formatMoney(totalProfit, 3)} Holdings: ${formatMoney(liquidation_value, 3)} (Cost: ${formatMoney(est_holdings_cost, 3)}) ` +
547 `Net: ${formatMoney(totalProfit + liquidation_value - est_holdings_cost, 3)}`
548 log(ns, status);
549 if (hudElement) hudElement.innerText = formatMoney(liquidation_value, 6, 3);
550}
551
552/** @param {NS} ns **/
553async function liquidate(ns) {
554 allStockSymbols ??= await getStockSymbols(ns);
555 if (allStockSymbols == null) return; // Nothing to liquidate, no API Access
556 let totalStocks = 0, totalSharesLong = 0, totalSharesShort = 0, totalRevenue = 0;
557 const dictPositions = mock ? null : await getStockInfoDict(ns, 'getPosition');
558 for (const sym of allStockSymbols) {
559 var [sharesLong, , sharesShort, avgShortCost] = dictPositions[sym];
560 if (sharesLong + sharesShort == 0) continue;
561 totalStocks++, totalSharesLong += sharesLong, totalSharesShort += sharesShort;
562 if (sharesLong > 0) totalRevenue += (await sellStockWrapper(ns, sym, sharesLong)) * sharesLong - commission;
563 if (sharesShort > 0) totalRevenue += (2 * avgShortCost - (await sellShortWrapper(ns, sym, sharesShort))) * sharesShort - commission;
564 }
565 log(ns, `Sold ${totalSharesLong.toLocaleString('en')} long shares and ${totalSharesShort.toLocaleString('en')} short shares ` +
566 `in ${totalStocks} stocks for ${formatMoney(totalRevenue, 3)}`, true, 'success');
567}
568
569/** @param {NS} ns **/
570/** @param {Player} playerStats **/
571async function tryGet4SApi(ns, playerStats, bitnodeMults, budget) {
572 if (await checkAccess(ns, 'has4SDataTIXAPI')) return false; // Only return true if we just bought it
573 const cost4sData = 1E9 * bitnodeMults.FourSigmaMarketDataCost;
574 const cost4sApi = 25E9 * bitnodeMults.FourSigmaMarketDataApiCost;
575 const has4S = await checkAccess(ns, 'has4SData');
576 const totalCost = (has4S ? 0 : cost4sData) + cost4sApi;
577 // Liquidate shares if it would allow us to afford 4S API data
578 if (totalCost > budget) /* Need to reserve some money to invest */
579 return false;
580 if (playerStats.money < totalCost)
581 await liquidate(ns);
582 if (!has4S) {
583 if (await tryBuy(ns, 'purchase4SMarketData'))
584 log(ns, `SUCCESS: Purchased 4SMarketData for ${formatMoney(cost4sData)} ` +
585 `(At ${formatDuration(getTimeInBitnode())} into BitNode)`, true, 'success');
586 else
587 log(ns, 'ERROR attempting to purchase 4SMarketData!', false, 'error');
588 }
589 if (await tryBuy(ns, 'purchase4SMarketDataTixApi')) {
590 log(ns, `SUCCESS: Purchased 4SMarketDataTixApi for ${formatMoney(cost4sApi)} ` +
591 `(At ${formatDuration(getTimeInBitnode())} into BitNode)`, true, 'success');
592 return true;
593 } else {
594 log(ns, 'ERROR attempting to purchase 4SMarketDataTixApi!', false, 'error');
595 if (!(5 in dictSourceFiles)) { // If we do not have access to bitnode multipliers, assume the cost is double and try again later
596 log(ns, 'INFO: Bitnode mults are not available (SF5) - assuming everything is twice as expensive in the current bitnode.');
597 bitnodeMults.FourSigmaMarketDataCost *= 2;
598 bitnodeMults.FourSigmaMarketDataApiCost *= 2;
599 }
600 }
601 return false;
602}
603
604/** @param {NS} ns
605 * @param {"hasWSEAccount"|"hasTIXAPIAccess"|"has4SData"|"has4SDataTIXAPI"} stockFn
606 * Helper to check for one of the stock access functions */
607async function checkAccess(ns, stockFn) {
608 return await getNsDataThroughFile(ns, `ns.stock.${stockFn}()`)
609}
610
611/** @param {NS} ns
612 * @param {"purchaseWseAccount"|"purchaseTixApi"|"purchase4SMarketData"|"purchase4SMarketDataTixApi"} stockFn
613 * Helper to try and buy a stock access. Yes, the code is the same as above, but I wanted to be explicit. */
614async function tryBuy(ns, stockFn) {
615 return await getNsDataThroughFile(ns, `ns.stock.${stockFn}()`)
616}
617
618/** @param {NS} ns
619 * @param {number} budget - The amount we are willing to spend on WSE and API access
620 * Tries to purchase access to the stock market **/
621async function tryGetStockMarketAccess(ns, budget) {
622 if (await checkAccess(ns, 'hasTIXAPIAccess')) return true; // Already have access
623 const costWseAccount = 200E6;
624 const costTixApi = 5E9;
625 const hasWSE = await checkAccess(ns, 'hasWSEAccount');
626 const totalCost = (hasWSE ? 0 : costWseAccount) + costTixApi;
627 if (totalCost > budget) return false;
628 if (!hasWSE) {
629 if (await tryBuy(ns, 'purchaseWseAccount'))
630 log(ns, `SUCCESS: Purchased a WSE (stockmarket) account for ${formatMoney(costWseAccount)} ` +
631 `(At ${formatDuration(getTimeInBitnode())} into BitNode)`, true, 'success');
632 else
633 log(ns, 'ERROR attempting to purchase WSE account!', false, 'error');
634 }
635 if (await tryBuy(ns, 'purchaseTixApi')) {
636 log(ns, `SUCCESS: Purchased Tix (stockmarket) Api access for ${formatMoney(costTixApi)} ` +
637 `(At ${formatDuration(getTimeInBitnode())} into BitNode)`, true, 'success');
638 return true;
639 } else
640 log(ns, 'ERROR attempting to purchase Tix Api!', false, 'error');
641 return false;
642}
643
644function initializeHud() {
645 const d = eval("document");
646 let htmlDisplay = d.getElementById("stock-display-1");
647 if (htmlDisplay !== null) return htmlDisplay;
648 // Get the custom display elements in HUD.
649 let customElements = d.getElementById("overview-extra-hook-0").parentElement.parentElement;
650 // Make a clone of the hook for extra hud elements, and move it up under money
651 let stockValueTracker = customElements.cloneNode(true);
652 // Remove any nested elements created by stats.js
653 stockValueTracker.querySelectorAll("p > p").forEach(el => el.parentElement.removeChild(el));
654 // Change ids since duplicate id's are invalid
655 stockValueTracker.querySelectorAll("p").forEach((el, i) => el.id = "stock-display-" + i);
656 // Get out output element
657 htmlDisplay = stockValueTracker.querySelector("#stock-display-1");
658 // Display label and default value
659 stockValueTracker.querySelectorAll("p")[0].innerText = "Stock";
660 htmlDisplay.innerText = "$0.000 "
661 // Insert our element right after Money
662 customElements.parentElement.insertBefore(stockValueTracker, customElements.parentElement.childNodes[2]);
663 return htmlDisplay;
664}
targets.js Raw
1/** @param {NS} ns */
2// is automatically populated by scan.js
3export var targets = [
4 "foodnstuff",
5 "n00dles",
6];