Bitburner

Bitburner

View Stats:
Ghost of Wall Street script (spoilers)
So I've written a script to trade stocks without utilizing the 4s api and thought I'd share it for people to learn from if interested. If you would like to try and do something like this completely on your own than do not look at the code. I put spoilers in the title as a warning about this. I provide this mainly for learning purposes but hey, I can't control if people just want to grab it and use it to take the easy way out, aside from not sharing it at all. They will spoil their own fun (or maybe feel the challenge isn't fun and just want the rewards). I'm mainly thinking of those that want to learn and improve and want to help with that, so enjoy if that is you.

simple-stock-trader.js:

let cash_min = 10 * 1000000; // Minimum cash to keep on hand
let commission = 100000; // Buy and sell commission
let cycle_time = 3000;
let max_hist = 12;

var stocks = [];
var cycles = 0;

function update(ns) {
let updated = false;
let wealth = 0;
let cost = 0;

for (const stock of stocks) {
const sym = stock.sym;
const position = ns.stock.getPosition(sym);

stock.owned = position[0]; // shares owned
stock.cost = position[1]; // average cost per share bought
stock.ask = ns.stock.getAskPrice(sym);
stock.bid = ns.stock.getBidPrice(sym);

if (stock.owned > 0) {
cost += stock.owned * stock.cost;
wealth += stock.owned * stock.bid;
}

// calculate trend based on history
const price = (stock.ask + stock.bid) / 2;
if (price !== stock.last_price) {
if (stock.hist.length == max_hist)
stock.trend -= stock.hist.shift();

let bit = price < stock.last_price ? 0 : 1 / max_hist;
if (price == stock.last_price)
bit = 0.5 / max_hist;

stock.hist.push(bit);
stock.trend += bit;
stock.last_price = price;
updated = true;
}
}

stocks.sort((a, b) => b.trend - a.trend);
if (updated)
cycles++;

return [cost, wealth];
}

export async function main(ns) {
function format(num) {
return (num < 0 ? "-$" : "$") + ns.nFormat(Math.abs(num), "0.000a");
}

function buy(stock, num_shares) {
let cost = ns.stock.buy(stock.sym, num_shares) * num_shares;
ns.print("Buy ", num_shares, " of ", stock.sym, " for ", format(cost));
balance -= cost + commission;
}

function sell(stock, num_shares) {
let proceeds = ns.stock.sell(stock.sym, num_shares) * num_shares;
ns.print("Sell ", num_shares, " of ", stock.sym, " for ", format(proceeds));
balance += proceeds - commission;
}

// Setup
let prev_wealth;
let balance = 0;
ns.disableLog("ALL");
ns.tail();

let syms = ns.stock.getSymbols();
for (const sym of syms)
stocks.push({
sym: sym,
max_shares: ns.stock.getMaxShares(sym),
hist: [],
last_price: (ns.stock.getAskPrice(sym) + ns.stock.getBidPrice(sym)) / 2,
trend: 0
});

// main loop
while (true) {
await ns.sleep(cycle_time);

let wealth = update(ns);
if (wealth !== prev_wealth) {
ns.print("Invested wealth = ", format(wealth[1]), " Profit: ", format(wealth[0] + balance));
prev_wealth = wealth;
}

if (cycles < max_hist)
continue;

// Sell downward trending stocks
for (let stock of stocks) {
if (stock.owned > 0 && stock.trend < 0.5) {
sell(stock, stock.owned);
}
}

let budget = ns.getServerMoneyAvailable("home") - cash_min;

// Buy shares of most promising stock
for (let stock of stocks) {
if (budget > commission * 2 && stock.trend > 0.75) {
let num_shares = Math.floor((budget - commission) / stock.ask);
let max_shares = stock.max_shares - stock.owned;
if (num_shares > max_shares)
num_shares = max_shares;

if (num_shares * stock.ask * 0.75 > commission * 2) {
buy(stock, num_shares);
budget = ns.getServerMoneyAvailable("home") - cash_min;
}
}
}
}
}
< >
Showing 1-11 of 11 comments
How's it working for you?

I'm thinking of taking this bitnode next, my plan is to develop a script like this on my current run with access to 4S so I can automatically compare my estimation to 4S and tweak parameters. Perhaps I should even record the stock market for a couple of hour or so to let me re-run simulations instantly. Have to be careful not to overfit to the recorded data though, but that should be fine if I have multiple recordings.

