nothumanallowed 15.1.69 → 16.0.0
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 +1 -1
- package/src/constants.mjs +1 -1
- package/src/server/index.mjs +12 -0
- package/src/server/routes/connectors.mjs +1142 -10
- package/src/services/tool-executor.mjs +509 -1
- package/src/ui-dist/assets/{index-DKPyRmuw.js → index-6sPt_3Be.js} +42 -41
- package/src/ui-dist/assets/index-C9ZZfWVS.css +1 -0
- package/src/ui-dist/index.html +2 -2
- package/src/ui-dist/assets/index-IQn8QiFW.css +0 -1
|
@@ -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);
|