Add stonks CLI (Finnhub), portfolio tracker, morning briefing + weekly review crons
This commit is contained in:
parent
956709f739
commit
9630edc4d4
6 changed files with 484 additions and 21 deletions
43
TOOLS.md
43
TOOLS.md
|
|
@ -220,4 +220,47 @@ tasks recurring
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Finnhub Stock Market API
|
||||||
|
|
||||||
|
Helper script: `~/.openclaw/workspace/bin/stonks`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stonks quote <symbol> # Current price
|
||||||
|
stonks search <query> # Search for symbols
|
||||||
|
stonks candles <symbol> [days] # Price history (default: 30)
|
||||||
|
stonks news <symbol> [days] # Company news (default: 7)
|
||||||
|
stonks profile <symbol> # Company info
|
||||||
|
stonks portfolio # Show virtual portfolio
|
||||||
|
stonks buy <symbol> <amount> # Virtual buy (EUR)
|
||||||
|
stonks sell <symbol> [amount] # Virtual sell (EUR or all)
|
||||||
|
stonks limit-buy <sym> <price> <amt> # Limit buy order
|
||||||
|
stonks limit-sell <sym> <price> [amt] # Limit sell order
|
||||||
|
stonks check-limits # Check & execute triggered limits
|
||||||
|
stonks history # Trade history
|
||||||
|
```
|
||||||
|
|
||||||
|
- Credentials: `.credentials/finnhub.env`
|
||||||
|
- Portfolio data: `memory/portfolio.json`
|
||||||
|
- Virtual starting capital: €1,000
|
||||||
|
- **N26 constraint:** Only trade instruments available on N26 (check with user)
|
||||||
|
- Symbols: Use Finnhub format (e.g., `RHM.DE` for Xetra, `AAPL` for NASDAQ)
|
||||||
|
|
||||||
|
**Portfolio management:**
|
||||||
|
- Check limits during morning briefing and heartbeats
|
||||||
|
- Include portfolio status in morning briefing and weekly review
|
||||||
|
- Be proactive about market-moving news affecting positions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scheduled Briefings
|
||||||
|
|
||||||
|
| Briefing | Schedule | Content |
|
||||||
|
|----------|----------|---------|
|
||||||
|
| Morning Briefing | Daily 9:00 Vienna | Calendar, weather, tasks, portfolio, news |
|
||||||
|
| Weekly Review | Sunday 18:00 Vienna | Week recap, open tasks, portfolio performance, next week preview |
|
||||||
|
| News (Der Standard) | 10:00, 14:00, 18:00, 22:00 Vienna | Austrian/international news |
|
||||||
|
| News (AI) | 10:00, 14:00, 18:00, 22:00 Vienna | AI/tech news |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Add whatever helps you do your job. This is your cheat sheet.
|
Add whatever helps you do your job. This is your cheat sheet.
|
||||||
|
|
|
||||||
396
bin/stonks
Executable file
396
bin/stonks
Executable file
|
|
@ -0,0 +1,396 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Finnhub stock market CLI tool
|
||||||
|
// Usage: stonks quote <symbol> — current price
|
||||||
|
// stonks search <query> — search for symbols
|
||||||
|
// stonks candles <symbol> [days] — price history (default: 30 days)
|
||||||
|
// stonks news <symbol> [days] — company news (default: 7 days)
|
||||||
|
// stonks profile <symbol> — company profile
|
||||||
|
// stonks portfolio — show virtual portfolio
|
||||||
|
// stonks buy <symbol> <amount> — virtual buy (amount in EUR)
|
||||||
|
// stonks sell <symbol> [amount] — virtual sell (amount in EUR, default: all)
|
||||||
|
// stonks limit-buy <symbol> <price> <amount> — set limit buy
|
||||||
|
// stonks limit-sell <symbol> <price> [amount] — set limit sell
|
||||||
|
// stonks check-limits — check & execute triggered limit orders
|
||||||
|
// stonks history — order history
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Load API key
|
||||||
|
const envPath = path.join(__dirname, '..', '.credentials', 'finnhub.env');
|
||||||
|
let API_KEY = '';
|
||||||
|
try {
|
||||||
|
const env = fs.readFileSync(envPath, 'utf8');
|
||||||
|
const match = env.match(/FINNHUB_API_KEY=(.+)/);
|
||||||
|
if (match) API_KEY = match[1].trim();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (!API_KEY || API_KEY === 'FILL_IN') {
|
||||||
|
console.error('ERROR: Set FINNHUB_API_KEY in .credentials/finnhub.env');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PORTFOLIO_PATH = path.join(__dirname, '..', 'memory', 'portfolio.json');
|
||||||
|
|
||||||
|
function loadPortfolio() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(PORTFOLIO_PATH, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
const initial = {
|
||||||
|
startingCapital: 1000,
|
||||||
|
cash: 1000,
|
||||||
|
currency: 'EUR',
|
||||||
|
positions: [],
|
||||||
|
limitOrders: [],
|
||||||
|
history: [],
|
||||||
|
created: new Date().toISOString()
|
||||||
|
};
|
||||||
|
fs.writeFileSync(PORTFOLIO_PATH, JSON.stringify(initial, null, 2));
|
||||||
|
return initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePortfolio(p) {
|
||||||
|
fs.writeFileSync(PORTFOLIO_PATH, JSON.stringify(p, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function finnhub(endpoint) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = `https://finnhub.io/api/v1${endpoint}&token=${API_KEY}`;
|
||||||
|
https.get(url, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (c) => data += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve(JSON.parse(data)); }
|
||||||
|
catch (e) { reject(new Error(`Parse error: ${data.slice(0, 200)}`)); }
|
||||||
|
});
|
||||||
|
}).on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quote(symbol) {
|
||||||
|
const q = await finnhub(`/quote?symbol=${encodeURIComponent(symbol)}`);
|
||||||
|
if (!q || q.c === 0) {
|
||||||
|
console.error(`No data for ${symbol}. Try search to find the right symbol.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const change = q.d || 0;
|
||||||
|
const changePct = q.dp || 0;
|
||||||
|
console.log(`${symbol}\t${q.c}\t${change >= 0 ? '+' : ''}${change}\t${changePct >= 0 ? '+' : ''}${changePct}%\tH:${q.h}\tL:${q.l}\tO:${q.o}\tPC:${q.pc}`);
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search(query) {
|
||||||
|
const r = await finnhub(`/search?q=${encodeURIComponent(query)}`);
|
||||||
|
if (!r || !r.result || r.result.length === 0) {
|
||||||
|
console.log('No results found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Show top 15 results
|
||||||
|
r.result.slice(0, 15).forEach(s => {
|
||||||
|
console.log(`${s.symbol}\t${s.description}\t${s.type}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function candles(symbol, days = 30) {
|
||||||
|
const to = Math.floor(Date.now() / 1000);
|
||||||
|
const from = to - (days * 86400);
|
||||||
|
const r = await finnhub(`/stock/candle?symbol=${encodeURIComponent(symbol)}&resolution=D&from=${from}&to=${to}`);
|
||||||
|
if (!r || r.s === 'no_data') {
|
||||||
|
console.log(`No candle data for ${symbol}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Date\tOpen\tHigh\tLow\tClose\tVolume');
|
||||||
|
for (let i = 0; i < r.t.length; i++) {
|
||||||
|
const d = new Date(r.t[i] * 1000).toISOString().split('T')[0];
|
||||||
|
console.log(`${d}\t${r.o[i]}\t${r.h[i]}\t${r.l[i]}\t${r.c[i]}\t${r.v[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function news(symbol, days = 7) {
|
||||||
|
const to = new Date().toISOString().split('T')[0];
|
||||||
|
const from = new Date(Date.now() - days * 86400000).toISOString().split('T')[0];
|
||||||
|
const r = await finnhub(`/company-news?symbol=${encodeURIComponent(symbol)}&from=${from}&to=${to}`);
|
||||||
|
if (!r || r.length === 0) {
|
||||||
|
console.log('No news found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r.slice(0, 10).forEach(n => {
|
||||||
|
const d = new Date(n.datetime * 1000).toISOString().split('T')[0];
|
||||||
|
console.log(`${d}\t${n.headline}\t${n.url}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function profile(symbol) {
|
||||||
|
const r = await finnhub(`/stock/profile2?symbol=${encodeURIComponent(symbol)}`);
|
||||||
|
if (!r || !r.name) {
|
||||||
|
console.log(`No profile for ${symbol}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Name:\t${r.name}`);
|
||||||
|
console.log(`Country:\t${r.country}`);
|
||||||
|
console.log(`Exchange:\t${r.exchange}`);
|
||||||
|
console.log(`Industry:\t${r.finnhubIndustry}`);
|
||||||
|
console.log(`Market Cap:\t${r.marketCapitalization}M`);
|
||||||
|
console.log(`IPO:\t${r.ipo}`);
|
||||||
|
console.log(`URL:\t${r.weburl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showPortfolio() {
|
||||||
|
const p = loadPortfolio();
|
||||||
|
let totalValue = p.cash;
|
||||||
|
|
||||||
|
console.log(`=== Hoid's Virtual Portfolio ===`);
|
||||||
|
console.log(`Starting Capital:\t€${p.startingCapital.toFixed(2)}`);
|
||||||
|
console.log(`Cash:\t€${p.cash.toFixed(2)}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (p.positions.length > 0) {
|
||||||
|
console.log('Symbol\tShares\tAvg Price\tCurrent\tValue\tP&L\tP&L%');
|
||||||
|
for (const pos of p.positions) {
|
||||||
|
try {
|
||||||
|
const q = await finnhub(`/quote?symbol=${encodeURIComponent(pos.symbol)}`);
|
||||||
|
const currentPrice = q.c || pos.avgPrice;
|
||||||
|
const value = pos.shares * currentPrice;
|
||||||
|
const cost = pos.shares * pos.avgPrice;
|
||||||
|
const pnl = value - cost;
|
||||||
|
const pnlPct = ((pnl / cost) * 100);
|
||||||
|
totalValue += value;
|
||||||
|
console.log(`${pos.symbol}\t${pos.shares.toFixed(4)}\t${pos.avgPrice.toFixed(2)}\t${currentPrice.toFixed(2)}\t€${value.toFixed(2)}\t${pnl >= 0 ? '+' : ''}€${pnl.toFixed(2)}\t${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%`);
|
||||||
|
} catch (e) {
|
||||||
|
const value = pos.shares * pos.avgPrice;
|
||||||
|
totalValue += value;
|
||||||
|
console.log(`${pos.symbol}\t${pos.shares.toFixed(4)}\t${pos.avgPrice.toFixed(2)}\t???\t€${value.toFixed(2)}\t???\t???`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No positions.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
const totalPnl = totalValue - p.startingCapital;
|
||||||
|
const totalPnlPct = ((totalPnl / p.startingCapital) * 100);
|
||||||
|
console.log(`Total Value:\t€${totalValue.toFixed(2)}`);
|
||||||
|
console.log(`Total P&L:\t${totalPnl >= 0 ? '+' : ''}€${totalPnl.toFixed(2)} (${totalPnlPct >= 0 ? '+' : ''}${totalPnlPct.toFixed(1)}%)`);
|
||||||
|
|
||||||
|
if (p.limitOrders.length > 0) {
|
||||||
|
console.log('');
|
||||||
|
console.log('=== Pending Limit Orders ===');
|
||||||
|
console.log('ID\tType\tSymbol\tLimit Price\tAmount\tCreated');
|
||||||
|
p.limitOrders.forEach(o => {
|
||||||
|
console.log(`${o.id}\t${o.type}\t${o.symbol}\t${o.limitPrice}\t€${o.amount.toFixed(2)}\t${o.created.split('T')[0]}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buy(symbol, amount) {
|
||||||
|
const p = loadPortfolio();
|
||||||
|
amount = parseFloat(amount);
|
||||||
|
|
||||||
|
if (amount > p.cash) {
|
||||||
|
console.error(`Not enough cash. Have €${p.cash.toFixed(2)}, need €${amount.toFixed(2)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = await finnhub(`/quote?symbol=${encodeURIComponent(symbol)}`);
|
||||||
|
if (!q || q.c === 0) {
|
||||||
|
console.error(`Cannot get price for ${symbol}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = q.c;
|
||||||
|
const shares = amount / price;
|
||||||
|
|
||||||
|
// Update or create position
|
||||||
|
const existing = p.positions.find(pos => pos.symbol === symbol);
|
||||||
|
if (existing) {
|
||||||
|
const totalCost = (existing.shares * existing.avgPrice) + amount;
|
||||||
|
existing.shares += shares;
|
||||||
|
existing.avgPrice = totalCost / existing.shares;
|
||||||
|
} else {
|
||||||
|
p.positions.push({ symbol, shares, avgPrice: price });
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cash -= amount;
|
||||||
|
p.history.push({
|
||||||
|
type: 'BUY',
|
||||||
|
symbol,
|
||||||
|
shares: shares,
|
||||||
|
price,
|
||||||
|
amount,
|
||||||
|
date: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
savePortfolio(p);
|
||||||
|
console.log(`BUY\t${symbol}\t${shares.toFixed(4)} shares @ ${price}\t€${amount.toFixed(2)}`);
|
||||||
|
console.log(`Cash remaining:\t€${p.cash.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sell(symbol, amount) {
|
||||||
|
const p = loadPortfolio();
|
||||||
|
const pos = p.positions.find(pos => pos.symbol === symbol);
|
||||||
|
|
||||||
|
if (!pos) {
|
||||||
|
console.error(`No position in ${symbol}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = await finnhub(`/quote?symbol=${encodeURIComponent(symbol)}`);
|
||||||
|
if (!q || q.c === 0) {
|
||||||
|
console.error(`Cannot get price for ${symbol}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = q.c;
|
||||||
|
let sharesToSell;
|
||||||
|
|
||||||
|
if (!amount || amount === 'all') {
|
||||||
|
sharesToSell = pos.shares;
|
||||||
|
} else {
|
||||||
|
sharesToSell = parseFloat(amount) / price;
|
||||||
|
if (sharesToSell > pos.shares) sharesToSell = pos.shares;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proceeds = sharesToSell * price;
|
||||||
|
const cost = sharesToSell * pos.avgPrice;
|
||||||
|
const pnl = proceeds - cost;
|
||||||
|
|
||||||
|
pos.shares -= sharesToSell;
|
||||||
|
if (pos.shares < 0.0001) {
|
||||||
|
p.positions = p.positions.filter(pp => pp.symbol !== symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cash += proceeds;
|
||||||
|
p.history.push({
|
||||||
|
type: 'SELL',
|
||||||
|
symbol,
|
||||||
|
shares: sharesToSell,
|
||||||
|
price,
|
||||||
|
amount: proceeds,
|
||||||
|
pnl,
|
||||||
|
date: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
savePortfolio(p);
|
||||||
|
console.log(`SELL\t${symbol}\t${sharesToSell.toFixed(4)} shares @ ${price}\t€${proceeds.toFixed(2)}\tP&L: ${pnl >= 0 ? '+' : ''}€${pnl.toFixed(2)}`);
|
||||||
|
console.log(`Cash:\t€${p.cash.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function limitOrder(type, symbol, limitPrice, amount) {
|
||||||
|
const p = loadPortfolio();
|
||||||
|
limitPrice = parseFloat(limitPrice);
|
||||||
|
amount = amount ? parseFloat(amount) : null;
|
||||||
|
|
||||||
|
if (type === 'limit-buy' && amount > p.cash) {
|
||||||
|
console.error(`Not enough cash. Have €${p.cash.toFixed(2)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Math.random().toString(36).slice(2, 8);
|
||||||
|
p.limitOrders.push({
|
||||||
|
id,
|
||||||
|
type: type === 'limit-buy' ? 'BUY' : 'SELL',
|
||||||
|
symbol,
|
||||||
|
limitPrice,
|
||||||
|
amount: amount || 0,
|
||||||
|
created: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
savePortfolio(p);
|
||||||
|
console.log(`Limit ${type === 'limit-buy' ? 'BUY' : 'SELL'}\t${symbol}\t@ ${limitPrice}\t€${(amount || 0).toFixed(2)}\tID: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkLimits() {
|
||||||
|
const p = loadPortfolio();
|
||||||
|
if (p.limitOrders.length === 0) {
|
||||||
|
console.log('No pending limit orders.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggered = [];
|
||||||
|
const remaining = [];
|
||||||
|
|
||||||
|
for (const order of p.limitOrders) {
|
||||||
|
try {
|
||||||
|
const q = await finnhub(`/quote?symbol=${encodeURIComponent(order.symbol)}`);
|
||||||
|
const price = q.c;
|
||||||
|
|
||||||
|
if (order.type === 'BUY' && price <= order.limitPrice) {
|
||||||
|
triggered.push({ ...order, currentPrice: price });
|
||||||
|
} else if (order.type === 'SELL' && price >= order.limitPrice) {
|
||||||
|
triggered.push({ ...order, currentPrice: price });
|
||||||
|
} else {
|
||||||
|
remaining.push(order);
|
||||||
|
console.log(`PENDING\t${order.type}\t${order.symbol}\tLimit: ${order.limitPrice}\tCurrent: ${price}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
remaining.push(order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.limitOrders = remaining;
|
||||||
|
savePortfolio(p);
|
||||||
|
|
||||||
|
for (const order of triggered) {
|
||||||
|
console.log(`TRIGGERED\t${order.type}\t${order.symbol}\t@ ${order.currentPrice} (limit: ${order.limitPrice})`);
|
||||||
|
if (order.type === 'BUY') {
|
||||||
|
await buy(order.symbol, order.amount);
|
||||||
|
} else {
|
||||||
|
await sell(order.symbol, order.amount || 'all');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHistory() {
|
||||||
|
const p = loadPortfolio();
|
||||||
|
if (p.history.length === 0) {
|
||||||
|
console.log('No trades yet.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Date\tType\tSymbol\tShares\tPrice\tAmount\tP&L');
|
||||||
|
p.history.forEach(h => {
|
||||||
|
const pnl = h.pnl ? `${h.pnl >= 0 ? '+' : ''}€${h.pnl.toFixed(2)}` : '';
|
||||||
|
console.log(`${h.date.split('T')[0]}\t${h.type}\t${h.symbol}\t${h.shares.toFixed(4)}\t${h.price}\t€${h.amount.toFixed(2)}\t${pnl}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
const [,, cmd, ...args] = process.argv;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
switch (cmd) {
|
||||||
|
case 'quote': await quote(args[0]); break;
|
||||||
|
case 'search': await search(args.join(' ')); break;
|
||||||
|
case 'candles': await candles(args[0], args[1] || 30); break;
|
||||||
|
case 'news': await news(args[0], args[1] || 7); break;
|
||||||
|
case 'profile': await profile(args[0]); break;
|
||||||
|
case 'portfolio': await showPortfolio(); break;
|
||||||
|
case 'buy': await buy(args[0], args[1]); break;
|
||||||
|
case 'sell': await sell(args[0], args[1]); break;
|
||||||
|
case 'limit-buy': limitOrder('limit-buy', args[0], args[1], args[2]); break;
|
||||||
|
case 'limit-sell': limitOrder('limit-sell', args[0], args[1], args[2]); break;
|
||||||
|
case 'check-limits': await checkLimits(); break;
|
||||||
|
case 'history': showHistory(); break;
|
||||||
|
default:
|
||||||
|
console.log('Usage:');
|
||||||
|
console.log(' stonks quote <symbol> Current price');
|
||||||
|
console.log(' stonks search <query> Search symbols');
|
||||||
|
console.log(' stonks candles <symbol> [days] Price history');
|
||||||
|
console.log(' stonks news <symbol> [days] Company news');
|
||||||
|
console.log(' stonks profile <symbol> Company info');
|
||||||
|
console.log(' stonks portfolio Show portfolio');
|
||||||
|
console.log(' stonks buy <symbol> <amount> Buy (EUR amount)');
|
||||||
|
console.log(' stonks sell <symbol> [amount] Sell (EUR amount or all)');
|
||||||
|
console.log(' stonks limit-buy <sym> <price> <amt> Limit buy');
|
||||||
|
console.log(' stonks limit-sell <sym> <price> [amt] Limit sell');
|
||||||
|
console.log(' stonks check-limits Check triggered limits');
|
||||||
|
console.log(' stonks history Trade history');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error: ${e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
9
memory/bg3.json
Normal file
9
memory/bg3.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"party": [],
|
||||||
|
"level": 3,
|
||||||
|
"act": 1,
|
||||||
|
"currentQuest": "Rescue Halsin (goblin camp — too low level currently)",
|
||||||
|
"completedAreas": [],
|
||||||
|
"decisions": [],
|
||||||
|
"notes": "Suggested leveling in Emerald Grove, Blighted Village, Owlbear Cave, Risen Road before tackling goblin camp."
|
||||||
|
}
|
||||||
|
|
@ -8,12 +8,12 @@
|
||||||
},
|
},
|
||||||
"lastChecks": {
|
"lastChecks": {
|
||||||
"news": "2026-01-30T08:17:00Z",
|
"news": "2026-01-30T08:17:00Z",
|
||||||
"rheinmetall": "2026-02-07T08:41:00Z",
|
"rheinmetall": "2026-02-10T08:11:00Z",
|
||||||
"rheinmetall_price": 1587.00,
|
"rheinmetall_price": 1653.0,
|
||||||
"calendar": "2026-02-04T04:25:00Z",
|
"calendar": "2026-02-10T08:11:00Z",
|
||||||
"steam_hardware": "2026-02-03T22:00:00Z",
|
"steam_hardware": "2026-02-03T22:00:00Z",
|
||||||
"hamr_40tb": "2026-02-04T21:10:00Z",
|
"hamr_40tb": "2026-02-04T21:10:00Z",
|
||||||
"notes": "RHM: €1,902.00, below €1,950 threshold. Steam hardware: still 'early 2026', no official price/date. HAMR 40TB: expected mid-2026, no release date yet."
|
"notes": "RHM: \u20ac1,653.00, well below \u20ac1,950. Calendar: nextcloud creds missing, could not check."
|
||||||
},
|
},
|
||||||
"watchlist": {
|
"watchlist": {
|
||||||
"hamr_40tb": {
|
"hamr_40tb": {
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,25 @@
|
||||||
"context": "Monorepo is done, using GitHub actions as fallback for now"
|
"context": "Monorepo is done, using GitHub actions as fallback for now"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "af198211",
|
"id": "c0bfb64b",
|
||||||
"added": "2026-02-07",
|
"added": "2026-02-10",
|
||||||
"text": "Set up MCP server for Forgejo instance",
|
"text": "WoW bot with image recognition",
|
||||||
"priority": "soon",
|
"priority": "someday",
|
||||||
"context": "Phase 1: MCP server for interactive Claude web sessions (fork + PR workflow). Phase 2: Automated issue-solving pipeline with clarification comments.",
|
"context": "Look into programming a bot for World of Warcraft using image recognition. Old idea worth revisiting."
|
||||||
"lastNudged": "2026-02-07T14:11:07.284Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "0902aaba",
|
"id": "29f86057",
|
||||||
"added": "2026-02-07",
|
"added": "2026-02-10",
|
||||||
"text": "TYPO3 v13 upgrade for GBV",
|
"text": "Look into Showboat & Rodney (Simon Willison)",
|
||||||
|
"priority": "someday",
|
||||||
|
"context": "CLI tools for coding agents to prove their work. Showboat: agents build Markdown demos with real command outputs. Rodney: lightweight browser automation for agents to capture screenshots. Both designed for agent-first workflows. https://simonwillison.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "07569224",
|
||||||
|
"added": "2026-02-11",
|
||||||
|
"text": "Herman Miller Embody kaufen (chairgo.de)",
|
||||||
"priority": "soon",
|
"priority": "soon",
|
||||||
"lastNudged": "2026-02-07T14:11:07.284Z"
|
"context": "Ergonomischer Bürostuhl für Programmier-Setup. ~€1.800-2.000. Evtl. probesitzen in Wien vorher."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
{
|
{
|
||||||
"date": "2026-02-07",
|
"date": "2026-02-11",
|
||||||
"entries": [
|
"events": [
|
||||||
{"time": "19:14", "activity": "Tinkering — troubleshooting Bazzite suspend/resume + VRAM on GPD Win 4", "note": "Saturday evening, tech tinkering not deep work. Active conversation."},
|
{ "time": "19:00", "activity": "Playing BG3 (wind-down gaming)" },
|
||||||
{"time": "19:44", "activity": "Still GPD tinkering — fixed VRAM (8GB via UMAF), narrowed suspend crash to CPU hotplug (PowerTools core parking + SMT)", "note": "Making progress, having fun with it. Not work."},
|
{ "time": "20:11", "note": "Still gaming, quiet" },
|
||||||
{"time": "20:10", "activity": "Done tinkering — nosmt kernel param fixed suspend. Switching to YouTube then TV/audiobook", "note": "Good wind-down transition. Reminded about nose shower."}
|
{ "time": "20:43", "activity": "Defeated the hag in BG3, nose shower done ✅" },
|
||||||
]
|
{ "time": "20:44", "activity": "Watching something, maybe more BG3 after" },
|
||||||
|
{ "time": "20:56", "activity": "Researching inflation/investment calculations" },
|
||||||
|
{ "time": "21:00", "activity": "Reading news briefing" },
|
||||||
|
{ "time": "23:01", "activity": "Still playing BG3 (Illithid powers decision)" },
|
||||||
|
{ "time": "23:18", "activity": "Still gaming, asking about party comp" },
|
||||||
|
{ "time": "23:50", "activity": "Still gaming (Wyll question)" },
|
||||||
|
{ "time": "00:04", "activity": "Stopped gaming. Drinking tea + audiobook (Askir). Winding down ✅" },
|
||||||
|
{ "time": "00:10", "activity": "Going to sleep 🌙" }
|
||||||
|
],
|
||||||
|
"summary": "Good wind-down. Gamed BG3 until ~midnight, then tea+audiobook. Nose shower done. Asleep around 00:10. Gaming ran a bit late but transitioned well."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue