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