Skip to content

Commit 825313e

Browse files
authored
Add Liquidity Shifts panel and include Silver in COT seeder (koala73#3070)
* Add liquidity shifts panel and extend COT seeding for silver * fix(liquidity-shifts): review fixes — i18n, CSS, CMD+K, div-by-zero Addresses review feedback on PR koala73#3070: - Pull all user-facing strings into src/locales/en.json under components.liquidityShifts.*; title and tooltip now use t(). - Move inline styles into .liquidity-shifts-panel / .liquidity-row CSS classes in src/styles/main.css so theme tokens and print/dark rules apply. - Replace bespoke renderShiftPill with shared formatChange + getChangeClass helpers (consistency with GulfEconomiesPanel and the .up/.down theme colors). - pct() now returns null when long+short is 0; renderShiftPill and lev formatter show '—' instead of a misleading 100%. - Replace em-dash in the stocks section title with a colon (repo rule). - Add Liquidity Shifts to CMD+K via src/config/commands.ts. - Instrument label lookup moved to INSTRUMENT_LABELS map. * fix(liquidity-shifts): register in COMMODITY variant, hide Lev line for commodities Greptile review on PR koala73#3070: - P1: add 'liquidity-shifts' to COMMODITY_PANELS — panel was specifically designed for commodity positioning but was absent from the variant. - P2: CFTC Disaggregated report (GC/SI/CL) has no Leveraged Funds category; only the TFF report (ES/NQ) populates those fields. The row previously rendered 'Lev +0.0%' for every commodity, which looked like broken data. Skip the sub-line entirely when both leveragedFunds values are 0. * fix(liquidity-shifts): sort stock quotes by TOP_STOCKS order listMarketQuotes preserves seed-bootstrap order (provider fallback sequence from seed-market-quotes), not request order, so the panel rendered 'Top US Stocks' in a shifting arbitrary order. Re-sort the returned quotes by the TOP_STOCKS index client-side to keep row order stable and deterministic.
1 parent ced3e70 commit 825313e

8 files changed

Lines changed: 209 additions & 0 deletions

File tree

‎scripts/seed-cot.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const FINANCIAL_INSTRUMENTS = [
1717

1818
const COMMODITY_INSTRUMENTS = [
1919
{ name: 'Gold', code: 'GC', contractCode: '088691' },
20+
{ name: 'Silver', code: 'SI', contractCode: '084691' },
2021
{ name: 'Crude Oil (WTI)', code: 'CL', contractCode: '067651' },
2122
];
2223

‎src/app/panel-layout.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
EarningsCalendarPanel,
7171
EconomicCalendarPanel,
7272
CotPositioningPanel,
73+
LiquidityShiftsPanel,
7374
GoldIntelligencePanel,
7475
DiseaseOutbreaksPanel,
7576
SocialVelocityPanel,
@@ -1111,6 +1112,7 @@ export class PanelLayoutManager implements AppModule {
11111112
this.createPanel('earnings-calendar', () => new EarningsCalendarPanel());
11121113
this.createPanel('economic-calendar', () => new EconomicCalendarPanel());
11131114
this.createPanel('cot-positioning', () => new CotPositioningPanel());
1115+
this.createPanel('liquidity-shifts', () => new LiquidityShiftsPanel());
11141116
this.createPanel('gold-intelligence', () => new GoldIntelligencePanel());
11151117
this.createPanel('hormuz-tracker', () => new HormuzPanel());
11161118
this.createPanel('etf-flows', () => new ETFFlowsPanel());
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import type { MarketServiceClient } from '@/generated/client/worldmonitor/market/v1/service_client';
2+
import { Panel } from './Panel';
3+
import { t } from '@/services/i18n';
4+
import { escapeHtml } from '@/utils/sanitize';
5+
import { formatChange, getChangeClass } from '@/utils';
6+
7+
let _client: MarketServiceClient | null = null;
8+
9+
async function getMarketClient(): Promise<MarketServiceClient> {
10+
if (!_client) {
11+
const { MarketServiceClient } = await import('@/generated/client/worldmonitor/market/v1/service_client');
12+
const { getRpcBaseUrl } = await import('@/services/rpc-client');
13+
_client = new MarketServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters<typeof fetch>) => globalThis.fetch(...args) });
14+
}
15+
return _client;
16+
}
17+
18+
const TOP_STOCKS = ['AAPL', 'MSFT', 'NVDA', 'AMZN', 'GOOGL', 'META', 'TSLA'];
19+
const COT_PRIORITY = ['CL', 'GC', 'SI', 'ES', 'NQ'];
20+
21+
// Display aliases for CFTC instrument codes that aren't self-explanatory.
22+
const INSTRUMENT_LABELS: Record<string, string> = {
23+
ES: 'S&P 500 futures',
24+
NQ: 'Nasdaq futures',
25+
};
26+
27+
function toNum(v: string | number): number {
28+
if (typeof v === 'number') return v;
29+
const n = parseInt(String(v), 10);
30+
return Number.isFinite(n) ? n : 0;
31+
}
32+
33+
function renderShiftPill(value: number | null): string {
34+
if (value === null) return '<span class="commodity-change">—</span>';
35+
return `<span class="commodity-change ${getChangeClass(value)}">${formatChange(value)}</span>`;
36+
}
37+
38+
function pct(longPos: number, shortPos: number): number | null {
39+
const gross = longPos + shortPos;
40+
if (gross <= 0) return null;
41+
return ((longPos - shortPos) / gross) * 100;
42+
}
43+
44+
function formatLevShift(value: number | null): string {
45+
if (value === null) return '—';
46+
const sign = value >= 0 ? '+' : '';
47+
return `${sign}${value.toFixed(1)}%`;
48+
}
49+
50+
export class LiquidityShiftsPanel extends Panel {
51+
private _hasData = false;
52+
53+
constructor() {
54+
super({
55+
id: 'liquidity-shifts',
56+
title: t('components.liquidityShifts.title'),
57+
showCount: false,
58+
infoTooltip: t('components.liquidityShifts.infoTooltip'),
59+
});
60+
}
61+
62+
public async fetchData(): Promise<boolean> {
63+
this.showLoading();
64+
try {
65+
const client = await getMarketClient();
66+
const [cotResp, stocksResp] = await Promise.all([
67+
client.getCotPositioning({}),
68+
client.listMarketQuotes({ symbols: TOP_STOCKS }),
69+
]);
70+
71+
const cotRows = (cotResp.instruments ?? [])
72+
.filter((i) => COT_PRIORITY.includes(i.code ?? ''))
73+
.sort((a, b) => COT_PRIORITY.indexOf(a.code ?? '') - COT_PRIORITY.indexOf(b.code ?? ''));
74+
75+
if (cotRows.length === 0 && (stocksResp.quotes?.length ?? 0) === 0) {
76+
if (!this._hasData) this.showError(t('components.liquidityShifts.unavailable'), () => void this.fetchData());
77+
return false;
78+
}
79+
80+
this._hasData = true;
81+
const cotHtml = cotRows.map((row) => {
82+
const longPos = toNum(row.assetManagerLong ?? 0);
83+
const shortPos = toNum(row.assetManagerShort ?? 0);
84+
const net = pct(longPos, shortPos);
85+
const levLong = toNum(row.leveragedFundsLong ?? 0);
86+
const levShort = toNum(row.leveragedFundsShort ?? 0);
87+
// CFTC Disaggregated report (GC/SI/CL) has no Leveraged Funds
88+
// category — only TFF report (ES/NQ) does. Skip the sub-line entirely
89+
// rather than render a misleading "Lev +0.0%" for commodity rows.
90+
const hasLev = levLong > 0 || levShort > 0;
91+
const levNet = hasLev ? pct(levLong, levShort) : null;
92+
const code = row.code ?? '';
93+
const label = INSTRUMENT_LABELS[code] ?? row.name ?? code;
94+
const levLine = hasLev
95+
? `<div class="market-symbol">${t('components.liquidityShifts.lev')} ${escapeHtml(formatLevShift(levNet))}</div>`
96+
: '';
97+
98+
return `<div class="liquidity-row">
99+
<div class="liquidity-row__info">
100+
<div class="market-name">${escapeHtml(label)}</div>
101+
<div class="market-symbol">${escapeHtml(code)} • ${t('components.liquidityShifts.longShort', { long: String(longPos), short: String(shortPos) })}</div>
102+
</div>
103+
<div class="liquidity-row__values">
104+
<div>${renderShiftPill(net)}</div>
105+
${levLine}
106+
</div>
107+
</div>`;
108+
}).join('');
109+
110+
// The RPC preserves seed-bootstrap order, not request order, so re-sort
111+
// by TOP_STOCKS to keep the panel's row order stable.
112+
const stockOrder = new Map(TOP_STOCKS.map((sym, i) => [sym, i]));
113+
const stocks = [...(stocksResp.quotes ?? [])].sort((a, b) => {
114+
const ai = stockOrder.get(a.symbol ?? '') ?? Number.MAX_SAFE_INTEGER;
115+
const bi = stockOrder.get(b.symbol ?? '') ?? Number.MAX_SAFE_INTEGER;
116+
return ai - bi;
117+
});
118+
const stockRows = stocks
119+
.map((q) => {
120+
const ch = Number(q.change ?? 0);
121+
return `<div class="market-item liquidity-stock-row">
122+
<div class="market-info">
123+
<span class="market-name">${escapeHtml(q.name || q.symbol || '')}</span>
124+
<span class="market-symbol">${escapeHtml(q.symbol || '')}</span>
125+
</div>
126+
<div>${renderShiftPill(ch)}</div>
127+
</div>`;
128+
})
129+
.join('');
130+
131+
const emptyCot = `<div class="market-symbol">${t('components.liquidityShifts.noCot')}</div>`;
132+
const emptyStocks = `<div class="market-symbol">${t('components.liquidityShifts.noStocks')}</div>`;
133+
const reportDateLine = cotResp.reportDate
134+
? `<div class="market-symbol liquidity-report-date">${t('components.liquidityShifts.reportDate', { date: cotResp.reportDate })}</div>`
135+
: '';
136+
137+
this.setContent(`
138+
<div class="liquidity-shifts-panel">
139+
<div class="liquidity-shifts-panel__section-title">${t('components.liquidityShifts.cotSection')}</div>
140+
${cotHtml || emptyCot}
141+
<div class="liquidity-shifts-panel__section-title liquidity-shifts-panel__section-title--gap">${t('components.liquidityShifts.stocksSection')}</div>
142+
${stockRows || emptyStocks}
143+
${reportDateLine}
144+
</div>
145+
`);
146+
return true;
147+
} catch (e) {
148+
if (!this._hasData) this.showError(e instanceof Error ? e.message : t('components.liquidityShifts.failed'), () => void this.fetchData());
149+
return false;
150+
}
151+
}
152+
}

