Update stocks script with features when buying stocks

This commit is contained in:
Holly McFarland 2025-05-29 14:03:21 -04:00
parent 9d3fd00dbf
commit 0cea3d1481
2 changed files with 270 additions and 128 deletions

View File

@ -11,7 +11,7 @@ Once you have Tampermonkey, click the name of the script you want to install. A
### Safety notes:
All of my scripts that have been approved by the /r/neopets Discord mod team as safe and fair are marked with a ✅ after their name.
All of my scripts that have been approved by the /r/neopets Discord mod team as safe and fair are marked with a ✅ after their name. If a previous version has been approved, a link to that one will be provided separately.
**No** userscripts, not mine or anyone else's, have been officially approved by TNT. Make peace with that before installing any script to use on Neopets.
@ -23,6 +23,8 @@ On the status page for the Swashbuckling Academy, Mystery Island Training School
\*I haven't tested this script at the Secret Ninja Training School because I don't have a pet even *close* to level 250. I have reason to believe it should work there, but I would appreciate confirmation from anyone who's able to test it out.
## [Stock Highlighter](https://git.hollymcfarland.com/monorail/NeopetsUserscripts/raw/branch/main/scripts/stock-highlighter.user.js) ✅
## [Stock Highlighter](https://git.hollymcfarland.com/monorail/NeopetsUserscripts/raw/branch/main/scripts/stock-highlighter.user.js) ([Approved version](https://git.hollymcfarland.com/monorail/NeopetsUserscripts/raw/commit/91e72b22d2d508240da43239d24f4ee32d3c3931/scripts/stock-highlighter.user.js) )
When checking your stock portfolio, automatically sort all stocks by current value and highlight any that are over your sell price. The sell price defaults to 60 neopoints but can be configured on the page itself. Custom sell prices are handled in real time and are automatically saved.
When checking your stock portfolio, automatically sort all stocks by current value and highlight any that are over your sell price. The sell price defaults to 60 neopoints but can be configured on the page itself. When viewing lists of stocks to purchase, sort them by current value, deemphasize those that are too cheap to buy, and highlight those at exactly the buy price.
Custom sell prices and a checkbox for whether or not the stock boon is enabled are handled in real time and are automatically saved.

View File