I'm hoping to be able to estimate volatility as well. It won't be 4 Sigma, but perhaps i can make it 2 Sigma...

Also, since you are not hacking for money, have you considered doing it to manipulate stock? Or auto-working for companies (has a beneficial impact on stock prices).
Oh, I'm going to make a stock market class with a methods for mapping symbols to prices etc, methods for buying/selling and a method for awaiting the next update. Then I can make an instance for the actual stock market that will wait, or an instance for pre-recorded stocks that will immediately fetch the next value from a file. Perhaps I can even generate random markets. Then I can run various trading scripts on those markets and calculate/compare gains.

The pretend buying/selling won't affect the forecast, but that should be a minor issue. This will be good :).
Originally posted by duregard:
How's it working for you?

I'm thinking of taking this bitnode next, my plan is to develop a script like this on my current run with access to 4S so I can automatically compare my estimation to 4S and tweak parameters. Perhaps I should even record the stock market for a couple of hour or so to let me re-run simulations instantly. Have to be careful not to overfit to the recorded data though, but that should be fine if I have multiple recordings.

I'm hoping to be able to estimate volatility as well. It won't be 4 Sigma, but perhaps i can make it 2 Sigma...

Also, since you are not hacking for money, have you considered doing it to manipulate stock? Or auto-working for companies (has a beneficial impact on stock prices).
It works well, though it takes time. What is interesting is that it doesn't always align with the 4s forecasts. I'll see it buy stock that has negatives, but that is what happens with RNG.

It has grown my starting $250m to $1b so I could buy the 4s data. From there I manually traded using that data until I got to $25b so I could get the 4s api. That is long and boring micro management. Now I can run my 4s stock trader to do even better.

I don't see volatility info being useful in any real way, personally. I just ignore it. I was thinking buying cheaper shares would allow me to buy more of them, leveraging. But it seems like price change magnitudes are based on percentage rather than some constant so share price doesn't seem to matter. You'll make the same gains either way. So I sort the stocks based on the best trend buy in that order. Practically speaking, this is really an early game script when you don't have much money, so you can only afford some of the stock available from a company. You won't be buying up all of one company have have extra to spend on the second best company, at least the vast majority of the time. Maybe a stock will drop really low.