‎src/components/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export * from './YieldCurvePanel';
8282
export * from './EarningsCalendarPanel';
8383
export * from './EconomicCalendarPanel';
8484
export * from './CotPositioningPanel';
85+
export * from './LiquidityShiftsPanel';
8586
export * from './GoldIntelligencePanel';
8687
export { HormuzPanel } from './HormuzPanel';
8788
export * from './ClimateNewsPanel';

‎src/config/commands.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export const COMMANDS: Command[] = [
177177
{ id: 'panel:earnings-calendar', keywords: ['earnings', 'earnings calendar', 'eps', 'quarterly results'], label: 'Panel: Earnings Calendar', icon: '\u{1F4C5}', category: 'panels' },
178178
{ id: 'panel:economic-calendar', keywords: ['economic calendar', 'macro events', 'nonfarm payrolls', 'gdp release', 'fomc'], label: 'Panel: Economic Calendar', icon: '\u{1F4C6}', category: 'panels' },
179179
{ id: 'panel:cot-positioning', keywords: ['cot', 'cot positioning', 'commitments of traders', 'futures positioning', 'cftc'], label: 'Panel: CFTC COT Positioning', icon: '\u{1F4CA}', category: 'panels' },
180+
{ id: 'panel:liquidity-shifts', keywords: ['liquidity', 'liquidity shifts', 'oil gold silver', 'top stocks', 'positioning'], label: 'Panel: Liquidity Shifts', icon: '\u{1F4B0}', category: 'panels' },
180181
{ id: 'panel:macro-tiles', keywords: ['macro tiles', 'macro indicators', 'macro overview'], label: 'Panel: Macro Indicators', icon: '\u{1F9E9}', category: 'panels' },
181182
{ id: 'panel:defense-patents', keywords: ['defense patents', 'r&d signal', 'military research', 'defense r&d'], label: 'Panel: R&D Signal', icon: '\u{1F9EA}', category: 'panels' },
182183
{ id: 'panel:climate-news', keywords: ['climate news', 'climate headlines', 'environment news', 'climate intelligence', 'carbon brief', 'copernicus'], label: 'Panel: Climate News', icon: '\u{1F30D}', category: 'panels' },

‎src/config/panels.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const FULL_PANELS: Record<string, PanelConfig> = {
7171
'earnings-calendar': { name: 'Earnings Calendar', enabled: false, priority: 2 },
7272
'economic-calendar': { name: 'Economic Calendar', enabled: false, priority: 2 },
7373
'cot-positioning': { name: 'COT Positioning', enabled: false, priority: 2 },
74+
'liquidity-shifts': { name: 'Liquidity Shifts', enabled: true, priority: 2 },
7475
'gold-intelligence': { name: 'Gold Intelligence', enabled: true, priority: 60 },
7576
'hormuz-tracker': { name: 'Hormuz Trade Tracker', enabled: true, priority: 2 },
7677
'energy-crisis': { name: 'Energy Crisis Tracker', enabled: true, priority: 2 },
@@ -448,6 +449,7 @@ const FINANCE_PANELS: Record<string, PanelConfig> = {
448449
'earnings-calendar': { name: 'Earnings Calendar', enabled: true, priority: 1 },
449450
'economic-calendar': { name: 'Economic Calendar', enabled: true, priority: 1 },
450451
'cot-positioning': { name: 'COT Positioning', enabled: true, priority: 2 },
452+
'liquidity-shifts': { name: 'Liquidity Shifts', enabled: true, priority: 1 },
451453
'gold-intelligence': { name: 'Gold Intelligence', enabled: true, priority: 60 },
452454
derivatives: { name: 'Derivatives & Options', enabled: true, priority: 2 },
453455
fintech: { name: 'Fintech & Trading Tech', enabled: true, priority: 2 },
@@ -739,6 +741,7 @@ const COMMODITY_PANELS: Record<string, PanelConfig> = {
739741
'live-news': { name: 'Commodity Headlines', enabled: true, priority: 1 },
740742
insights: { name: 'AI Commodity Insights', enabled: true, priority: 1 },
741743
'commodity-news': { name: 'Commodity News', enabled: true, priority: 1 },
744+
'liquidity-shifts': { name: 'Liquidity Shifts', enabled: true, priority: 1 },
742745
'gold-silver': { name: 'Gold & Silver', enabled: true, priority: 1 },
743746
energy: { name: 'Energy Markets', enabled: true, priority: 1 },
744747
'mining-news': { name: 'Mining News', enabled: true, priority: 1 },

‎src/locales/en.json‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2087,6 +2087,19 @@
20872087
"cotPositioning": {
20882088
"infoTooltip": "<strong>CFTC COT Positioning</strong> Commitments of Traders report: net positioning of commercials, large speculators, and small traders across commodity and financial futures. Published weekly (Friday). Source: CFTC."
20892089
},
2090+
"liquidityShifts": {
2091+
"title": "Liquidity Shifts",
2092+
"infoTooltip": "<strong>Liquidity Shifts</strong> High-liquidity positioning and daily shift monitor for oil, gold, silver, equity index futures, and top US stocks. COT asset-manager net and leveraged-funds net vs daily price change.",
2093+
"cotSection": "Oil / Gold / Silver + Index Positioning (COT)",
2094+
"stocksSection": "Top US Stocks: Daily Shift",
2095+
"longShort": "Long {{long}} / Short {{short}}",
2096+
"lev": "Lev",
2097+
"noCot": "No COT rows available.",
2098+
"noStocks": "No top stock quotes available.",
2099+
"reportDate": "COT report date: {{date}}",
2100+
"unavailable": "Liquidity data unavailable",
2101+
"failed": "Failed to load"
2102+
},
20902103
"goldIntelligence": {
20912104
"infoTooltip": "<strong>Gold Intelligence</strong> Gold spot price, gold/silver ratio, gold vs platinum premium, cross-currency XAU prices (EUR, GBP, JPY, CNY, INR, CHF), and CFTC managed money positioning. Sources: Yahoo Finance, CFTC COT."
20922105
},

‎src/styles/main.css‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6163,6 +6163,42 @@ body.playback-mode .status-dot {
61636163
color: var(--red);
61646164
}
61656165

6166+
.liquidity-shifts-panel {
6167+
padding: 10px 14px;
6168+
}
6169+
6170+
.liquidity-shifts-panel__section-title {
6171+
font-size: 11px;
6172+
font-weight: 700;
6173+
margin-bottom: 6px;
6174+
}
6175+
6176+
.liquidity-shifts-panel__section-title--gap {
6177+
margin-top: 10px;
6178+
}
6179+
6180+
.liquidity-row {
6181+
display: flex;
6182+
justify-content: space-between;
6183+
gap: 8px;
6184+
align-items: center;
6185+
padding: 8px 0;
6186+
}
6187+
6188+
.liquidity-row__values {
6189+
text-align: right;
6190+
}
6191+
6192+
.liquidity-stock-row {
6193+
display: flex;
6194+
justify-content: space-between;
6195+
}
6196+
6197+
.liquidity-report-date {
6198+
margin-top: 8px;
6199+
text-align: right;
6200+
}
6201+
61666202
.commodity-item .mini-sparkline {
61676203
display: block;
61686204
margin: 2px 0;

0 commit comments

Comments
 (0)