nothumanallowed 15.1.69 → 15.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "15.1.69",
3
+ "version": "15.1.70",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '15.1.69';
8
+ export const VERSION = '15.1.70';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -97,7 +97,7 @@ export const DESTRUCTIVE_ACTIONS = new Set([
97
97
  // Cron / scheduling
98
98
  'cron_create', 'cron_delete',
99
99
  // Portfolio (local file)
100
- 'portfolio_add', 'portfolio_remove',
100
+ 'portfolio_add', 'portfolio_remove', 'portfolio_tx_add',
101
101
  ]);
102
102
 
103
103
  // ── Tool Definitions (for system prompt) ─────────────────────────────────────
@@ -742,6 +742,38 @@ TOOLS:
742
742
  SEC Form 4 (insider/officer transactions) for a US-listed company. Returns last N filings with direct URLs.
743
743
  Open each URL to see whether the insider BOUGHT or SOLD shares — strong signal when clustered.
744
744
 
745
+ 107. option_strategy_builder(ticker: string, direction?: string, maxRisk?: number, daysToExpiry?: number)
746
+ Suggests concrete option strategies with strikes, expiry, net debit/credit, max profit/loss, breakeven, ROI.
747
+ direction: "bullish" | "bearish" | "neutral". Default: neutral. maxRisk: $ budget (default 1000). daysToExpiry: default 30.
748
+ Output adapts to ATM IV percentile vs 30d realized vol (cheap < 30th = buy premium; rich > 70th = sell premium):
749
+ - bullish+cheap → Bull Call Debit Spread - bullish+rich → Bull Put Credit Spread
750
+ - bearish+cheap → Bear Put Debit Spread - bearish+rich → Bear Call Credit Spread
751
+ - neutral+rich → Iron Condor - neutral+cheap → Long Straddle
752
+ Each suggestion includes exact contracts × ticker × expiry × strike × side, with sizing scaled to maxRisk.
753
+
754
+ 108. crypto_onchain_metrics(coin: string)
755
+ Multi-source on-chain & derivatives view of a crypto asset. coin = CoinGecko ID (bitcoin, ethereum, solana, …).
756
+ Combines:
757
+ - CoinGecko market data: price, mcap, supply, ATH distance, social signal
758
+ - DeFi Llama TVL (for L1s): 30-day TVL trend
759
+ - Binance perpetual futures: 8h funding rate (last + avg of 8), open interest, positioning signal 🟢/🔴/🟡
760
+ - Alternative.me Fear & Greed Index (0-100)
761
+ - CoinGecko global: BTC/ETH/stablecoin market share
762
+ All-free APIs. No API key required.
763
+
764
+ 109. portfolio_tax_lots(method?: string)
765
+ Tax-lot accounting on the transaction history. method: "FIFO" | "LIFO" | "HIFO". Default: FIFO.
766
+ Computes realized short-term (taxed at income rate) vs long-term (15%/20% US) gains per ticker.
767
+ Lists open lots with cost basis, age (long-term threshold 365 days), unrealized P/L per lot.
768
+ Surfaces:
769
+ - ⚠ Wash-sale warnings: buys within 30 days of a loss sale (IRS §1091)
770
+ - 💡 Tax-loss harvest candidates: open lots with unrealized loss > $100, flags if hold < 31d (wash-sale risk)
771
+ Requires transactions recorded via portfolio_tx_add (legacy portfolio_add still works for current state).
772
+
773
+ 110. portfolio_tx_add(ticker: string, type: "buy"|"sell", qty: number, price: number, date?: string)
774
+ Record a dated transaction. Feeds portfolio_tax_lots. Also updates current positions for portfolio_summary parity.
775
+ date format: "YYYY-MM-DD" (defaults to today).
776
+
745
777
  --- CODE EXECUTION ---
746
778
 
747
779
  81. execute_code(language: "python"|"javascript"|"typescript", code: string, files?: [{path: string, content: string}], packages?: string[], stdin?: string, timeout?: number)
@@ -4692,6 +4724,482 @@ print(f" Max drawdown: {max_dd*100:.2f}%")
4692
4724
  return lines.join('\n');
4693
4725
  }
4694
4726
 
