// Options
// Automatic trading and broker buying when true
var stockerAutoTrading = true
// Stock market is running when true
var stockerMarketOn = true;
// Minimum number of brokers required for automatic trading
var stockerMinBrokers = 72 // Default of 72 results in 0.5% commission
// Fraction of banked cookies allowed for automatic trading
var stockerCookiesThreshold = 0.05;
// Buy all necessary brokers as soon as we can afford them
var stockerAutoBuyBrokers = true
// Increases number of warehouses in sync with the highest raw CPS during this session
var stockerExponential = true;
// The ratio of the highest raw CPS to the original raw CPS is raised to this power when Exponential Warehouses is on
var stockerExponentialPower = 1.0;
// Announce transactions in game notifications
var stockerTransactionNotifications = true;
// Make regular profit reports
var stockerActivityReport = false
// How often to make regular reports in ms (one hour by default)
var stockerActivityReportFrequency = 1000 * 60 * 60
// Make game notifications fade away on their own
var stockerFastNotifications = false
// Use console.log for more detailed info on prices and trends
var stockerConsoleAnnouncements = false
// Display warning message when broker numbers or bank cookies are insufficient to run automatic trading.
var stockerResourcesWarning = true
// Display more detailed trading info near the top of the stock market display
var stockerAdditionalTradingStats = true
// Logic loop frequency; do not touch it unless you are cheating
var stockerLoopFrequency = 1000 * 30
// The cheat itself. Rolls the cycle every time logic loop triggers
var stockerForceLoopUpdates = false
var stockerGreeting = 'click clack you will soon be in debt'
// ===================================================================================
const CS_TEN_YEARS = 86400 * 365.25 * 10; // seconds
const CS_GASEOUS_PROFITS = 31536000; // $31,536,000
const CS_PLASMIC_PROFITS = 100000000; // $100,000,000
const CS_BOSE_EINSTEIN_PROFITS = 500000000; // $500,000,000
if (typeof CCSE === 'undefined')
Game.LoadMod('https://klattmose.github.io/CookieClicker/SteamMods/CCSE/main.js')
if (typeof CookiStocker === 'undefined') var CookiStocker = {};
CookiStocker.name = 'CookiStocker';
CookiStocker.version = '3.0';
CookiStocker.GameVersion = '2.053';
CookiStocker.build = 'Friday 2025-09-19 08:13:16 AM';
// One place to hold the interval handle + the current period (ms)
CookiStocker.reportTimer = 0;
CookiStocker._reportEveryMs = 0;
CookiStocker._cfgReady = false; // set true at the end of CookiStocker.load()
CookiStocker.Bank = 0;
if (typeof CookiStocker.stockList === 'undefined') {
CookiStocker.stockList = (typeof stockList === 'object' && stockList) || {};
}
var stockList = CookiStocker.stockList;
stockList = {
Check: 'dump eet',
Goods: [],
Start: Date.now() + 500,
lastTime: Date.now() + 500,
startingProfits: 0,
Profits: 0,
netProfits: 0,
grossProfits: 0,
grossLosses: 0,
totalStocks: 0,
totalShares: 0,
totalValue: 0,
unrealizedProfits: 0,
profitableStocks: 0,
unprofitableStocks: 0,
profitableTrades: 0,
unprofitableTrades: 0,
Purchases: 0,
Sales: 0,
Uptime: 0,
hourlyProfits: 0,
dailyProfits: 0,
minCookies: Number.MAX_VALUE,
maxCookies: 0,
noModActions: false,
origCookiesPsRawHighest: 0,
Amount: 0,
canBuy: true,
shadowGone: false,
}
let stockerModeProfits = [
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
];
var modeDecoder = ['stable','slowly rising','slowly falling','rapidly rising','rapidly falling','chaotic'] // meanings of each market trend (good.mode)
var goodIcons = [[2,33],[3,33],[4,33],[15,33],[16,33],[17,33],[5,33],[6,33],[7,33],[8,33],[13,33],[14,33],[19,33],[20,33],[32,33],[33,33],[34,33],[35,33]];
CookiStocker.launch = function() {
CookiStocker.Bank = Game.Objects['Bank'].minigame;
this.isLoaded = 1;
}
if (!CookiStocker.isLoaded){
if (CCSE && CCSE.isLoaded) {
CookiStocker.launch();
}
else {
if(!CCSE) var CCSE = {};
if(!CCSE.postLoadHooks) CCSE.postLoadHooks = [];
CCSE.postLoadHooks.push(CookiStocker.launch);
}
}
function ensureStockerStyles() {
if (document.getElementById('stocker-styles')) return; // avoid duplicates
const css = `
.stocker-stats{
display:flex;
flex-wrap:wrap; /* allow wrapping onto a new line */
justify-content:center; /* center each line */
align-items:baseline;
gap:0 3px; /* horizontal spacing between fields */
white-space:normal; /* permit wrapping */
}
.stocker-stats .stat{
white-space:nowrap; /* keep each field intact */
font-size:10px;
color:rgba(255,255,255,0.8);
padding:1px 3px;
}
/* Force a manual break after a chosen field on narrow panes */
.stocker-stats .break{ flex-basis:100%; height:0; }
@media (min-width: 950px){ .stocker-stats .break{ display:none; } }
`;
const style = document.createElement('style');
style.id = 'stocker-styles';
style.textContent = css;
document.head.appendChild(style);
}
ensureStockerStyles();
// Optional stats container id
CookiStocker.extraStatsId = 'stockerExtra';
// Rebuilds the 2nd/3rd/4th lines exactly as before
CookiStocker.buildExtraStatsHTML = function(){
// These are the same strings you already use (datStr2/datStr3/datStr4)
// Keeping markup identical; wrapped by our container.
let html = '';
html += `
Net cookies won: 0 .
Cookies per hour: 0 .
Cookies per day: 0 .
Purchases: 0 .
Sales: 0 .
`;
html += `
CPS multiple: 0 .
Stocks held: ${stockList.totalStocks} .
Total shares: ${Beautify(stockList.totalShares, 0)} .
Total value: ${Beautify(stockList.totalValue, 2)} .
Unrealized profits: ${Beautify(stockList.unrealizedProfits, 0)} .
`;
html += `
Profitable stocks: 0 .
Unprofitable stocks: 0 .
Profitable trades: 0 .
Unprofitable trades: 0 .
Average profit per trade: $0 .
Average loss per trade: $0 .
`;
return html;
};
// Shows or hides the optional block immediately when the option changes.
// If enabling and the container doesn't exist yet, we create and populate it.
CookiStocker.updateAdditionalStatsVisibility = function(){
const header = l('bankHeader');
const host = header && header.firstChild ? header.firstChild : null;
if (!host) return;
let extra = l(CookiStocker.extraStatsId);
if (stockerAdditionalTradingStats){
// Ensure container exists and is visible
if (!extra){
extra = document.createElement('div');
extra.id = CookiStocker.extraStatsId;
extra.innerHTML = CookiStocker.buildExtraStatsHTML();
host.appendChild(extra);
}
extra.style.display = '';
} else {
// Hide; we could also removeChild if you prefer to tear it down
if (extra){
extra.style.display = 'none';
}
}
};
function stockerTimeBeautifier(duration) {
var milliseconds = Math.floor(duration % 1000),
seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60),
hours = Math.floor((duration / (1000 * 60 * 60)) % 24),
days = Math.floor(duration / (1000 * 60 * 60 * 24));
if (seconds && (minutes || hours || days) && !stockerForceLoopUpdates)
seconds = 0; // Don't display
var strSeconds = seconds + ' second' + (seconds != 1 ? 's' : '');
var strMinutes = minutes ? minutes + ' minute' + (minutes != 1 ? 's' : '') + (seconds ? (hours || days ? ', and ' : ' and ') : '') : '';
var strHours = hours ? hours + ' hour' + (hours != 1 ? 's' : '') + (minutes && seconds ? ', ' : ((minutes ? !seconds : seconds) ? ' and ' : '')) : '';
var strDays = days ? days + ' day' + (days != 1 ? 's' : '') + (hours && minutes || hours && seconds || minutes && seconds ? ', ' : (((hours ? !minutes : minutes) ? !seconds : seconds) ? ' and ' : '')) : '';
var strTime = strDays + strHours + strMinutes;
if (stockerForceLoopUpdates && seconds)
strTime += strSeconds;
if (minutes || hours || days) {
return (strTime);
} else
return (strSeconds);
}
// --- Anchored scheduling on each market tick ------------------------------
CookiStocker._tickHookInstalled = 0;
CookiStocker._tickTimeout = 0;
CookiStocker._reportTimeout = 0;
CookiStocker._onMarketTick = function() {
if (CookiStocker._tickTimeout) { clearTimeout(CookiStocker._tickTimeout); CookiStocker._tickTimeout = 0; }
if (CookiStocker._reportTimeout) { clearTimeout(CookiStocker._reportTimeout); CookiStocker._reportTimeout = 0; }
CookiStocker._tickTimeout = setTimeout(function() {
try {
if (typeof stockerLoop === 'function') stockerLoop();
else if (CookiStocker && typeof CookiStocker.stockerLoop === 'function') CookiStocker.stockerLoop();
} catch (e) {}
var delay = stockerForceLoopUpdates ? 0 : 30000; // 0 ms when forcing; else 30 s
CookiStocker._reportTimeout = setTimeout(function() {
try { CookiStocker.Reports(); } catch (e) {}
}, delay);
}, 500); // let the minigame finish its own recompute
};
CookiStocker.installBankTickHook = function() {
if (CookiStocker._tickHookInstalled) return;
var M = Game && Game.Objects && Game.Objects['Bank'] && Game.Objects['Bank'].minigame;
if (!M || typeof M.tick !== 'function') return;
CookiStocker._tickHookInstalled = 1;
var _origTick = M.tick;
M.tick = function() {
var ret = _origTick.apply(this, arguments);
if (typeof stockerMarketOn === 'undefined' || stockerMarketOn) {
CookiStocker._onMarketTick();
}
return ret;
};
};
// One place to hold the interval handle + the current period (ms)
CookiStocker.reportTimer = 0;
CookiStocker._reportEveryMs = 0;
// Arm/disarm the periodic reporter so there is exactly one timer when needed
CookiStocker.ensureReportTimer = function() {
if (CookiStocker.reportTimer) {
clearInterval(CookiStocker.reportTimer);
CookiStocker.reportTimer = 0;
}
// Do we need a periodic timer at all right now?
const need = stockerMarketOn && (stockerActivityReport || stockerConsoleAnnouncements);
const next = need ? Math.max(1000, (+stockerActivityReportFrequency || 3600000)) : 0;
// If we don't need it, tear down anything that exists and reset bookkeeping
if (!need) {
if(CookiStocker.reportTimer) {
clearInterval(CookiStocker.reportTimer);
CookiStocker.reportTimer = 0;
}
CookiStocker._reportEveryMs = 0;
return;
}
// If we need it and the period hasn't changed and it's already running, do nothing
if (CookiStocker.reportTimer && CookiStocker._reportEveryMs === next) return;
// (Re)arm with the new period
if (CookiStocker.reportTimer) {
clearInterval(CookiStocker.reportTimer);
CookiStocker.reportTimer = 0;
}
CookiStocker._reportEveryMs = next;
CookiStocker.reportTimer = setInterval(function(){ CookiStocker.Reports(); }, next);
};
CookiStocker.ensureAchievements = function(){
if (Game.Achievements['Plasmic assets'] && Game.Achievements['Bose-Einstein Condensed Assets'])
return; // already created
CookiStocker.AchPlasmic = CCSE.NewAchievement(
'Plasmic assets',
'Have your stock market profits surpass $100 million .This will get you charged up! Your warehouse companies double their space. ',
[10,13]
);
CookiStocker.AchPlasmic.order = 1003100;
CookiStocker.AchBoseEinstein = CCSE.NewAchievement(
'Bose-Einstein Condensed Assets',
'Have your stock market profits surpass $500 million .You have so many assets, we need to condense them! Your warehouse companies double their space. ',
[9,19]
);
CookiStocker.AchBoseEinstein.pool = 'shadow';
CookiStocker.AchBoseEinstein.order = 1003101;
};
Game.registerMod('CookiStocker',{
init: function () {
Game.registerHook('reset', function (hard) {
CookiStocker.reset(hard);
});
CookiStocker.ReplaceGameMenu();
Game.Notify('CookiStocker is loaded',stockerGreeting,[1,33],false);
this.startStocking();
},
save: function () {
return CookiStocker.save();
},
// The game will pass the string we returned from save() back into load(str).
// We defer until the Bank minigame is present so CookiStocker.load can safely touch its state.
load: function (str) {
var tries = 0;
(function tryLoad() {
var bankReady =
typeof Game === 'object' && Game.ready &&
Game.Objects && Game.Objects['Bank'] &&
Game.Objects['Bank'].minigame && stockList.Goods[0];
if (bankReady) {
try {
// Ensure CookiStocker sees the Bank minigame
if (typeof CookiStocker.Bank === 'undefined' || !CookiStocker.Bank) {
CookiStocker.Bank = Game.Objects['Bank'].minigame;
}
CookiStocker.load(str || '');
} catch (e) {
console.warn('[CookiStocker] load failed:', e);
}
} else {
// Try again a few times while the game finishes loading UI/minigames.
if (tries++ < 120) setTimeout(tryLoad, 250); // up to ~30s
else console.warn('[CookiStocker] load skipped (Bank minigame never became ready).');
}
})();
},
startStocking: function () {
if (!(CookiStocker.Bank = Game.Objects['Bank'].minigame)) {
// console.log('=====$$$=== Stock Market minigame has not initialised yet! Will try again in 500 ms.');
setTimeout(() => {
this.startStocking();
}, 500);
return
}
else {
console.log('=====$$$=== CookiStocker logic loop initialised at ' + new Date());
console.log('=====$$$=== With main options as follows:')
console.log('=====$$$=== Logic loop frequency: ' + stockerTimeBeautifier(stockerLoopFrequency))
console.log('=====$$$=== Report frequency: ' + stockerTimeBeautifier(stockerActivityReportFrequency))
console.log('=====$$$=== Cheating: ' + stockerForceLoopUpdates)
console.log(stockList.Check);
}
CookiStocker.Bank = Game.Objects['Bank'].minigame;
CookiStocker.patchedMaxStock || (function(){ /* the override above */ })();
if (!CookiStocker.patchedMaxStock) {
var M = Game.Objects['Bank'].minigame;
var oldGet = M.getGoodMaxStock;
M.getGoodMaxStock = function(good){
var base = oldGet.call(this, good);
if (CookiStocker.Bank.officeLevel < 3 || stockList.Profits < CS_PLASMIC_PROFITS)
return base;
var mult = 1;
if (!stockList.shadowGone && stockList.Profits >= CS_GASEOUS_PROFITS) {
if (Game.Achievements['Gaseous assets'] && Game.Achievements['Gaseous assets'].won) {
Game.Achievements['Gaseous assets'].pool = '';
stockList.shadowGone = true;
} else
return;
}
if (stockerExponential && stockList.origCookiesPsRawHighest)
mult *= Game.cookiesPsRawHighest ** (stockerExponentialPower / stockList.origCookiesPsRawHighest);
if (Game.Achievements['Plasmic assets'] && Game.Achievements['Plasmic assets'].won && stockList.Profits >= CS_PLASMIC_PROFITS * mult)
mult *= 2;
if (Game.Achievements['Bose-Einstein Condensed Assets'] && Game.Achievements['Bose-Einstein Condensed Assets'].won && stockList.Profits >= CS_BOSE_EINSTEIN_PROFITS * mult)
mult *= 2;
return Math.ceil(base * mult);
};
CookiStocker.patchedMaxStock = true;
}
CookiStocker.installBankTickHook();
let datStr = `
Net profits: $0 .
Profits per hour: $0 .
Profits per day: $0 .
Gross profits: $0 .
Gross losses: $0 .
Runtime: ${stockerForceLoopUpdates ? "0:00:00" : "0:00"}
`;
let datStrWarn = `
THERE ARE INSUFFICENT RESOURCES TO RUN AUTOMATIC TRADING. PLEASE SEE THE FOLLOWING LINE AND READ THE STEAM GUIDE.
`;
let datStrWarn2 = `
AUTO TRADING IS TURNED OFF IN THE OPTIONS.
`;
let datStrWarn3 = `
THE STOCK MARKET IS TURNED OFF IN THE OPTIONS.
`;
l('bankHeader').firstChild.insertAdjacentHTML('beforeend', datStr);
let datStr1 = `
Brokers: 0 .
Brokers Needed: 0 .
Banked cookies: 0 .
Required cookie minimum: 0 .
Maximum: 0 .
`;
l('bankHeader').firstChild.insertAdjacentHTML('beforeend', datStrWarn);
l('bankHeader').firstChild.insertAdjacentHTML('beforeend', datStrWarn2);
l('bankHeader').firstChild.insertAdjacentHTML('beforeend', datStrWarn3);
l('bankHeader').firstChild.insertAdjacentHTML('beforeend', datStr1);
// optional lines now live in a single container we control
let extra = l(CookiStocker.extraStatsId);
if (!extra){
extra = document.createElement('div');
extra.id = CookiStocker.extraStatsId;
l('bankHeader').firstChild.appendChild(extra);
}
// initial visibility / content
if (stockerAdditionalTradingStats) {
extra.innerHTML = CookiStocker.buildExtraStatsHTML();
extra.style.display = '';
} else {
extra.innerHTML = ''; // keep empty and hidden initially
extra.style.display = 'none';
}
let market = CookiStocker.Bank.goodsById; // read market
console.log('Reading the market:');
stockList.startingProfits = CookiStocker.Bank.profit;
for (let i = 0; i < market.length; i++){
stockList.Goods.push({
name: market[i].name,
stock: market[i].stock,
currentPrice: market[i].val,
mode: market[i].mode,
lastMode: market[i].mode,
lastDur: market[i].dur,
unchangedDur: 0,
dropCount: 0,
riseCount: 0,
profit: 0,
someSold: false,
someBought: false,
});
console.log('Stock: ' + market[i].name.replace('%1', Game.bakeryName) + ' Status: ' + modeDecoder[market[i].mode] + ' at $' + market[i].val + (market[i].stock ? ' (own)' : ''));
}
CookiStocker.ensureAchievements();
CookiStocker.ensureReportTimer();
CookiStocker.TradingStats();
var stockerLoop = setInterval(function() {
if (l("Brokers") == null)
return; // Stock market went away
if (stockerMarketOn) {
if (stockList.noModActions) {
stockList.noModActions = false;
CookiStocker.TradingStats();
}
if (stockerForceLoopUpdates)
CookiStocker.Bank.secondsPerTick = Math.max(0.001, stockerLoopFrequency / 1000);
else
CookiStocker.Bank.secondsPerTick = 60;
} else {
if (stockList.noModActions)
return;
CookiStocker.Bank.secondsPerTick = CS_TEN_YEARS;
}
let doUpdate = false;
// setting stockerForceLoopUpdates to true will make the logic loop force the market to tick every time it triggers,
// making this an obvious cheat, and i will personally resent you.
//
// but
// if you backup your save and set stockerLoopFrequency to like 10 milliseconds it looks very fun and effective.
// yes, this is how i made the gif on the steam guide page. [Comments by Gingerguy.]
if (!stockerForceLoopUpdates && stockerMarketOn)
stockerLoopFrequency = CookiStocker.Bank.secondsPerTick * 500; // Keep up to date
if (CookiStocker.Bank.profit >= 100000000 && !Game.Achievements['Plasmic assets'].won)
Game.Win('Plasmic assets');
if (CookiStocker.Bank.profit >= 500000000 && !Game.Achievements['Bose-Einstein Condensed Assets'].won)
Game.Win('Bose-Einstein Condensed Assets');
const smallDelta = 3;
const largeDelta = 4;
const alwaysBuyBelow = 2;
const neverSellBelow = 11;
let amount = 0;
if (stockerAutoBuyBrokers) {
let buyBrokers = stockerMinBrokers - CookiStocker.Bank.brokers;
if (buyBrokers > 0 && stockerMinBrokers <= CookiStocker.Bank.getMaxBrokers() && buyBrokers * CookiStocker.Bank.getBrokerPrice() < Game.cookies * 0.1) {
Game.Spend(CookiStocker.Bank.getBrokerPrice() * buyBrokers);
CookiStocker.Bank.brokers = stockerMinBrokers;
CookiStocker.TradingStats();
}
}
market = CookiStocker.Bank.goodsById; // update market
stockList.canBuy = stockerAutoTrading && CookiStocker.Bank.brokers >= stockerMinBrokers;
for (let i = 0; i < market.length; i++) {
if (stockList.canBuy && !((CookiStocker.Bank.getGoodMaxStock(market[i]) - market[i].stock) * Game.cookiesPsRawHighest * market[i].val < Game.cookies * stockerCookiesThreshold)) {
let now = Date.now();
let remainder;
stockList.Start += now - stockList.lastTime;
stockList.Uptime = Math.floor((now - stockList.Start) / 1000) * 1000;
if (remainder = stockList.Uptime % stockerLoopFrequency) {
stockList.Start += CookiStocker.Bank.secondsPerTick * 1000 + remainder;
stockList.Uptime -= CookiStocker.Bank.secondsPerTick * 1000 + remainder;
}
stockList.lastTime = now;
CookiStocker.TradingStats();
stockList.canBuy = false;
if (!stockerAutoTrading) {
stockList.noModActions = true;
if (CookiStocker.reportTimer) {
clearInterval(CookiStocker.reportTimer);
CookiStocker.reportTimer = null;
}
}
}
amount += Game.ObjectsById[i+2].amount;
}
if (!(stockList.Amount = amount)) // No stocks active
return;
CookiStocker.TradingStats();
CookiStocker.ensureReportTimer();
if (stockList.canBuy && !stockList.origCookiesPsRawHighest)
stockList.origCookiesPsRawHighest = Game.cookiesPsRawHighest;
for (let i = 0; i < market.length; i++) {
let stockerNotificationTime = stockerFastNotifications * 6;
let lastPrice = stockList.Goods[i].currentPrice;
let currentPrice = market[i].val;
// update stockList
stockList.Goods[i].stock = market[i].stock;
stockList.Goods[i].currentPrice = market[i].val;
stockList.Goods[i].mode = market[i].mode;
let md = stockList.Goods[i].mode;
let lmd = stockList.Goods[i].lastMode;
let lastStock = market[i].stock;
let deltaPrice = largeDelta;
let stockName = market[i].name.replace('%1', Game.bakeryName);
// Our ceilingPrice is the maximum of the bank ceiling and the (deprecated but still useful) stock ceiling
let ceilingPrice = Math.max(10*(i+1) + Game.Objects['Bank'].level + 49, 97 + Game.Objects['Bank'].level * 3);
if (Game.ObjectsById[i+2].amount == 0 && stockList.canBuy) { // stock must be active
console.log(`${stockName} stock is inactive`);
continue;
}
if (lmd == md && (stockList.Goods[i].stock && (md == 2 || md == 4) || // Make selling into a downturn easier
!stockList.Goods[i].stock && (md == 1 || md == 3))) // Make buying into an upturn easier
deltaPrice = smallDelta;
if (md != lmd && (md == 3 && lmd != 1 || md == 4 && lmd != 2 || md == 1 && lmd != 3 || md == 2 && lmd != 4)) {
stockList.Goods[i].dropCount = 0;
stockList.Goods[i].riseCount = 0;
} else if (currentPrice > lastPrice) {
stockList.Goods[i].dropCount = 0;
stockList.Goods[i].riseCount++;
} else if (currentPrice < lastPrice) {
stockList.Goods[i].riseCount = 0;
stockList.Goods[i].dropCount++;
}
if (stockList.Goods[i].lastDur != market[i].dur || ++stockList.Goods[i].unchangedDur > 1) {
stockList.Goods[i].unchangedDur = 0;
doUpdate = true;
}
if (stockerConsoleAnnouncements && doUpdate && stockList.canBuy) { // Tick tick
if (md == lmd)
console.log(`${stockName} mode is unchanged at ${lmd} [${modeDecoder[lmd]}] at $${Beautify(currentPrice, 2)}`);
else
console.log(`MODE CHANGE ${stockName} old mode was ${lmd} [${modeDecoder[lmd]}] and new mode is ${md} [${modeDecoder[md]}] at $${Beautify(currentPrice, 2)}`);
}
stockList.Goods[i].lastDur = market[i].dur;
if ( // buy conditions
(
currentPrice < alwaysBuyBelow || md != 4 && ((currentPrice > lastPrice &&
stockList.Goods[i].riseCount >= deltaPrice || (md == 1 || md == 3) && md != lmd ||
md == 0 && !stockList.Goods[i].someSold && stockList.Goods[i].dropCount < deltaPrice &&
currentPrice >= 10) && (currentPrice < ceilingPrice || md == 1 || md == 3))
)
&& stockList.canBuy && ((CookiStocker.Bank.getGoodMaxStock(market[i]) - market[i].stock) * Game.cookiesPsRawHighest * market[i].val < Game.cookies * stockerCookiesThreshold && CookiStocker.Bank.brokers >= stockerMinBrokers)
&& CookiStocker.Bank.buyGood(i,10000) // actual buy attempt
)
{
// buying
let mode = (lmd != md) ? 'is no longer in ' + modeDecoder[lmd] + ' mode' : 'is ';
let units = market[i].stock - lastStock;
stockList.Goods[i].someBought = true;
stockList.Goods[i].stock = market[i].stock;
if (typeof market[i].prevBuyMode1 !== 'undefined')
{
market[i].prevBuyMode1 = lmd;
market[i].prevBuyMode2 = md;
}
market[i].buyTime = Date.now();
if (typeof StockAssistant !== 'undefined')
{
StockAssistant.stockData.goods[i].boughtVal = market[i].prev;
StockAssistant.buyGood(i);
}
stockList.Purchases++;
if (stockerTransactionNotifications)
if (currentPrice >= 2) Game.Notify(`Buying ${stockName} ${new Date().toLocaleTimeString([], {hourCycle: 'h23', hour: '2-digit', minute: '2-digit'})}`,`Buying ${units} unit${(units > 1 ? 's' : '')}. The stock ${mode} at $${Beautify(market[i].prev, 2)} per unit (your buying price) and is in ${modeDecoder[md]} mode now.`,goodIcons[i],stockerNotificationTime);
else Game.Notify(`Buying ${stockName} ${new Date().toLocaleTimeString([], {hourCycle: 'h23', hour: '2-digit', minute: '2-digit'})}`, `Buying ${units} unit${(units > 1 ? 's' : '')}. The price has dropped below $2 per unit, and your buying price is $${Beautify(market[i].prev, 2)}.`,goodIcons[i],stockerNotificationTime);
if (stockerConsoleAnnouncements) console.log('=====$$$=== Buying '+ stockName + ' at $' + Beautify(market[i].prev, 2));
} else if ( // sell conditions
stockList.Goods[i].stock > 0 && (currentPrice < lastPrice &&
stockList.Goods[i].dropCount >= deltaPrice ||
(md == 2 || md == 4) && md != lmd) && currentPrice >= neverSellBelow // not near the bottom
)
{
let profit = 0;
let strProfit = 'profit '
let mode = (lmd != md) ? 'is no longer in ' + modeDecoder[lmd] + ' mode and ' : '';
if (!CookiStocker.Bank.sellGood(i,stockList.Goods[i].stock)) {
stockList.Goods[i].lastMode = stockList.Goods[i].mode;
continue;
}
stockList.Goods[i].someSold = true;
market[i].prevSale = market[i].val;
market[i].prevSellMode1 = lmd;
market[i].prevSellMode2 = md;
market[i].sellTime = Date.now();
if (typeof StockAssistant !== 'undefined')
StockAssistant.sellGood(i);
stockList.Sales++;
profit = (market[i].val - market[i].prev) * stockList.Goods[i].stock;
stockList.Goods[i].profit += profit;
if (profit > 0) {
stockList.grossProfits += profit;
stockList.profitableTrades++;
} else {
stockList.grossLosses += -profit;
stockList.unprofitableTrades++;
}
stockList.netProfits += profit;
stockerModeProfits[lmd][md][0] += profit;
stockerModeProfits[lmd][md][1] += profit;
stockerModeProfits[lmd][md][2]++;
if (profit < 0)
{
strProfit = 'loss ';
profit = -profit;
}
if (stockerTransactionNotifications) Game.Notify(`Selling ${stockName} ${new Date().toLocaleTimeString([], {hourCycle: 'h23', hour: '2-digit', minute: '2-digit'})}`,`Selling ${stockList.Goods[i].stock} unit${(stockList.Goods[i].stock > 1 ? 's' : '')} at a price of $${Beautify(market[i].val, 2)} per unit for a ${strProfit} of $${Beautify(profit, 2)} and total revenue of $${Beautify(market[i].val*stockList.Goods[i].stock, 2)}, which is added to the total market profits. The stock ${mode} is in ${modeDecoder[md]} mode now. Bought at a price of $${Beautify(market[i].prev, 2)} per unit.`,goodIcons[i],stockerNotificationTime);
if (stockerConsoleAnnouncements) console.log(`=====$$$=== Selling ${stockName} at $${Beautify(market[i].val, 2)} for a ${strProfit}of $${Beautify(profit, 2)} and total revenue of $${Beautify(market[i].val*stockList.Goods[i].stock, 2)}. Last bought at $${Beautify(market[i].prev, 2)}.`);
}
stockList.Profits = CookiStocker.Bank.profit - stockList.startingProfits;
stockList.Goods[i].lastMode = stockList.Goods[i].mode;
}
stockList.profitableStocks = stockList.unprofitableStocks = 0;
for (let i = 0; i < market.length; i++) { // Must recalculate the whole list on every pass
if (stockList.Goods[i].profit > 0)
stockList.profitableStocks++;
else if (stockList.Goods[i].profit < 0)
stockList.unprofitableStocks++;
}
CookiStocker.TradingStats();
if (!stockerMarketOn) {
if (CookiStocker.reportTimer) { clearInterval(CookiStocker.reportTimer); CookiStocker.reportTimer = null; }
CookiStocker.Reports(); // one last summary
stockList.noModActions = true; // freeze until ON
return;
}
},stockerLoopFrequency);
},
})
CookiStocker.Reports = function() {
if (l("Brokers") == null || !stockList.Amount || !stockList.canBuy)
return; // Stock market went away
CookiStocker.TradingStats();
if (stockList.noModActions || !stockerActivityReport && !stockerConsoleAnnouncements)
return;
let stockerNotificationTime = stockerFastNotifications * 6;
if (stockerActivityReport)
if ((stockList.Purchases + stockList.Sales) == 0) {
Game.Notify(
`CookiStocker report ${new Date().toLocaleTimeString([], {hourCycle: 'h23', hour: '2-digit', minute: '2-digit'})}`,
`This session has been running for ${stockerTimeBeautifier(stockList.Uptime)}, but no good investment opportunities were detected! Luck is not on our side, yet.`
,[1, 33],stockerNotificationTime
);
} else {
Game.Notify(
`CookiStocker report ${new Date().toLocaleTimeString([], {hourCycle: 'h23', hour: '2-digit', minute: '2-digit'})}`,
`This session has been running for ${stockerTimeBeautifier(stockList.Uptime)} and has made $${Beautify(stockList.netProfits, 0)} in net profits and $${Beautify(stockList.Profits, 0)} in revenue (displayed profits) in ${Beautify(stockList.Purchases, 0)} purchases and ${Beautify(stockList.Sales, 0)} sales.`,[1, 33],stockerNotificationTime
);
}
if (stockerConsoleAnnouncements) {
let totalProfits = 0;
let subtotalProfits = 0;
let deltaTotalProfits = 0;
let deltaSubtotalProfits = 0;
let totalTrades = 0;
let subtotalTrades = 0;
let profit = 0;
let lastProfit = 0;
let trades = 0;
let strProfit = '';
let strDeltaModeProfits = '';
let strTrades = '';
let j, k;
console.log(`Running for ${stockerTimeBeautifier(stockList.Uptime)} and made $${Beautify(stockList.netProfits, 0)}\n in net profits and $${Beautify(stockList.Profits, 0)} in revenue (displayed profits)\n in ${Beautify(stockList.Purchases, 0)} purchases and ${Beautify(stockList.Sales, 0)} sales.\nTotal number of stocks held: ${stockList.totalStocks}. Total shares: ${Beautify(stockList.totalShares, 0)}.\nTotal value: $${Beautify(stockList.totalValue)}. Unrealized profits: $${Beautify(stockList.unrealizedProfits, 2)}.\nTotal gross profits: $${Beautify(stockList.grossProfits, 0)}. Profitable stocks: ${stockList.profitableStocks}.\nProfitable trades: ${stockList.profitableTrades}. Average profit per trade: $${stockList.grossProfits ? Beautify(stockList.grossProfits / stockList.profitableTrades, 2) : 0}.\nTotal gross losses: $${Beautify(stockList.grossLosses, 0)}. Unprofitable stocks: ${stockList.unprofitableStocks}.\nUnprofitable trades: ${stockList.unprofitableTrades}. Average loss per trade: $${stockList.grossLosses ? Beautify(stockList.grossLosses / stockList.unprofitableTrades, 2) : 0}.`);
// Stats for individual modes
for (j = 0; j < 6; j++)
for (k = 0; k < 6; k++)
totalProfits += stockerModeProfits[j][k][0];
for (j = 0; j < 6; j++)
for (k = 0; k < 6; k++) {
profit = stockerModeProfits[j][k][0];
lastProfit = stockerModeProfits[j][k][1];
trades = stockerModeProfits[j][k][2];
strProfit = profit ? ((100 * profit/totalProfits).toFixed(2) + '%').padStart(8) : '';
strDeltaModeProfits = (lastProfit ? '$' + Beautify(lastProfit, 2) : '').padStart(14);
strTrades = trades ? (' ' + trades + ' trade' + (trades > 1 ? 's' : ' ')).padStart(13) : '';
console.log(`Profits[${j}][${k}] = $${Beautify(profit, 2).padEnd(14)} ${strProfit}${strDeltaModeProfits}${strTrades}`);
subtotalProfits += profit;
deltaSubtotalProfits += lastProfit;
deltaTotalProfits += lastProfit;
subtotalTrades += trades;
totalTrades += trades;
}
// Stats for subtotals
for (j = 0; j < 6; j++) {
subtotalProfits = 0;
deltaSubtotalProfits = 0;
subtotalTrades = 0;
for (k = 0; k < 6; k++) {
subtotalProfits += stockerModeProfits[j][k][0];
deltaSubtotalProfits += stockerModeProfits[j][k][1];
subtotalTrades += stockerModeProfits[j][k][2];
stockerModeProfits[j][k][1] = 0;
}
strProfit = subtotalProfits ? ((100 * subtotalProfits/totalProfits).toFixed(2) + '%').padStart(8) : '';
strDeltaModeProfits = (deltaSubtotalProfits ? '$' + Beautify(deltaSubtotalProfits, 2) : '').padStart(14);
strTrades = subtotalTrades ? (' ' + subtotalTrades + ' trade' + (subtotalTrades > 1 ? 's' : ' ')).padStart(13) : '';
console.log(`Subtotal[${j}]`.padEnd(14) + `= $${Beautify(subtotalProfits, 2).padEnd(14)} ${strProfit}${strDeltaModeProfits}${strTrades}`);
subtotalProfits = 0;
deltaSubtotalProfits = 0;
subtotalTrades = 0;
}
// Stats for totals
stockList.hourlyProfits = totalProfits * (stockerLoopFrequency / 60_000) * 3600_000 / (stockList.Uptime + 1);
stockList.dailyProfits = totalProfits * (stockerLoopFrequency / 60_000) * 86_400_000 / (stockList.Uptime + 1);
if (!stockerForceLoopUpdates) {
stockList.hourlyProfits *= 2;
stockList.dailyProfits *= 2;
}
console.log(`Total profits = $${Beautify(totalProfits, 2).padEnd(22)}${(deltaTotalProfits ? '$' + Beautify(deltaTotalProfits, 2) : '').padStart(15)}${totalTrades ? (' ' + totalTrades + ' trade' + (totalTrades > 1 ? 's' : ' ')).padStart(13) : ''}`);
console.log(`Profit per hour = $${Beautify(stockList.hourlyProfits, 2)}; profit per day = $${Beautify(stockList.dailyProfits, 2)}`);
console.log(`That's ${Beautify(stockList.hourlyProfits * Game.cookiesPsRawHighest, 2)} cookies and ${Beautify(stockList.dailyProfits * Game.cookiesPsRawHighest, 2)} cookies, respectively. It's also ${Beautify((stockList.hourlyProfits / 3600), 0)} times your highest raw cookie production rate.`);
if (stockerForceLoopUpdates) {
console.log('In unadjusted, true numbers:');
stockList.hourlyProfits *= 60_000 / stockerLoopFrequency;
stockList.dailyProfits *= 60_000 / stockerLoopFrequency;
console.log(`Profit per hour = $${Beautify(stockList.hourlyProfits, 2)}; profit per diem = $${Beautify(stockList.dailyProfits, 2)}`);
console.log(`That's ${Beautify(stockList.hourlyProfits * Game.cookiesPsRawHighest, 2)} cookies and ${Beautify(stockList.dailyProfits * Game.cookiesPsRawHighest, 2)} cookies, respectively. It's also ${Beautify((stockList.hourlyProfits / 3600), 0)} times your highest raw cookie production rate.`);
}
console.log('------------------------------------------------------------------');
}
};
CookiStocker.DataStats = function(id, value, dollars) {
let it = l(id);
it.innerHTML = (value < 0 ? "-" : "") + (dollars ? '$' : '') + Beautify(Math.abs(value), 0);
if (id === "Brokers" && CookiStocker.Bank.brokers < stockerMinBrokers)
value = -1;
else if (id === "bankedCookies") {
if (Game.cookies > stockList.minCookies && Game.cookies < stockList.maxCookies) {
it.classList.remove("green");
it.style.color = 'yellow';
return;
}
else if (Game.cookies < stockList.minCookies)
value = -1;
}
if (value > 0) {
it.classList.add("green");
it.style.color = '';
} else if (value < 0) {
it.classList.remove("green");
it.classList.remove("yellow");
it.style.color = '#ff3b3b';
}
}
CookiStocker.TradingStats = function()
{
if (typeof CookiStocker.Bank === 'undefined')
return;
let i, shares, cookies;
let now = Date.now();
let market = CookiStocker.Bank.goodsById;
if (now > stockList.lastTime + stockerActivityReportFrequency + 500) { // Were we sleeping?
stockList.Start += now - stockList.lastTime - stockerActivityReportFrequency;
}
stockList.totalStocks = 0;
stockList.totalShares = 0;
stockList.totalValue = 0;
stockList.unrealizedProfits = 0;
for (i = 0; i < market.length; i++) {
if (stockList.Goods[i].stock) {
stockList.totalStocks++;
stockList.totalShares += stockList.Goods[i].stock;
stockList.totalValue += stockList.Goods[i].stock * stockList.Goods[i].currentPrice;
stockList.unrealizedProfits += (market[i].val - market[i].prev) * stockList.Goods[i].stock;
}
}
stockList.minCookies = Number.MAX_VALUE;
stockList.maxCookies = 0;
for (i = 0; i < market.length; i++) {
shares = CookiStocker.Bank.getGoodMaxStock(market[i]) - market[i].stock;
cookies = shares * Game.cookiesPsRawHighest * market[i].val / stockerCookiesThreshold;
if (!stockList.minCookies || shares && cookies < stockList.minCookies)
stockList.minCookies = cookies;
if (shares && cookies > stockList.maxCookies)
stockList.maxCookies = cookies;
}
CookiStocker.DataStats("Brokers", CookiStocker.Bank.brokers, 0);
CookiStocker.DataStats("brokersNeeded", stockerMinBrokers, 0);
CookiStocker.DataStats("bankedCookies", Game.cookies, 0);
CookiStocker.DataStats("minCookies", stockList.minCookies, 0);
CookiStocker.DataStats("maxCookies", stockList.maxCookies, 0);
CookiStocker.DataStats("Profits", stockList.netProfits, 1);
CookiStocker.DataStats("profitsHour", stockList.hourlyProfits, 1);
CookiStocker.DataStats("profitsDay", stockList.dailyProfits, 1);
CookiStocker.DataStats("grossProfits", stockList.grossProfits, 1);
CookiStocker.DataStats("grossLosses", -stockList.grossLosses, 1);
stockList.lastTime = now;
stockList.Uptime = Math.floor((now - stockList.Start) / 1000) * 1000;
stockList.Uptime -= stockList.Uptime % stockerLoopFrequency;
let uptimeHours = Math.floor(stockList.Uptime / 3600000);
let uptimeDays = Math.floor(uptimeHours / 24);
if (uptimeDays >= 1) {
uptimeDays += ':';
uptimeHours %= 24;
if (uptimeHours < 10)
uptimeHours = '0' + uptimeHours;
} else
uptimeDays = '';
let it = l("runTime");
it.innerHTML = uptimeDays + uptimeHours + ':';
if (stockerForceLoopUpdates) {
it.innerHTML += new Date(stockList.Uptime).toLocaleTimeString([], {minute: '2-digit', second: '2-digit'});
} else {
let uptimeMinutes = (Math.floor(stockList.Uptime / 60000)) % 60;
it.innerHTML += (uptimeMinutes < 10 ? '0' : '') + uptimeMinutes;
}
if (stockerAdditionalTradingStats) {
CookiStocker.DataStats("netCookies", stockList.netProfits * Game.cookiesPsRawHighest, 0);
CookiStocker.DataStats("cookiesHour", stockList.hourlyProfits * Game.cookiesPsRawHighest, 0);
CookiStocker.DataStats("cookiesDay", stockList.dailyProfits * Game.cookiesPsRawHighest, 0);
l("Purchases").innerHTML = stockList.Purchases;
l("Sales").innerHTML = stockList.Sales;
l("cpsMultiple").innerHTML = stockList.hourlyProfits >= 0 ? Beautify(stockList.hourlyProfits / 3600, 3) : -Beautify(-stockList.hourlyProfits / 3600, 3);
l("stocksHeld").innerHTML = stockList.totalStocks;
l("totalShares").innerHTML = Beautify(stockList.totalShares);
CookiStocker.DataStats("totalValue", stockList.totalValue, 1);
CookiStocker.DataStats("unrealizedProfits", stockList.unrealizedProfits, 1);
l("profitableStocks").innerHTML = stockList.profitableStocks;
l("unprofitableStocks").innerHTML = stockList.unprofitableStocks
l("profitableTrades").innerHTML = stockList.profitableTrades;
l("unprofitableTrades").innerHTML = stockList.unprofitableTrades;
CookiStocker.DataStats("averageProfit", stockList.profitableTrades ? stockList.grossProfits / stockList.profitableTrades : 0, 1);
CookiStocker.DataStats("averageLoss", stockList.unprofitableTrades ? -stockList.grossLosses / stockList.unprofitableTrades : 0, 1);
}
if (it.innerHTML == '')
it.innerHTML = "0:00";
CookiStocker.updateWarn();
}
CookiStocker.updateWarn = function() {
let warn = l('stockerWarnLine');
let warn2 = l('stockerWarnLine2');
let warn3 = l('stockerWarnLine3');
// Hide all first
if (warn) warn.style.display = 'none';
if (warn2) warn2.style.display = 'none';
if (warn3) warn3.style.display = 'none';
if (!stockerResourcesWarning) {
return;
}
warn3= l('stockerWarnLine3');
if (warn3 && !stockerMarketOn) {
warn3.style.display = '';
return;
}
warn2 = l('stockerWarnLine2');
if (warn2 && !stockerAutoTrading) {
warn2.style.display = '';
return;
}
warn = l('stockerWarnLine');
if (!warn) return;
// Insufficient if we’re short on brokers or short on banked cookies for a full lot
if (CookiStocker.Bank.brokers < stockerMinBrokers) {
warn.style.display = '';
return;
}
let market = CookiStocker.Bank.goodsById; // update market
for (let i = 0; i < market.length; i++)
if ((CookiStocker.Bank.getGoodMaxStock(market[i]) - market[i].stock) * Game.cookiesPsRawHighest * market[i].val >= Game.cookies * stockerCookiesThreshold) {
warn.style.display = '';
return;
}
warn.style.display = 'none';
};
// ===== CookiStocker options UI & persistence =====
/** Mirror of boolean prefs for CCSE.ToggleButton to read/write.
We keep the *real* vars exactly where they are (top of file); we just sync them here. */
CookiStocker.state = {
stockerAutoTrading: +!!stockerAutoTrading,
stockerMarketOn: +!!stockerMarketOn,
stockerAutoBuyBrokers: +!!stockerAutoBuyBrokers,
stockerResourcesWarning: +!!stockerResourcesWarning,
stockerExponential: +!!stockerExponential,
stockerTransactionNotifications:+!!stockerTransactionNotifications,
stockerActivityReport: +!!stockerActivityReport,
stockerFastNotifications: +!!stockerFastNotifications,
stockerConsoleAnnouncements: +!!stockerConsoleAnnouncements,
stockerAdditionalTradingStats: +!!stockerAdditionalTradingStats,
stockerForceLoopUpdates: +!!stockerForceLoopUpdates
};
// Toggle handler (signature matches CCSE.MenuHelper.ToggleButton)
CookiStocker.Toggle = function(prefName, button, on, off, invert) {
// Flip state bit
CookiStocker.state[prefName] = CookiStocker.state[prefName] ? 0 : 1;
// Update button text/class
l(button).innerHTML = CookiStocker.state[prefName] ? on : off;
l(button).className = 'smallFancyButton prefButton option' + ((CookiStocker.state[prefName]^invert) ? '' : ' off');
// Write through to the *real* globals
switch (prefName) {
case 'stockerAutoTrading': stockerAutoTrading = !!CookiStocker.state[prefName]; CookiStocker.updateWarn(); break;
case 'stockerMarketOn': stockerMarketOn = !!CookiStocker.state[prefName];
CookiStocker.updateWarn();
CookiStocker.ensureReportTimer();
break;
case 'stockerAutoBuyBrokers': stockerAutoBuyBrokers = !!CookiStocker.state[prefName]; CookiStocker.TradingStats(); break;
case 'stockerResourcesWarning': stockerResourcesWarning = !!CookiStocker.state[prefName]; CookiStocker.updateWarn(); break;
case 'stockerExponential': stockerExponential = !!CookiStocker.state[prefName]; break;
case 'stockerTransactionNotifications': stockerTransactionNotifications = !!CookiStocker.state[prefName]; break;
case 'stockerActivityReport': stockerActivityReport = !!CookiStocker.state[prefName]; CookiStocker.ensureReportTimer(); break;
case 'stockerFastNotifications': stockerFastNotifications = !!CookiStocker.state[prefName]; break;
case 'stockerConsoleAnnouncements': stockerConsoleAnnouncements = !!CookiStocker.state[prefName]; CookiStocker.ensureReportTimer(); break;
case 'stockerAdditionalTradingStats': stockerAdditionalTradingStats = !!CookiStocker.state[prefName];
CookiStocker.updateAdditionalStatsVisibility();
break;
case 'stockerForceLoopUpdates': stockerForceLoopUpdates = !!CookiStocker.state[prefName]; break;
}
PlaySound('snd/tick.mp3');
Game.UpdateMenu();
};
// Commission = 20% * 0.95^brokers
CookiStocker.calcCommission = function(n) {
const rate = 0.20 * Math.pow(0.95, Math.max(0, Math.min(162, +n || 0)));
return (rate * 100).toFixed(3) + "%";
};
// Minutes:seconds → ms writer for the two time prefs
CookiStocker.ChangeTime = function(prefName, minId, secId) {
let mins = Math.max(0, Math.floor(+l(minId).value || 0));
let secs = Math.max(0, Math.min(59, Math.floor(+l(secId).value || 0)));
let ms = (mins * 60 + secs) * 1000;
switch (prefName) {
case 'stockerActivityReportFrequency':
stockerActivityReportFrequency = ms;
if (CookiStocker.reportTimer) {
clearInterval(CookiStocker.reportTimer);
CookiStocker.reportTimer = null;
}
CookiStocker.ensureReportTimer();
break;
case 'stockerLoopFrequency':
stockerLoopFrequency = ms;
if (stockerForceLoopUpdates && CookiStocker.Bank && CookiStocker.Bank.secondsPerTick) {
CookiStocker.Bank.secondsPerTick = Math.max(0.001, stockerLoopFrequency / 1000);
}
break;
}
PlaySound('snd/tick.mp3');
};
CookiStocker.ChangeNumber = function(prefName, val) {
let v = Math.max(0, Math.floor(+val || 0));
switch (prefName) {
case 'stockerMinBrokers':
stockerMinBrokers = v;
break;
case 'stockerActivityReportFrequency':
stockerActivityReportFrequency = v;
if (CookiStocker.reportTimer) {
clearInterval(CookiStocker.reportTimer);
CookiStocker.reportTimer = null;
}
CookiStocker.ensureReportTimer();
break;
case 'stockerLoopFrequency':
stockerLoopFrequency = v;
if (stockerForceLoopUpdates && CookiStocker.Bank && CookiStocker.Bank.secondsPerTick) {
CookiStocker.Bank.secondsPerTick = Math.max(0.001, stockerLoopFrequency / 1000);
}
break;
}
PlaySound('snd/tick.mp3');
};
// Build the Options menu body (uses CCSE.MenuHelper like Cookie Assistant)
CookiStocker.getMenuString = function() {
const minutes = (stockerActivityReportFrequency || 0) / 60000; // may be fractional
const loopSeconds = Math.floor((stockerLoopFrequency || 0) / 1000);
var m = CCSE.MenuHelper;
let str = '';
str += '';
// ---------- Automation ----------
str += m.Header('Automation');
// Auto trading
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerAutoTrading','CS_autoTrading','Auto Trading ON','Auto Trading OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerAutoTrading', false)
+ '
';
// Turns the entire stock market on or off.
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerMarketOn','CS_market','Stock Market ON','Stock Market OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerMarketOn', false)
+ '
';
// Auto-buy brokers
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerAutoBuyBrokers','CS_autoBuyBrokers','Auto-buy Brokers ON','Auto-buy Brokers OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerAutoBuyBrokers', false)
+ '
';
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerResourcesWarning','CS_resourcesWarning','Resources Warning ON','Resources Warning OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerResourcesWarning', false)
+ '
';
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerExponential','CS_Exponential','Exponential Warehouses ON','Exponential Warehouses OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerExponential', false)
+ '
';
// ---Exponential multiplier slider (CCSE helper, right-hand live value)
var cbWarehouseExponent = "stockerExponentialPower = (l('exponentSlider').value);"
+ " l('exponentSliderRightText').textContent = stockerExponentialPower;"
+ " CookiStocker.TradingStats();";
str += '
'
+ CCSE.MenuHelper.Slider(
'exponentSlider', 'Warehouse Exponent', ''+stockerExponentialPower+' ', () => stockerExponentialPower, cbWarehouseExponent, 0.1, 3.0, 0.1
)
+ ' ' + CookiStocker.note('stockerExponentialPower', false)
+ '
';
// --- Min brokers slider (CCSE helper, right-hand live value)
var cbMinBrokers = "stockerMinBrokers = Math.round(l('minBrokersSlider').value);"
+ " l('minBrokersSliderRightText').textContent = stockerMinBrokers;"
+ " l('CS_commissionVal').textContent = CookiStocker.calcCommission(stockerMinBrokers);"
+ " CookiStocker.TradingStats();";
str += '
'
+ CCSE.MenuHelper.Slider(
'minBrokersSlider', 'Minimum Brokers', '', () => stockerMinBrokers, cbMinBrokers, 0, 162, 1
)
+ ' (Commission: '
+ CookiStocker.calcCommission(stockerMinBrokers)
+ ' ) '
+ CookiStocker.note('stockerMinBrokers', false)
+ '
';
// --- Bank percentage allowed per purchase (CCSE helper; shows %; stores fraction)
var stockerCookiesPercent = Math.round((stockerCookiesThreshold || 0) * 100);
var cbCookies = "var v = Math.round(l('cookiesPercentSlider').value);"
+ " stockerCookiesPercent = v;"
+ " stockerCookiesThreshold = v / 100;"
+ " l('cookiesPercentSliderRightText').textContent = v + '%';"
+ " CookiStocker.TradingStats();";
str += '
'
+ CCSE.MenuHelper.Slider('cookiesPercentSlider', 'Max Bank % per Purchase', '' + stockerCookiesPercent+'% ', () => stockerCookiesPercent, cbCookies, 1, 100, 1)
+ ' ' + CookiStocker.note('stockerCookiesThreshold', false)
+ '
';
// ---------- Reporting & Notifications ----------
str += m.Header('Reporting & Notifications');
// Transaction notifs
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerTransactionNotifications','CS_txNotifs','TX Notifications ON','TX Notifications OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerTransactionNotifications', false)
+ '
';
// Fast notifs
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerFastNotifications','CS_fastNotifs','Fast Notifications ON','Fast Notifications OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerFastNotifications', false)
+ '
';
// Console announcements
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerConsoleAnnouncements','CS_console','Console Announce ON','Console Announce OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerConsoleAnnouncements', false)
+ '
';
// --- Activity report interval: mm:ss
var _arMin = Math.floor(stockerActivityReportFrequency / 60000);
var _arSec = Math.floor((stockerActivityReportFrequency % 60000) / 1000);
str += '
'
+ 'Report interval: '
+ ' : '
+ ' '
+ 'mm:ss '
+ '
';
// Extra stats
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerAdditionalTradingStats','CS_moreStats','Extra Trading Stats ON','Extra Trading Stats OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerAdditionalTradingStats', false)
+ '
';
// ---------- Timing (Advanced) ----------
str += m.Header('Timing (Advanced)');
// Loop frequency (UI shows SECONDS; integers; right-justified ~5 digits)
str += '
'
+ 'Loop (seconds): '
+ ' '
+ CookiStocker.note('stockerLoopFrequency', true)
+ '
';
// Force loop (cheat)
str += '
'
+ m.ToggleButton(CookiStocker.state,'stockerForceLoopUpdates','CS_forceLoop','Force Loop (cheat) ON','Force Loop (cheat) OFF',"CookiStocker.Toggle")
+ CookiStocker.note('stockerForceLoopUpdates', true)
+ '
';
str += '
';
return str;
};
// ---------- Inline option comments (no tooltips) ----------
CookiStocker.esc = function(s){
return String(s).replace(/&/g,'&').replace(//g,'>')
.replace(/'/g,''').replace(/"/g,'"');
};
// Texts copied from the comments above your variable defs
CookiStocker.docs = {
stockerAutoTrading: "Automatic trading and broker buying when on",
stockerMarketOn: "Stock market is running when on",
stockerMinBrokers: "Minimum number of brokers required for automatic trading",
stockerCookiesThreshold: "Percentage of banked cookies allowed for a single automatic trade",
stockerAutoBuyBrokers: "Buy all necessary brokers as soon as we can afford them",
stockerResourcesWarning: "Display warning when market conditions and/or options do not permit auto trading",
stockerExponential: "Increases number of warehouses in sync with the highest raw CPS during this session",
stockerExponentialPower: "The ratio of the highest raw CPS to the original raw CPS is raised to this power when Exponential Warehouses is on",
stockerTransactionNotifications: "Announce transactions in game notifications",
stockerActivityReport: "Make regular profit reports",
stockerActivityReportFrequency: "How often to make regular reports (minutes and seconds)",
stockerFastNotifications: "Make game notifications fade away on their own after 6 seconds",
stockerConsoleAnnouncements: "Use console.log for more detailed info on prices and trends",
stockerAdditionalTradingStats: "Display more detailed trading info near the top of the stock market display",
stockerLoopFrequency: "Logic loop frequency (seconds) — CHEAT",
stockerForceLoopUpdates: "The cheat itself. Rolls the cycle every time logic loop triggers — CHEAT"
};
// Render end-of-line text (red if cheat)
CookiStocker.note = function(key, cheat){
const t = CookiStocker.esc(CookiStocker.docs[key] || "");
return t ? (' '+t+' ') : '';
};
// Commission = 20% * 0.95^brokers → show as percentage
CookiStocker.calcCommission = function(n){
const rate = 0.20 * Math.pow(0.95, Math.max(0, Math.min(162, +n||0)));
return (rate*100).toFixed(3) + "%";
};
CookiStocker.ReplaceGameMenu = function()
{
Game.customOptionsMenu.push(function()
{
// Build a real node from the HTML string
const content = document.createElement('div');
content.innerHTML = CookiStocker.getMenuString();
// CCSE expects a Node here, not a string
CCSE.AppendCollapsibleOptionsMenu(CookiStocker.name, content);
});
Game.customStatsMenu.push(function() {
CCSE.AppendStatsVersionNumber(CookiStocker.name, CookiStocker.version);
if (!CookiStocker.Bank || !CookiStocker.Bank.goodsById) return;
// example rollup; adjust to taste
var p = CookiStocker.Bank.profit;
var held = CookiStocker.Bank.goodsById.reduce((a,g)=>a+g.stock,0);
var worth = CookiStocker.Bank.goodsById.reduce((a,g)=>a+g.stock * g.val * Game.cookiesPsRawHighest,0);
CCSE.AppendStatsGeneral('Stock Market has earned you : $' + Beautify(p) + ' (' + Game.tinyCookie() + Beautify(p * Game.cookiesPsRawHighest) + ' cookies)
');
/* CCSE.AppendStatsGeneral(
'CookiStocker
'
+ 'Net profits: $' + Beautify(p, 2) + '
'
+ 'Total shares held: ' + Beautify(held) + '
'
+ 'Portfolio (at current prices): $' + Beautify(worth,2) + '
'
);
*/ });
}
/*
function sleepSync(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {} // busy-wait
}
*/
CookiStocker.save = function() {
var str = '';
if (typeof CookiStocker.Bank === 'undefined')
return '';
let market = CookiStocker.Bank.goodsById;
str += Number(stockList.Check);
for (let i = 0; i < market.length; i++) {
str += '_' + encodeURIComponent(stockList.Goods[i].name);
str += '_' + Number(stockList.Goods[i].stock);
str += '_' + Number(stockList.Goods[i].val);
str += '_' + Number(stockList.Goods[i].currentPrice);
str += '_' + Number(stockList.Goods[i].mode);
str += '_' + Number(stockList.Goods[i].lastMode);
str += '_' + Number(stockList.Goods[i].lastDur);
str += '_' + Number(stockList.Goods[i].unchangedDur);
str += '_' + Number(stockList.Goods[i].dropCount);
str += '_' + Number(stockList.Goods[i].riseCount);
str += '_' + Number(stockList.Goods[i].profit);
str += '_' + (+!!stockList.Goods[i].someSold);
str += '_' + (+!!stockList.Goods[i].someBought);
}
str += '_' + Number(stockList.Start);
str += '_' + Number(stockList.lastTime);
str += '_' + Number(stockList.startingProfits);
str += '_' + Number(stockList.Profits);
str += '_' + Number(stockList.netProfits);
str += '_' + Number(stockList.grossProfits);
str += '_' + Number(stockList.grossLosses);
str += '_' + Number(stockList.totalStocks);
str += '_' + Number(stockList.totalShares);
str += '_' + Number(stockList.totalValue);
str += '_' + Number(stockList.unrealizedProfits);
str += '_' + Number(stockList.profitableStocks);
str += '_' + Number(stockList.unprofitableStocks);
str += '_' + Number(stockList.profitableTrades);
str += '_' + Number(stockList.unprofitableTrades);
str += '_' + Number(stockList.Purchases);
str += '_' + Number(stockList.Sales);
str += '_' + Number(stockList.Uptime);
str += '_' + Number(stockList.hourlyProfits);
str += '_' + Number(stockList.dailyProfits);
str += '_' + Number(stockList.minCookies);
str += '_' + Number(stockList.maxCookies);
str += '_' + (+!!stockList.noModActions);
str += '_' + Number(stockList.origCookiesPsRawHighest);
for (i = 0; i < stockerModeProfits.length; i++)
for (j = 0; j < stockerModeProfits[i].length; j++)
for (k = 0; k < stockerModeProfits[i][j].length; k++)
str += '_' + Number(stockerModeProfits[i][j][k]);
str += '_' + Number(Game.Achievements['Plasmic assets'].won);
str += '_' + Number(Game.Achievements['Bose-Einstein Condensed Assets'].won);
// Append options tail (backward‑compatible)
const cfg = {
stockerAutoTrading,
stockerMinBrokers,
stockerAutoBuyBrokers,
stockerTransactionNotifications,
stockerActivityReport,
stockerActivityReportFrequency,
stockerFastNotifications,
stockerConsoleAnnouncements,
stockerAdditionalTradingStats,
stockerLoopFrequency,
stockerForceLoopUpdates,
stockerCookiesThreshold,
stockerResourcesWarning,
stockerMarketOn,
stockerExponential,
stockerExponentialPower,
};
str += '|CFG:' + JSON.stringify(cfg);
return str;
}
CookiStocker.load = function(str) {
let i = 0;
let j, k, m;
let spl = str.split('_');
if (typeof CookiStocker.Bank === 'undefined' || !str || !(stockList.Goods[0].name.length > 0))
return false;
let market = CookiStocker.Bank.goodsById;
// Split optional config tail (if present)
let cfgIdx = (str||'').indexOf('|CFG:');
let cfg = null;
if (cfgIdx > -1) {
cfg = JSON.parse(str.slice(cfgIdx + 5));
str = str.slice(0, cfgIdx); // strip tail before underscore parsing
}
stockList.Check = Number(spl[i++] || 0);
for (j = 0; j < market.length; j++) {
stockList.Goods[j].name = decodeURIComponent(spl[i++] || 0);
stockList.Goods[j].stock = Number(spl[i++] || 0);
stockList.Goods[j].val = Number(spl[i++] || 0);
stockList.Goods[j].currentPrice = Number(spl[i++] || 0);
stockList.Goods[j].mode = Number(spl[i++] || 0);
stockList.Goods[j].lastMode = Number(spl[i++] || 0);
stockList.Goods[j].lastDur = Number(spl[i++] || 0);
stockList.Goods[j].unchangedDur = Number(spl[i++] || 0);
stockList.Goods[j].dropCount = Number(spl[i++] || 0);
stockList.Goods[j].riseCount = Number(spl[i++] || 0);
stockList.Goods[j].profit = Number(spl[i++] || 0);
stockList.Goods[j].someSold = !!(+spl[i++] || 0);
stockList.Goods[j].someBought = !!(+spl[i++] || 0);
}
stockList.Start = Number(spl[i++] || 0);
stockList.lastTime = Number(spl[i++] || 0);
stockList.startingProfits = Number(spl[i++] || 0);
stockList.Profits = Number(spl[i++] || 0);
stockList.netProfits = Number(spl[i++] || 0);
stockList.grossProfits = Number(spl[i++] || 0);
stockList.grossLosses = Number(spl[i++] || 0);
stockList.totalStocks = Number(spl[i++] || 0);
stockList.totalShares = Number(spl[i++] || 0);
stockList.totalValue = Number(spl[i++] || 0);
stockList.unrealizedProfits = Number(spl[i++] || 0);
stockList.profitableStocks = Number(spl[i++] || 0);
stockList.unprofitableStocks = Number(spl[i++] || 0);
stockList.profitableTrades = Number(spl[i++] || 0);
stockList.unprofitableTrades = Number(spl[i++] || 0);
stockList.Purchases = Number(spl[i++] || 0);
stockList.Sales = Number(spl[i++] || 0);
stockList.Uptime = Number(spl[i++] || 0);
stockList.hourlyProfits = Number(spl[i++] || 0);
stockList.dailyProfits = Number(spl[i++] || 0);
stockList.minCookies = Number(spl[i++] || Number.MAX_VALUE);
stockList.maxCookies = Number(spl[i++] || 0);
stockList.noModActions = !!(+spl[i++] || 0);
stockList.origCookiesPsRawHighest = Number(spl[i++] || 0);
for (j = 0; j < stockerModeProfits.length; j++)
for (k = 0; k < stockerModeProfits[j].length; k++)
for (m = 0; m < stockerModeProfits[j][k].length; m++)
stockerModeProfits[j]