@ -1,10 +1,10 @@
// ==UserScript==
// @name Stock Highlighter
// @namespace https://hollymcfarland.com
// @version 1.1
// @description Sort stocks in your portfolio and highlight ones that are ready to sell
// @version 2.0
// @description Sort stocks in your portfolio, highlight ready to sell. Hide stocks too cheap to buy
// @author monorail
// @match https://www.neopets.com/stockmarket.phtml?type=portfolio
// @match https://www.neopets.com/stockmarket.phtml*
// @icon https://www.google.com/s2/favicons?sz=64&domain=neopets.com
// @grant none
// ==/UserScript==
@ -13,8 +13,34 @@
'use strict';
const DEFAULT_SELL_PRICE = 60;
const SELL_PRICE_KEY = "__monorail_userscript_sell_price"; // Unlikely to collide with anything, I think
const SELL_COLOUR = "#77FF77";
const READY_COLOUR = "#77FF77";
const KEY_PREFIX = "__monorail_userscript_"; // Unlikely to collide with anything, I think
const SELL_PRICE_KEY = `${KEY_PREFIX}sell_price`;
const BOON_KEY = `${KEY_PREFIX}has_boon`;
function classIf(element, className, condition) {
/*
If `condition`, add `className` to `element`.classList
Otherwise, remove `className` from `element`.classList
Adding an element that's already there or removing one that isn't are
noops, so there's no harm in not checking whether the class is present
*/
if (condition) {
element.classList.add(className);
} else {
element.classList.remove(className);
}
}
const currentPage = new URL(window.location.href).searchParams.get("type");
if (currentPage === "portfolio") {
/*
Handle everything on the portfolio page
- Sort stocks by price, descending
- Highlight stocks that are equal or greater to the defined sell price
- Handle sell price config
*/
function getPairs(iter) {
/*
@ -49,7 +75,7 @@
}
// Get the stocks from the DOM
const portfolio = document.querySelector("#postForm > table > tbody");
const portfolio = document.querySelector(".content tbody");
const stocks = getPairs(portfolio.children);
const footer = stocks.pop()[0]; // Store footer elsewhere
@ -93,21 +119,14 @@
stylesheet.insertRule("#postForm > table > tbody > tr:not([bgcolor]):not(.readyToSell):nth-child(4n+3) { background-color: #EEEEFF; }");
// Any stocks with a value higher than the sell price are highlighted
stylesheet.insertRule(`.readyToSell { background-color: ${SELL_COLOUR}; }`);
stylesheet.insertRule(`.readyToSell { background-color: ${READY_COLOUR}; }`);
document.adoptedStyleSheets = [stylesheet];
// Now that we're all set up, we can start working on highlighting stocks
function highlightStocks(sellPrice) {
for (let [stock, _] of stocks) {
// There's no reason not to add a class that already exists, or not to
// remove one that doesn't exist, that's all handled for us. So we can
// simply add or remove the class based on the price and it all works out
if (getPrice(stock) >= sellPrice) {
stock.classList.add("readyToSell");
} else {
stock.classList.remove("readyToSell");
}
classIf(stock, "readyToSell", getPrice(stock) >= sellPrice);
}
}
// We define the function and then call it right away, rather than just doing the
@ -144,4 +163,125 @@
localStorage.setItem(SELL_PRICE_KEY, this.value);
highlightStocks(Number(this.value));
});
} else {
/*
Handle buying stocks
- Sort stocks by price, ascending
- Hide stocks lower than 15 (or 10 with stock boon)
- Highlight stocks exactly at 15 (or 10 with stock boon)
*/
function getPrice(stock) {
/* Given a row representing a stock, return its price */
return Number(stock.children[5].innerText);
}
function stockCompare(a, b, floor, ignoreBy) {
/*
Sort function for comparing stocks, with extra functionality
Any stocks priced below `floor` have `ignoreBy` added to their
effective price before sorting. By setting `floor` to 15 (or 10
with the stock boon) and ignoreBy to the maximum stock price,
any stocks that are too cheap to buy can be moved to the end
while staying in sorted order
*/
return (
(getPrice(a) + (getPrice(a) < floor ? ignoreBy : 0))
- (getPrice(b) + (getPrice(b) < floor ? ignoreBy : 0))
);
}
function handleStocks(stocksList, hasBoon) {
/*
Sort stocks by price, ignoring those too cheap to buy,
and highlight those at exactly the buy price
*/
const buyPrice = hasBoon ? 10 : 15;
const stocks = [...stocksList.childNodes];
stocks.shift(); // Remove first element, i.e. header
// Maximum price of any stock
const max = Math.max(...stocks.map((stock) => (getPrice(stock))));
stocks.sort((a, b) => (stockCompare(a, b, buyPrice, max)));
for (const stock of stocks) {
// Append all stocks to the object they're already a child of in
// sorted order and apply classes for styling
stocksList.appendChild(stock);
// Apply relevant classes for styling
classIf(stock, "atBuyPrice", getPrice(stock) === buyPrice);
classIf(stock, "tooCheap", getPrice(stock) < buyPrice);
// Clean up some bad-practice HTML stuff that's going to get in our way
for (const e of stock.childNodes) {
e.removeAttribute("bgcolor");
if (!e.innerHTML.includes("<img>") && !e.innerHTML.includes("<p>")) {
e.innerHTML = `<p>${e.innerHTML}</p>`;
}
}
}
}
// There's no way to tell from the URL if there's a table of stocks to buy
// (If you search for stocks with a given name, there's no GET params at all)
// So we'll just have to look for it
const stocksList = document.querySelector(".content tbody");
// Make sure there's a table and that it has "Logo" in its first row
if (!stocksList) return;
if (!stocksList.childNodes[0].innerText.includes("Logo")) return;
// Now we know that we're on a page with a list of stocks
// Add a custom stylesheet for highlighting stocks at the buy price and hiding
// stocks that are too cheap to buy
const stylesheet = new CSSStyleSheet();
// Default background colour, because the bgcolor attribute is removed to make
// the styles work
stylesheet.insertRule(".content table { background-color: #EEEEFF; border: 1px ssolid black; }")
// Highlight stocks at the buy price
stylesheet.insertRule(`.atBuyPrice { background-color: ${READY_COLOUR}; }`);
// Hide stocks that are too cheap to buy
stylesheet.insertRule(".tooCheap td img, .tooCheap td p { opacity: 0.4; }");
document.adoptedStyleSheets = [stylesheet];
// Check saved value for whether the stock boon is enabled
const hasBoon = localStorage.getItem(BOON_KEY) === "true";
// Do the initial sort and styling
handleStocks(stocksList, hasBoon);
// Create and lightly styling the input for setting whether or not you have the boon
const configContainer = document.createElement("div");
configContainer.style.margin = "auto";
configContainer.style.paddingTop = "1rem";
configContainer.style.width = "fit-content";
const boonCheckLabel = document.createElement("label");
boonCheckLabel.setAttribute("for", "hasboon");
boonCheckLabel.innerText = "Stock boon enabled: ";
boonCheckLabel.style.fontWeight = "bold";
configContainer.appendChild(boonCheckLabel);
const boonCheckInput = document.createElement("input");
boonCheckInput.setAttribute("type", "checkbox");
boonCheckInput.setAttribute("name", "hasboon");
boonCheckInput.checked = hasBoon;
configContainer.appendChild(boonCheckInput);
document.querySelector(".content table").after(configContainer);
// Wire up the input to update the saved boon value and re-sort
boonCheckInput.addEventListener("change", function() {
localStorage.setItem(BOON_KEY, this.checked);
handleStocks(stocksList, this.checked);
});
}
})();