This script only takes advantage of long trades. You could try and to shorts as well. I am working out the code to try and do both with my 4s stock trader.
Originally posted by tclord:
I don't see volatility info being useful in any real way, personally. I just ignore it.
(prediction-0.5)*volatilty gives you the expected gain/loss of a stock. Using that as a weight for choosing stocks to invest in will earn you money faster than just using prediction.
Originally posted by duregard:
Originally posted by tclord:
I don't see volatility info being useful in any real way, personally. I just ignore it.
(prediction-0.5)*volatilty gives you the expected gain/loss of a stock. Using that as a weight for choosing stocks to invest in will earn you money faster than just using prediction.
Well, in theory. It should be the statistical average. The fact that RNG factors in so heavily means that with my bad luck, it is irrelevant. RNG seems to have a particular bias against me. And if decides it wants to work against me, magnifying that bad luck with the volatility seems like a poor idea.
@tclord I have now created an awesome script for recording stock movements and then replaying scripts against them (by emulating the TIX api, so you only need to replace ns.hack by replace ns.sleep/ns.getServerMoneyAvailable("home") by calls to the mock-API.

I ran this on your script for a recording of 1400 updates (so around 2h20 min real time), and it went from $10b to $25b. Running the same recording on a reference script that uses 4Sigma gave $100b. I'd say 25 is pretty good, but there's some room for improvement.

I tried tweaking the history size, but the income jumped all over the place. Needs more data...
Originally posted by duregard:
@tclord I have now created an awesome script for recording stock movements and then replaying scripts against them (by emulating the TIX api, so you only need to replace ns.hack by replace ns.sleep/ns.getServerMoneyAvailable("home") by calls to the mock-API.

I ran this on your script for a recording of 1400 updates (so around 2h20 min real time), and it went from $10b to $25b. Running the same recording on a reference script that uses 4Sigma gave $100b. I'd say 25 is pretty good, but there's some room for improvement.

I tried tweaking the history size, but the income jumped all over the place. Needs more data...
So do you remove the delays then to accelerate the test results? If so, how quickly does it go through the 2h20m?

With fixed input data, you can play around with the formulas a bit and see the different before and after pretty easily. Once you get some good improvements you'd want to test it against a different run of input data to make sure you aren't exploiting something specific to one set of input data. Let me know if you find any good improvements to my algorithms or formulas.
Originally posted by tclord:
So do you remove the delays then to accelerate the test results? If so, how quickly does it go through the 2h20m?
Pretty much instantly. I've noticed the log output becomes inconsistent when you have large volumes of printing without delay, probably some kind of race condition in how the engine deals with logging (message order gets messed up). The end result seems to replicate reliably though.
Originally posted by tclord:
With fixed input data, you can play around with the formulas a bit and see the different before and after pretty easily. Once you get some good improvements you'd want to test it against a different run of input data to make sure you aren't exploiting something specific to one set of input data. Let me know if you find any good improvements to my algorithms or formulas.
Yeah, that would be the issue of overfitting that I mentioned earlier. I have a setup with multiple recordings stored in different files, and I can split large recordings up to get multiple more-or-less independent data points.
I thought I'd drop in my stock trading script as well:

Compared to the OP's code, mine has some differences:
  • I have an associated hacking script (not included) that hacks/grows to influence the stock market based on my current stock holdings. This is optional.
  • I save some state to a file on the home server. This is mainly to ensure I can kill and re-run the trading script without needing to gather data for a long period again.
  • There's a section in there where I sell all stocks to buy the APIs. You can comment it out to get the no-4S achievement (takes a long time though).
  • When determining the trend of a stock, I don't use the price of the stock, but whether the stock went up or down. This removes the "volatility" of the stock.
  • My script buys about 5 positions at once, roughly evenly splitting your total assets into the most promising stocks. But I only buy 1 position per loop (simplifies logic a little).
  • Also includes a chunk of 4S logic.

"/stocks/main.js"

/** @param {NS} ns **/
export async function main(ns) {
ns.disableLog("ALL");

// Unlock basic stock market functionality
while (true) {
if (ns.getPlayer().hasWseAccount && ns.getPlayer().hasTixApiAccess) {
break;
}

await ns.sleep(1000);
}

// Used to influence the market with targeted hacking/growing
var nodes = ["home"];
for (var i = 0; i < nodes.length; i++) {
// Find more servers and add the new ones
var next = ns.scan(nodes[i]);
for (var j in next) {
if (!nodes.includes(next[j])) {
nodes.push(next[j]);
}
}
}
async function UpdatePositions() {
// Update the list of positions on all servers
var symbols = ns.stock.getSymbols();
var positions = {
longs: [],
shorts: []
};
for (var i in symbols) {
if (ns.stock.getPosition(symbols[i])[0] > 0) {
positions.longs.push(symbols[i]);
}
if (ns.stock.getPosition(symbols[i])[2] > 0) {
positions.shorts.push(symbols[i]);
}
}
await ns.write("positions.txt", JSON.stringify(positions), "w");

for (var i = 1; i < nodes.length; i++) {
await ns.scp("positions.txt", nodes[i]);
}
}
await UpdatePositions();

// Stock market resets upon Augmentation, so data must be wiped
var history = JSON.parse(ns.read("/stocks/history.txt"));

// Two time windows to keep track of
var LONG_COUNT = 50;
var LONG_MA = 1 - (1 / LONG_COUNT);
var NEAR_MA = 1 - (2 / LONG_COUNT);

// Primitive buying/selling
// Keep track of historic prices and buy when trending up, sell otherwise
while (true) {
var symbols = ns.stock.getSymbols();

// Figure out how much money we have, plus stocks
var money = ns.getPlayer().money;
for (var i in symbols) {
var [longs, _, shorts] = ns.stock.getPosition(symbols[i]);
if (longs > 0) {
money += ns.stock.getSaleGain(symbols[i], longs, "Long");
}
if (shorts > 0) {
money += ns.stock.getSaleGain(symbols[i], shorts, "Short");
}
}

// Unlock the more efficient approach
if (ns.stock.purchase4SMarketData() && ns.stock.purchase4SMarketDataTixApi()) {
break;
} else {
function SellAll() {
for (var i in symbols) {
var [longs, _, shorts] = ns.stock.getPosition(symbols[i]);
if (longs > 0) {
ns.stock.sell(symbols[i], longs);
}
if (shorts > 0 && ns.stock.getSaleGain(symbols[i], shorts, "Short") > 0) {
ns.stock.sellShort(symbols[i], shorts);
}
}
}

if (!ns.stock.purchase4SMarketData()
&& money > 1e9 * ns.getBitNodeMultipliers().FourSigmaMarketDataCost) {
SellAll();
ns.stock.purchase4SMarketData();
continue;
}

if (!ns.stock.purchase4SMarketDataTixApi()
&& money > 25e9 * ns.getBitNodeMultipliers().FourSigmaMarketDataApiCost) {
SellAll();
ns.stock.purchase4SMarketDataTixApi();
continue;
}
}

for (var i in symbols) {
var price = ns.stock.getPrice(symbols[i]);

// Update history
if (history[symbols[i]]) {
// Keep track of the bounds
if (price > history[symbols[i]].high) {
history[symbols[i]].high = price;
}
if (price < history[symbols[i]].low) {
history[symbols[i]].low = price;
}

// Keep a moving average of price changes
// We're trying to approximate the 4S data
if (price != history[symbols[i]].prev) {
if (i == 0) {
history.updates++;
}

// The moving average is the probability of a positive price change
history[symbols[i]].long *= LONG_MA;
history[symbols[i]].near *= NEAR_MA;
if (price > history[symbols[i]].prev) {
history[symbols[i]].long += (1 - LONG_MA);
history[symbols[i]].near += (1 - NEAR_MA);
}
history[symbols[i]].prev = price;
}
} else {
history[symbols[i]] = {
high: price,
low: price,
prev: price,
long: 0.5,
near: 0.5,
};
}

// Primitive sell strategy
// 1) Check all current positions
// 2) Sell when the trend flips, but hold until profit
var [longs, longPrice, shorts, shortPrice] = ns.stock.getPosition(symbols[i]);
if (longs > 0) {
var net = ns.stock.getSaleGain(symbols[i], longs, "Long") - longs * longPrice;
if (history[symbols[i]].near < 0.5
|| net > 0 && (history[symbols[i]].near < 0.6 || history[symbols[i]].long < 0.55)) {
var sold = ns.stock.sell(symbols[i], longs);
ns.print("Sold " + symbols[i] + " for " + Math.round(sold) + " (" + Math.round(net / 1e6) + "m)");
await UpdatePositions();
}
}

if (shorts > 0) {
var net = ns.stock.getSaleGain(symbols[i], shorts, "Short") - shorts * shortPrice;
if (history[symbols[i]].near > 0.5
|| net > 0 && (history[symbols[i]].near > 0.35 || history[symbols[i]].long > 0.4)) {
var sold = ns.stock.sellShort(symbols[i], shorts);
ns.print("Unshorted " + symbols[i] + " for " + Math.round(sold) + " (" + Math.round(net / 1e6) + "m)");
await UpdatePositions();
}
}
}

// Save history
await ns.write("/stocks/history.txt", JSON.stringify(history, undefined, 2), "w");

// Buy based on total money + stocks, in 20% chunks, with at least 50m each time
money = Math.min(Math.max(5e7, money * 0.2), ns.getPlayer().money * 0.98)
if (!ns.scriptRunning("/stocks/sell.js", "home")
&& history.updates > LONG_COUNT
&& money >= 5e7) {
// Primitive buy strategy
// 1) Exclude symbols with an existing position
// 2) Include only symbols trending upward
// 3) Rank symbols by trend
// 4) Buy the most upward trending
var filtered = symbols
.filter((value) => ns.stock.getPosition(value)[0] <= 0 && ns.stock.getPosition(value)[2] <= 0);
var longing = filtered
.filter((value) => history[value].long > 0.525 && history[value].near > 0.675)
.sort((a, b) => history[b].near - history[a].near);
var shorting = filtered // Riskier, so thresholds are higher
.filter((value) => history[value].long < 0.425 && history[value].near < 0.275)
.sort((a, b) => history[a].near - history[b].near);

if (longing.length > 0) {
var cost = ns.stock.buy(
longing[0],
Math.round(Math.min(
money / ns.stock.getAskPrice(longing[0]),
ns.stock.getMaxShares(longing[0]))
));
ns.print("Bought " + longing[0] + " for " + Math.round(cost)
+ " [" + (Math.round(history[longing[0]].near * 1000) / 1000) + "]");
await UpdatePositions();
} else if (shorting.length > 0) {
var cost = ns.stock.short(
shorting[0],
Math.round(Math.min(
money / ns.stock.getBidPrice(shorting[0]),
ns.stock.getMaxShares(shorting[0]))
));
ns.print("Shorted " + shorting[0] + " for " + Math.round(cost)
+ " [" + (Math.round(history[shorting[0]].near * 1000) / 1000) + "]");
await UpdatePositions();
}
}

await ns.sleep(2000);
}

// Data-driven buying/selling
while (true) {
var symbols = ns.stock.getSymbols();

// Simple sell strategy
// 1) Check all current positions
// 2) Sell as soon as the price forcast is net negative
for (var i in symbols) {
var [longs, longPrice, shorts, shortPrice] = ns.stock.getPosition(symbols[i]);
if (longs > 0) {
if (ns.stock.getForecast(symbols[i]) < 0.5) {
var net = ns.stock.getSaleGain(symbols[i], longs, "Long") - longs * longPrice;
var sold = ns.stock.sell(symbols[i], longs);
ns.print("Sold " + symbols[i] + " for " + Math.round(sold) + " (" + Math.round(net / 1e6) + "m)");
await UpdatePositions();
}
}

if (shorts > 0) {
if (ns.stock.getForecast(symbols[i]) > 0.475) {
var net = ns.stock.getSaleGain(symbols[i], shorts, "Short") - shorts * shortPrice;
var sold = ns.stock.sellShort(symbols[i], shorts);
ns.print("Unshorted " + symbols[i] + " for " + Math.round(sold) + " (" + Math.round(net / 1e6) + "m)");
await UpdatePositions();
}
}
}

var money = ns.getPlayer().money * 0.98;
if (!ns.scriptRunning("/stocks/sell.js", "home") && money > 1e8) {
// Simple buy strategy
// 1) Buy in bulk (either as much as possible, or at least doubling existing holdings)
// 2) Include only symbols which are trending significantly up
// 3) Rank symbols by trend
// 4) Buy the top symbol
var filtered = symbols
.filter((value) => {
// No shares or all shares
var [longs, _, shorts] = ns.stock.getPosition(value);
var shares = longs + shorts;
if (shares <= 0) {
return true;
}
if (shares >= ns.stock.getMaxShares(value)) {
return false;
}

// All remaining or at least double
var affordable = money / ns.stock.getAskPrice(value);
if (affordable > ns.stock.getMaxShares(value) - shares || affordable > shares) {
return true;
}
return false;
});
var longing = filtered
.filter((value) => ns.stock.getForecast(value) > 0.575)
.sort((a, b) => ns.stock.getForecast(b) - ns.stock.getForecast(a));
var shorting = filtered // Riskier, so threshold are higher
.filter((value) => ns.stock.getForecast(value) < 0.4)
.sort((a, b) => ns.stock.getForecast(a) - ns.stock.getForecast(b));

if (longing.length > 0) {
var cost = ns.stock.buy(longing[0],
Math.round(Math.min(
money / ns.stock.getAskPrice(longing[0]),
ns.stock.getMaxShares(longing[0]) - ns.stock.getPosition(longing[0])[0])
));
ns.print("Bought " + longing[0] + " for " + Math.round(cost)
+ " (" + Math.round(ns.stock.getForecast(longing[0]) * 1000) / 1000 + ")");
await UpdatePositions();
} else if (shorting.length > 0) {
var cost = ns.stock.short(shorting[0],
Math.round(Math.min(
money / ns.stock.getAskPrice(shorting[0]),
ns.stock.getMaxShares(shorting[0]) - ns.stock.getPosition(shorting[0])[2])
));
ns.print("Shorted " + shorting[0] + " for " + Math.round(cost)
+ " (" + Math.round(ns.stock.getForecast(shorting[0]) * 1000) / 1000 + ")");
await UpdatePositions();
}
}

await ns.sleep(2000);
}
}
This seems VERY unreliable.
I've spent hours negative, and never been positive by more that 30ish million.
Why is this allowed to sell at a loss, and how do I fix that?
Which script are you using?

My script tends to show as negative until you actually sell all the stocks. Only when gains/losses are actualized, do you see the total profit/loss. During regular trading, my script limits losses by selling, while holding high performers. So it'll seem like negative, but it should trend upwards.
< >
Showing 1-11 of 11 comments
Per page: 1530 50