4727
+ case 'option_strategy_builder': {
4728
+ const ticker = String(params.ticker || '').toUpperCase();
4729
+ const direction = String(params.direction || 'neutral').toLowerCase();
4730
+ const maxRisk = parseFloat(params.maxRisk || params.max_risk || '1000');
4731
+ const targetDays = parseInt(params.daysToExpiry || params.dte || '30', 10);
4732
+ if (!ticker) return 'option_strategy_builder: ticker required.';
4733
+ try {
4734
+ // 1. Fetch options chain (Yahoo Finance, free)
4735
+ const chainRes = await fetch(`https://query2.finance.yahoo.com/v7/finance/options/${encodeURIComponent(ticker)}`, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
4736
+ if (!chainRes.ok) return `option_strategy_builder: HTTP ${chainRes.status}`;
4737
+ const cd = await chainRes.json();
4738
+ const result = cd?.optionChain?.result?.[0];
4739
+ if (!result) return `option_strategy_builder: no options data for ${ticker}.`;
4740
+ const spot = result.quote?.regularMarketPrice;
4741
+ // 2. Pick the expiration closest to targetDays
4742
+ const today = Date.now() / 1000;
4743
+ const exps = result.expirationDates || [];
4744
+ const expPicked = exps.reduce((best, e) => {
4745
+ const dte = (e - today) / 86400;
4746
+ if (dte < 5) return best;
4747
+ const bestDte = best ? (best - today) / 86400 : Infinity;
4748
+ return Math.abs(dte - targetDays) < Math.abs(bestDte - targetDays) ? e : best;
4749
+ }, 0);
4750
+ if (!expPicked) return `option_strategy_builder: no suitable expiry near ${targetDays} days.`;
4751
+ const dtePicked = Math.round((expPicked - today) / 86400);
4752
+ // Fetch the picked chain
4753
+ const pickedRes = await fetch(`https://query2.finance.yahoo.com/v7/finance/options/${encodeURIComponent(ticker)}?date=${expPicked}`, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
4754
+ const pickedData = await pickedRes.json();
4755
+ const opts = pickedData?.optionChain?.result?.[0]?.options?.[0] || {};
4756
+ const calls = opts.calls || [];
4757
+ const puts = opts.puts || [];
4758
+ if (calls.length === 0 || puts.length === 0) return `option_strategy_builder: empty chain for chosen expiry.`;
4759
+ // 3. ATM IV (average call+put closest to spot)
4760
+ const closest = (arr) => arr.reduce((a, b) => Math.abs(a.strike - spot) < Math.abs(b.strike - spot) ? a : b);
4761
+ const atmCall = closest(calls);
4762
+ const atmPut = closest(puts);
4763
+ const atmIV = ((atmCall.impliedVolatility || 0) + (atmPut.impliedVolatility || 0)) / 2;
4764
+ // 4. IV rank from market_chart historical realized vol as proxy (no API for historical IV free)
4765
+ const chartRes = await fetch(`https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?range=1y&interval=1d`, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
4766
+ let ivPercentile = null;
4767
+ try {
4768
+ const ch = await chartRes.json();
4769
+ const closes = ch?.chart?.result?.[0]?.indicators?.quote?.[0]?.close?.filter(Number.isFinite) || [];
4770
+ // Rolling 30-day realized vol annualized
4771
+ const rollingVols = [];
4772
+ for (let i = 30; i < closes.length; i++) {
4773
+ const window = closes.slice(i - 30, i);
4774
+ const rets = window.slice(1).map((v, j) => Math.log(v / window[j]));
4775
+ const mean = rets.reduce((a, b) => a + b, 0) / rets.length;
4776
+ const variance = rets.reduce((a, r) => a + (r - mean) ** 2, 0) / (rets.length - 1);
4777
+ rollingVols.push(Math.sqrt(variance * 252));
4778
+ }
4779
+ if (rollingVols.length > 20) {
4780
+ const sorted = [...rollingVols].sort((a, b) => a - b);
4781
+ const rank = sorted.findIndex(v => v >= atmIV);
4782
+ ivPercentile = rank < 0 ? 100 : (rank / sorted.length) * 100;
4783
+ }
4784
+ } catch {}
4785
+ const ivCheap = ivPercentile != null && ivPercentile < 30;
4786
+ const ivRich = ivPercentile != null && ivPercentile > 70;
4787
+ // 5. Build strategy recommendations
4788
+ const expStr = new Date(expPicked * 1000).toISOString().slice(0, 10);
4789
+ const findStrike = (arr, targetDelta) => {
4790
+ // Find option whose abs(delta) is closest to target. Yahoo doesn't always provide delta;
4791
+ // approximate by inMoneyness: for calls, delta≈0.5 at ATM; +0.05 per 1% ITM.
4792
+ let best = arr[0], bestDist = Infinity;
4793
+ for (const o of arr) {
4794
+ const moneyness = (o.strike - spot) / spot;
4795
+ const approxDelta = arr === calls ? Math.max(0.05, Math.min(0.95, 0.5 - moneyness * 5)) : Math.max(-0.95, Math.min(-0.05, -0.5 - moneyness * 5));
4796
+ const dist = Math.abs(Math.abs(approxDelta) - targetDelta);
4797
+ if (dist < bestDist) { bestDist = dist; best = o; }
4798
+ }
4799
+ return best;
4800
+ };
4801
+ const mid = (o) => ((o.bid || 0) + (o.ask || 0)) / 2 || o.lastPrice || 0;
4802
+ const strategies = [];
4803
+
4804
+ if (direction === 'bullish' || direction === 'long') {
4805
+ if (ivCheap || ivPercentile == null) {
4806
+ // Bull Call Debit Spread (buy ATM, sell OTM)
4807
+ const buy = atmCall;
4808
+ const sell = calls.find(c => c.strike > spot * 1.05) || calls[calls.length - 1];
4809
+ const debit = (mid(buy) - mid(sell)) * 100;
4810
+ const width = (sell.strike - buy.strike) * 100;
4811
+ const maxProfit = width - debit;
4812
+ const maxLoss = debit;
4813
+ const be = buy.strike + (debit / 100);
4814
+ const contracts = Math.max(1, Math.floor(maxRisk / debit));
4815
+ strategies.push({
4816
+ name: 'Bull Call Debit Spread',
4817
+ rationale: 'Bullish, IV cheap → buy premium, defined risk',
4818
+ legs: [`BUY ${contracts}x ${ticker} ${expStr} ${buy.strike} CALL @ ~$${mid(buy).toFixed(2)}`,
4819
+ `SELL ${contracts}x ${ticker} ${expStr} ${sell.strike} CALL @ ~$${mid(sell).toFixed(2)}`],
4820
+ netDebit: debit * contracts,
4821
+ maxProfit: maxProfit * contracts,
4822
+ maxLoss: maxLoss * contracts,
4823
+ breakeven: be.toFixed(2),
4824
+ roi: ((maxProfit / Math.max(1, maxLoss)) * 100).toFixed(0) + '%',
4825
+ });
4826
+ }
4827
+ if (ivRich) {
4828
+ // Bull Put Credit Spread (sell OTM put, buy further OTM put)
4829
+ const sell = puts.find(p => p.strike < spot * 0.97 && p.strike > spot * 0.93) || puts[Math.floor(puts.length / 2)];
4830
+ const buy = puts.find(p => p.strike < sell.strike - spot * 0.03) || puts[0];
4831
+ const credit = (mid(sell) - mid(buy)) * 100;
4832
+ const width = (sell.strike - buy.strike) * 100;
4833
+ const maxLoss = width - credit;
4834
+ const be = sell.strike - (credit / 100);
4835
+ const contracts = Math.max(1, Math.floor(maxRisk / maxLoss));
4836
+ strategies.push({
4837
+ name: 'Bull Put Credit Spread',
4838
+ rationale: 'Bullish, IV rich → sell premium, defined risk',
4839
+ legs: [`SELL ${contracts}x ${ticker} ${expStr} ${sell.strike} PUT @ ~$${mid(sell).toFixed(2)}`,
4840
+ `BUY ${contracts}x ${ticker} ${expStr} ${buy.strike} PUT @ ~$${mid(buy).toFixed(2)}`],
4841
+ netCredit: credit * contracts,
4842
+ maxProfit: credit * contracts,
4843
+ maxLoss: maxLoss * contracts,
4844
+ breakeven: be.toFixed(2),
4845
+ roi: ((credit / Math.max(1, maxLoss)) * 100).toFixed(0) + '%',
4846
+ });
4847
+ }
4848
+ }
4849
+
4850
+ if (direction === 'bearish' || direction === 'short') {
4851
+ if (ivCheap || ivPercentile == null) {
4852
+ const buy = atmPut;
4853
+ const sell = puts.find(p => p.strike < spot * 0.95) || puts[0];
4854
+ const debit = (mid(buy) - mid(sell)) * 100;
4855
+ const width = (buy.strike - sell.strike) * 100;
4856
+ const maxProfit = width - debit;
4857
+ const be = buy.strike - (debit / 100);
4858
+ const contracts = Math.max(1, Math.floor(maxRisk / debit));
4859
+ strategies.push({
4860
+ name: 'Bear Put Debit Spread',
4861
+ rationale: 'Bearish, IV cheap → buy premium, defined risk',
4862
+ legs: [`BUY ${contracts}x ${ticker} ${expStr} ${buy.strike} PUT @ ~$${mid(buy).toFixed(2)}`,
4863
+ `SELL ${contracts}x ${ticker} ${expStr} ${sell.strike} PUT @ ~$${mid(sell).toFixed(2)}`],
4864
+ netDebit: debit * contracts, maxProfit: maxProfit * contracts, maxLoss: debit * contracts,
4865
+ breakeven: be.toFixed(2), roi: ((maxProfit / Math.max(1, debit)) * 100).toFixed(0) + '%',
4866
+ });
4867
+ }
4868
+ if (ivRich) {
4869
+ const sell = calls.find(c => c.strike > spot * 1.03 && c.strike < spot * 1.07) || calls[Math.floor(calls.length / 2)];
4870
+ const buy = calls.find(c => c.strike > sell.strike + spot * 0.03) || calls[calls.length - 1];
4871
+ const credit = (mid(sell) - mid(buy)) * 100;
4872
+ const width = (buy.strike - sell.strike) * 100;
4873
+ const maxLoss = width - credit;
4874
+ const be = sell.strike + (credit / 100);
4875
+ const contracts = Math.max(1, Math.floor(maxRisk / maxLoss));
4876
+ strategies.push({
4877
+ name: 'Bear Call Credit Spread',
4878
+ rationale: 'Bearish, IV rich → sell premium, defined risk',
4879
+ legs: [`SELL ${contracts}x ${ticker} ${expStr} ${sell.strike} CALL @ ~$${mid(sell).toFixed(2)}`,
4880
+ `BUY ${contracts}x ${ticker} ${expStr} ${buy.strike} CALL @ ~$${mid(buy).toFixed(2)}`],
4881
+ netCredit: credit * contracts, maxProfit: credit * contracts, maxLoss: maxLoss * contracts,
4882
+ breakeven: be.toFixed(2), roi: ((credit / Math.max(1, maxLoss)) * 100).toFixed(0) + '%',
4883
+ });
4884
+ }
4885
+ }
4886
+
4887
+ if (direction === 'neutral' || direction === 'range') {
4888
+ if (ivRich) {
4889
+ // Iron Condor — sell OTM call + OTM put, buy further OTM wings
4890
+ const sellPut = puts.find(p => p.strike < spot * 0.95 && p.strike > spot * 0.90) || puts[Math.floor(puts.length * 0.3)];
4891
+ const buyPut = puts.find(p => p.strike < sellPut.strike - spot * 0.05) || puts[0];
4892
+ const sellCall = calls.find(c => c.strike > spot * 1.05 && c.strike < spot * 1.10) || calls[Math.floor(calls.length * 0.7)];
4893
+ const buyCall = calls.find(c => c.strike > sellCall.strike + spot * 0.05) || calls[calls.length - 1];
4894
+ const credit = (mid(sellPut) + mid(sellCall) - mid(buyPut) - mid(buyCall)) * 100;
4895
+ const widthPut = (sellPut.strike - buyPut.strike) * 100;
4896
+ const widthCall = (buyCall.strike - sellCall.strike) * 100;
4897
+ const maxLoss = Math.max(widthPut, widthCall) - credit;
4898
+ const contracts = Math.max(1, Math.floor(maxRisk / maxLoss));
4899
+ strategies.push({
4900
+ name: 'Iron Condor',
4901
+ rationale: 'Neutral, IV rich → defined-risk short premium, profits if price stays in range',
4902
+ legs: [
4903
+ `SELL ${contracts}x ${expStr} ${sellPut.strike} PUT @ ~$${mid(sellPut).toFixed(2)}`,
4904
+ `BUY ${contracts}x ${expStr} ${buyPut.strike} PUT @ ~$${mid(buyPut).toFixed(2)}`,
4905
+ `SELL ${contracts}x ${expStr} ${sellCall.strike} CALL @ ~$${mid(sellCall).toFixed(2)}`,
4906
+ `BUY ${contracts}x ${expStr} ${buyCall.strike} CALL @ ~$${mid(buyCall).toFixed(2)}`,
4907
+ ],
4908
+ netCredit: credit * contracts, maxProfit: credit * contracts, maxLoss: maxLoss * contracts,
4909
+ breakeven: `${(sellPut.strike - credit / 100).toFixed(2)} to ${(sellCall.strike + credit / 100).toFixed(2)}`,
4910
+ roi: ((credit / Math.max(1, maxLoss)) * 100).toFixed(0) + '%',
4911
+ });
4912
+ }
4913
+ if (ivCheap) {
4914
+ // Long Straddle — direction-agnostic, profits from large move
4915
+ const debit = (mid(atmCall) + mid(atmPut)) * 100;
4916
+ const contracts = Math.max(1, Math.floor(maxRisk / debit));
4917
+ strategies.push({
4918
+ name: 'Long Straddle',
4919
+ rationale: 'Neutral, IV cheap → expecting a large move either direction (often pre-earnings)',
4920
+ legs: [
4921
+ `BUY ${contracts}x ${expStr} ${atmCall.strike} CALL @ ~$${mid(atmCall).toFixed(2)}`,
4922
+ `BUY ${contracts}x ${expStr} ${atmPut.strike} PUT @ ~$${mid(atmPut).toFixed(2)}`,
4923
+ ],
4924
+ netDebit: debit * contracts, maxProfit: 'unlimited (call) / large (put)', maxLoss: debit * contracts,
4925
+ breakeven: `${(atmCall.strike - debit / 100).toFixed(2)} or ${(atmCall.strike + debit / 100).toFixed(2)}`,
4926
+ roi: 'depends on move magnitude',
4927
+ });
4928
+ }
4929
+ }
4930
+
4931
+ if (strategies.length === 0) {
4932
+ strategies.push({ name: '(no recommendation)', rationale: `direction=${direction} + IV=${ivPercentile?.toFixed(0)}% — no matching template. Try direction=bullish/bearish/neutral.` });
4933
+ }
4934
+
4935
+ const ivLabel = ivPercentile == null ? 'n/a' : `${ivPercentile.toFixed(0)}th percentile (${ivCheap ? 'CHEAP' : ivRich ? 'RICH' : 'normal'})`;
4936
+ const lines = [
4937
+ `Option strategy builder — ${ticker} @ $${spot}`,
4938
+ `Direction: ${direction} | ATM IV: ${(atmIV * 100).toFixed(1)}% | IV rank: ${ivLabel}`,
4939
+ `Expiry chosen: ${expStr} (${dtePicked} DTE) | Max risk budget: $${maxRisk}`,
4940
+ '',
4941
+ ];
4942
+ strategies.forEach((s, i) => {
4943
+ lines.push(`${i + 1}. ${s.name}`);
4944
+ lines.push(` ${s.rationale}`);
4945
+ (s.legs || []).forEach(l => lines.push(` ${l}`));
4946
+ if (s.netDebit) lines.push(` Net debit: $${(s.netDebit).toFixed(0)}`);
4947
+ if (s.netCredit) lines.push(` Net credit: $${(s.netCredit).toFixed(0)}`);
4948
+ if (s.maxProfit != null) lines.push(` Max profit: ${typeof s.maxProfit === 'number' ? '$' + s.maxProfit.toFixed(0) : s.maxProfit}`);
4949
+ if (s.maxLoss != null) lines.push(` Max loss: $${typeof s.maxLoss === 'number' ? s.maxLoss.toFixed(0) : s.maxLoss}`);
4950
+ if (s.breakeven) lines.push(` Breakeven: ${s.breakeven}`);
4951
+ if (s.roi) lines.push(` ROI: ${s.roi}`);
4952
+ lines.push('');
4953
+ });
4954
+ return lines.join('\n');
4955
+ } catch (e) { return `option_strategy_builder error: ${e.message}`; }
4956
+ }
4957
+
4958
+ case 'crypto_onchain_metrics': {
4959
+ const coin = String(params.coin || 'bitcoin').toLowerCase();
4960
+ const lines = [`On-chain & derivatives — ${coin}`];
4961
+ try {
4962
+ // 1. CoinGecko: price + market cap + supply
4963
+ const cgRes = await fetch(`https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coin)}?localization=false&tickers=false&community_data=true&developer_data=false`, { headers: { 'Accept': 'application/json', 'User-Agent': 'NHA/1.0' } });
4964
+ if (cgRes.ok) {
4965
+ const cg = await cgRes.json();
4966
+ const md = cg.market_data || {};
4967
+ const price = md.current_price?.usd;
4968
+ const mcap = md.market_cap?.usd;
4969
+ const supply = md.circulating_supply;
4970
+ const ath = md.ath?.usd;
4971
+ const athDate = md.ath_date?.usd?.slice(0, 10);
4972
+ const fromAth = md.ath_change_percentage?.usd?.toFixed(1);
4973
+ lines.push(`Price: $${price?.toLocaleString()} | Market cap: $${(mcap / 1e9)?.toFixed(2)}B | Circulating: ${(supply / 1e6)?.toFixed(2)}M`);
4974
+ lines.push(`ATH: $${ath?.toLocaleString()} on ${athDate} (${fromAth}% from ATH)`);
4975
+ if (cg.community_data?.twitter_followers) lines.push(`Twitter followers: ${cg.community_data.twitter_followers.toLocaleString()}`);
4976
+ // MVRV approximation using market cap / realized cap (CoinGecko doesn't expose realized cap directly,
4977
+ // but we approximate via mcap / (supply × avg_30d_price) — quick proxy).
4978
+ if (md.market_cap_change_percentage_24h_in_currency?.usd != null) {
4979
+ lines.push(`24h mcap change: ${md.market_cap_change_percentage_24h_in_currency.usd.toFixed(2)}%`);
4980
+ }
4981
+ }
4982
+
4983
+ // 2. DeFi Llama TVL (free, no key) — for L1s and DeFi protocols
4984
+ try {
4985
+ const llSlugMap = { bitcoin: null, ethereum: 'ethereum', solana: 'solana', avalanche: 'avalanche', polygon: 'polygon', arbitrum: 'arbitrum', optimism: 'optimism', polkadot: 'polkadot', cosmos: 'cosmos' };
4986
+ const slug = llSlugMap[coin];
4987
+ if (slug) {
4988
+ const llRes = await fetch(`https://api.llama.fi/v2/historicalChainTvl/${slug}`);
4989
+ if (llRes.ok) {
4990
+ const tvl = await llRes.json();
4991
+ const latest = tvl[tvl.length - 1];
4992
+ const month = tvl[tvl.length - 30] || tvl[0];
4993
+ const chgPct = month?.tvl ? ((latest.tvl - month.tvl) / month.tvl * 100) : 0;
4994
+ lines.push(`\nDeFi TVL on ${slug}: $${(latest.tvl / 1e9).toFixed(2)}B (30d ${chgPct >= 0 ? '+' : ''}${chgPct.toFixed(1)}%)`);
4995
+ }
4996
+ } else if (coin === 'bitcoin') {
4997
+ // BTC: pull total DeFi TVL on bitcoin sidechains
4998
+ const llRes = await fetch('https://api.llama.fi/v2/historicalChainTvl/Bitcoin');
4999
+ if (llRes.ok) {
5000
+ const tvl = await llRes.json();
5001
+ const latest = tvl[tvl.length - 1];
5002
+ lines.push(`\nDeFi TVL on Bitcoin (sidechains/L2): $${(latest.tvl / 1e6).toFixed(0)}M`);
5003
+ }
5004
+ }
5005
+ } catch {}
5006
+
5007
+ // 3. Binance perpetual futures: funding rate + open interest (free, no key)
5008
+ try {
5009
+ const sym = coin === 'bitcoin' ? 'BTCUSDT' : coin === 'ethereum' ? 'ETHUSDT' : coin === 'solana' ? 'SOLUSDT' : null;
5010
+ if (sym) {
5011
+ const [fr, oi] = await Promise.all([
5012
+ fetch(`https://fapi.binance.com/fapi/v1/fundingRate?symbol=${sym}&limit=8`).then(r => r.ok ? r.json() : null).catch(() => null),
5013
+ fetch(`https://fapi.binance.com/fapi/v1/openInterest?symbol=${sym}`).then(r => r.ok ? r.json() : null).catch(() => null),
5014
+ ]);
5015
+ if (Array.isArray(fr) && fr.length) {
5016
+ const lastFr = parseFloat(fr[fr.length - 1].fundingRate) * 100;
5017
+ const avg8 = fr.reduce((a, x) => a + parseFloat(x.fundingRate), 0) / fr.length * 100;
5018
+ lines.push(`\nBinance perp funding (8h × 8 readings): last ${lastFr.toFixed(4)}% | avg ${avg8.toFixed(4)}%`);
5019
+ lines.push(` → ${avg8 > 0.02 ? '🟢 longs paying shorts (bullish positioning)' : avg8 < -0.02 ? '🔴 shorts paying longs (bearish positioning)' : '🟡 neutral'}`);
5020
+ }
5021
+ if (oi?.openInterest) lines.push(`Open Interest: ${parseFloat(oi.openInterest).toLocaleString()} ${sym.replace('USDT', '')}`);
5022
+ }
5023
+ } catch {}
5024
+
5025
+ // 4. Fear & Greed Index (alternative.me, free)
5026
+ try {
5027
+ const fgRes = await fetch('https://api.alternative.me/fng/?limit=1', { headers: { 'Accept': 'application/json' } });
5028
+ if (fgRes.ok) {
5029
+ const fg = await fgRes.json();
5030
+ const f = fg.data?.[0];
5031
+ if (f) lines.push(`\nFear & Greed Index: ${f.value}/100 — ${f.value_classification}`);
5032
+ }
5033
+ } catch {}
5034
+
5035
+ // 5. BTC dominance + Stablecoin supply proxy
5036
+ try {
5037
+ const gRes = await fetch('https://api.coingecko.com/api/v3/global', { headers: { 'Accept': 'application/json' } });
5038
+ if (gRes.ok) {
5039
+ const g = await gRes.json();
5040
+ const dom = g.data?.market_cap_percentage || {};
5041
+ lines.push(`\nMarket structure: BTC dominance ${dom.btc?.toFixed(1)}% | ETH ${dom.eth?.toFixed(1)}% | Stablecoins (USDT+USDC) ${((dom.usdt || 0) + (dom.usdc || 0)).toFixed(1)}%`);
5042
+ }
5043
+ } catch {}
5044
+
5045
+ return lines.join('\n');
5046
+ } catch (e) { return `crypto_onchain_metrics error: ${e.message}`; }
5047
+ }
5048
+
5049
+ case 'portfolio_tax_lots': {
5050
+ // Tax-lot accounting. Reads ~/.nha/portfolio.json's `transactions` array
5051
+ // (buy/sell events with date + qty + price), produces realized + unrealized
5052
+ // gain breakdown using the chosen accounting method.
5053
+ const method = String(params.method || 'FIFO').toUpperCase(); // FIFO | LIFO | HIFO
5054
+ const fs = await import('fs'); const path = await import('path'); const os = await import('os');
5055
+ const file = path.default.join(os.default.homedir(), '.nha', 'portfolio.json');
5056
+ if (!fs.default.existsSync(file)) return 'Portfolio empty (no ~/.nha/portfolio.json).';
5057
+ const pf = JSON.parse(fs.default.readFileSync(file, 'utf-8'));
5058
+ const txs = (pf.transactions || []).slice().sort((a, b) => a.date.localeCompare(b.date));
5059
+ if (txs.length === 0) return 'No transactions recorded. Use portfolio_tx_add to record buys/sells with date.';
5060
+
5061
+ // Build open lots per ticker by applying buys/sells with the chosen method.
5062
+ const lotsByTicker = {};
5063
+ const realizedByTicker = {};
5064
+ const washSaleWarnings = [];
5065
+ const today = new Date().toISOString().slice(0, 10);
5066
+ const yearAgoMs = Date.now() - 365 * 86400000;
5067
+
5068
+ for (const tx of txs) {
5069
+ const t = (tx.ticker || '').toUpperCase();
5070
+ if (!t) continue;
5071
+ lotsByTicker[t] = lotsByTicker[t] || [];
5072
+ realizedByTicker[t] = realizedByTicker[t] || { shortTerm: 0, longTerm: 0, totalSold: 0 };
5073
+ if (tx.type === 'buy' || tx.type === 'BUY') {
5074
+ lotsByTicker[t].push({ date: tx.date, qty: tx.qty, cost: tx.price, remaining: tx.qty });
5075
+ // Wash sale check: any sale at a LOSS within 30 days before this buy?
5076
+ const recentLosses = (realizedByTicker[t].events || []).filter(ev => ev.loss < 0 && (new Date(tx.date) - new Date(ev.date)) <= 30 * 86400000 && (new Date(tx.date) - new Date(ev.date)) >= 0);
5077
+ if (recentLosses.length) washSaleWarnings.push(`${t}: buy on ${tx.date} may trigger wash-sale rule (loss sale on ${recentLosses[0].date}, $${(-recentLosses[0].loss).toFixed(2)} disallowed).`);
5078
+ } else if (tx.type === 'sell' || tx.type === 'SELL') {
5079
+ let qtyToClose = tx.qty;
5080
+ const sortLots = (lots) => {
5081
+ if (method === 'LIFO') return [...lots].sort((a, b) => b.date.localeCompare(a.date));
5082
+ if (method === 'HIFO') return [...lots].sort((a, b) => b.cost - a.cost);
5083
+ return [...lots].sort((a, b) => a.date.localeCompare(b.date)); // FIFO
5084
+ };
5085
+ const order = sortLots(lotsByTicker[t]);
5086
+ for (const lot of order) {
5087
+ if (qtyToClose <= 0) break;
5088
+ if (lot.remaining <= 0) continue;
5089
+ const take = Math.min(lot.remaining, qtyToClose);
5090
+ const proceeds = take * tx.price;
5091
+ const costBasis = take * lot.cost;
5092
+ const gain = proceeds - costBasis;
5093
+ const holdMs = new Date(tx.date) - new Date(lot.date);
5094
+ const isLongTerm = holdMs > 365 * 86400000;
5095
+ if (isLongTerm) realizedByTicker[t].longTerm += gain;
5096
+ else realizedByTicker[t].shortTerm += gain;
5097
+ realizedByTicker[t].totalSold += take;
5098
+ realizedByTicker[t].events = realizedByTicker[t].events || [];
5099
+ realizedByTicker[t].events.push({ date: tx.date, qty: take, gain, loss: gain < 0 ? gain : 0, lotDate: lot.date, isLongTerm });
5100
+ lot.remaining -= take;
5101
+ qtyToClose -= take;
5102
+ }
5103
+ }
5104
+ }
5105
+
5106
+ // Fetch live prices for unrealized gain.
5107
+ const tickersOpen = Object.keys(lotsByTicker).filter(t => lotsByTicker[t].some(l => l.remaining > 0));
5108
+ let priceMap = {};
5109
+ if (tickersOpen.length) {
5110
+ try {
5111
+ const r = await fetch(`https://query1.finance.yahoo.com/v7/finance/quote?symbols=${tickersOpen.join(',')}`, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
5112
+ if (r.ok) {
5113
+ const d = await r.json();
5114
+ priceMap = Object.fromEntries((d?.quoteResponse?.result || []).map(q => [q.symbol, q.regularMarketPrice]));
5115
+ }
5116
+ } catch {}
5117
+ }
5118
+
5119
+ const lines = [`Tax lot report — method: ${method}`, ''];
5120
+ let totalRealizedShort = 0, totalRealizedLong = 0, totalUnrealized = 0;
5121
+ let lossHarvestCandidates = [];
5122
+ for (const t of Object.keys(lotsByTicker)) {
5123
+ const open = lotsByTicker[t].filter(l => l.remaining > 0);
5124
+ const realized = realizedByTicker[t] || { shortTerm: 0, longTerm: 0 };
5125
+ const price = priceMap[t];
5126
+ let unrealized = 0;
5127
+ open.forEach(l => { if (price) unrealized += (price - l.cost) * l.remaining; });
5128
+ totalRealizedShort += realized.shortTerm;
5129
+ totalRealizedLong += realized.longTerm;
5130
+ totalUnrealized += unrealized;
5131
+ // Tax-loss harvesting: lot with unrealized loss + holding > some threshold
5132
+ open.forEach(l => {
5133
+ if (price && (price - l.cost) * l.remaining < -100) {
5134
+ const holdDays = (Date.now() - new Date(l.date).getTime()) / 86400000;
5135
+ lossHarvestCandidates.push({ ticker: t, lotDate: l.date, qty: l.remaining, cost: l.cost, currentPrice: price, loss: (price - l.cost) * l.remaining, holdDays });
5136
+ }
5137
+ });
5138
+ lines.push(`${t}:`);
5139
+ lines.push(` Realized: short-term $${realized.shortTerm.toFixed(2)} | long-term $${realized.longTerm.toFixed(2)} | total $${(realized.shortTerm + realized.longTerm).toFixed(2)}`);
5140
+ if (open.length) {
5141
+ lines.push(` Open lots (${open.length}, live $${price?.toFixed(2) || '?'}):`);
5142
+ open.forEach(l => {
5143
+ const u = price ? (price - l.cost) * l.remaining : 0;
5144
+ const ageDays = ((Date.now() - new Date(l.date).getTime()) / 86400000).toFixed(0);
5145
+ lines.push(` ${l.date} qty ${l.remaining} cost $${l.cost.toFixed(2)} unrealized $${u.toFixed(2)} (${ageDays}d, ${ageDays > 365 ? 'long-term' : 'SHORT-TERM'})`);
5146
+ });
5147
+ }
5148
+ lines.push('');
5149
+ }
5150
+ lines.push(`TOTALS:`);
5151
+ lines.push(` Realized short-term gains: $${totalRealizedShort.toFixed(2)} (taxed at ordinary income rate)`);
5152
+ lines.push(` Realized long-term gains: $${totalRealizedLong.toFixed(2)} (taxed at 0/15/20% in US)`);
5153
+ lines.push(` Unrealized gains: $${totalUnrealized.toFixed(2)}`);
5154
+ if (washSaleWarnings.length) {
5155
+ lines.push(`\n⚠ Wash-sale warnings:\n ${washSaleWarnings.join('\n ')}`);
5156
+ }
5157
+ if (lossHarvestCandidates.length) {
5158
+ lines.push(`\n💡 Tax-loss harvest candidates (unrealized loss > $100):`);
5159
+ lossHarvestCandidates.sort((a, b) => a.loss - b.loss).slice(0, 5).forEach(c => {
5160
+ lines.push(` ${c.ticker} lot ${c.lotDate} qty ${c.qty}: realize $${c.loss.toFixed(2)} loss${c.holdDays < 31 ? ' ⚠ <31 days, wash-sale risk' : ''}`);
5161
+ });
5162
+ }
5163
+ return lines.join('\n');
5164
+ }
5165
+
5166
+ case 'portfolio_tx_add': {
5167
+ // Record a buy/sell transaction. Used to feed portfolio_tax_lots.
5168
+ const ticker = String(params.ticker || '').toUpperCase();
5169
+ const type = String(params.type || 'buy').toLowerCase();
5170
+ const qty = parseFloat(params.qty || params.quantity || '0');
5171
+ const price = parseFloat(params.price || '0');
5172
+ const date = String(params.date || new Date().toISOString().slice(0, 10));
5173
+ if (!ticker || qty <= 0 || !['buy', 'sell'].includes(type)) return 'portfolio_tx_add: ticker, type (buy|sell), qty (>0), price required.';
5174
+ const fs = await import('fs'); const path = await import('path'); const os = await import('os');
5175
+ const file = path.default.join(os.default.homedir(), '.nha', 'portfolio.json');
5176
+ let pf = { positions: [], transactions: [] };
5177
+ try { if (fs.default.existsSync(file)) pf = JSON.parse(fs.default.readFileSync(file, 'utf-8')); } catch {}
5178
+ pf.transactions = pf.transactions || [];
5179
+ pf.transactions.push({ ticker, type, qty, price, date, recordedAt: new Date().toISOString() });
5180
+ // Also update current `positions` for parity with portfolio_summary.
5181
+ pf.positions = pf.positions || [];
5182
+ const idx = pf.positions.findIndex(p => p.ticker === ticker);
5183
+ if (type === 'buy') {
5184
+ if (idx >= 0) {
5185
+ const existing = pf.positions[idx];
5186
+ const totalQty = existing.qty + qty;
5187
+ existing.cost = ((existing.qty * existing.cost) + (qty * price)) / totalQty;
5188
+ existing.qty = totalQty;
5189
+ } else {
5190
+ pf.positions.push({ ticker, qty, cost: price, addedAt: new Date().toISOString() });
5191
+ }
5192
+ } else {
5193
+ if (idx >= 0) {
5194
+ pf.positions[idx].qty -= qty;
5195
+ if (pf.positions[idx].qty <= 0) pf.positions.splice(idx, 1);
5196
+ }
5197
+ }
5198
+ fs.default.mkdirSync(path.default.dirname(file), { recursive: true });
5199
+ fs.default.writeFileSync(file, JSON.stringify(pf, null, 2));
5200
+ return `Transaction recorded: ${type.toUpperCase()} ${qty} ${ticker} @ $${price} on ${date}. Total transactions: ${pf.transactions.length}.`;
5201
+ }
5202
+
4695
5203
  case 'insider_trading': {
4696
5204
  const ticker = String(params.ticker || '').toUpperCase();
4697
5205
  const limit = Math.min(parseInt(params.limit || '15', 10), 30);