From 6cb109051ac1efe2e6e830b1dae94d1bbd56f8a5 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:03:42 +0800 Subject: [PATCH 01/20] feat(image-analysis): Implement MLLM-based image feature computation for trading insights --- .../agents/common/trading/constants.py | 1 + .../agents/common/trading/data/interfaces.py | 15 + .../agents/common/trading/data/screenshot.py | 140 + .../trading/features/configs/charts.json | 3150 +++++++++++++++++ .../agents/common/trading/features/image.py | 141 + .../common/trading/features/interfaces.py | 31 +- .../common/trading/features/pipeline.py | 82 +- .../valuecell/agents/common/trading/models.py | 2 +- python/valuecell/utils/model.py | 36 + 9 files changed, 3582 insertions(+), 16 deletions(-) create mode 100644 python/valuecell/agents/common/trading/data/screenshot.py create mode 100644 python/valuecell/agents/common/trading/features/configs/charts.json create mode 100644 python/valuecell/agents/common/trading/features/image.py diff --git a/python/valuecell/agents/common/trading/constants.py b/python/valuecell/agents/common/trading/constants.py index 74f262fa9..6dda52bb6 100644 --- a/python/valuecell/agents/common/trading/constants.py +++ b/python/valuecell/agents/common/trading/constants.py @@ -15,3 +15,4 @@ FEATURE_GROUP_BY_KEY = "group_by_key" FEATURE_GROUP_BY_INTERVAL_PREFIX = "interval_" FEATURE_GROUP_BY_MARKET_SNAPSHOT = "market_snapshot" +FEATURE_GROUP_BY_IMAGE_ANALYSIS = "image_analysis" diff --git a/python/valuecell/agents/common/trading/data/interfaces.py b/python/valuecell/agents/common/trading/data/interfaces.py index cbad57a47..f97f53233 100644 --- a/python/valuecell/agents/common/trading/data/interfaces.py +++ b/python/valuecell/agents/common/trading/data/interfaces.py @@ -3,6 +3,8 @@ from abc import ABC, abstractmethod from typing import List +from agno.media import Image + from valuecell.agents.common.trading.models import Candle, MarketSnapShotType # Contracts for market data sources (module-local abstract interfaces). @@ -42,3 +44,16 @@ async def get_market_snapshot(self, symbols: List[str]) -> MarketSnapShotType: """ raise NotImplementedError + + +class BaseScreenshotDataSource(ABC): + """ + Abstract base class for screenshot data sources. + """ + + @abstractmethod + async def capture(self, *args, **kwargs) -> Image: + """ + Captures a screenshot and returns an agno.media.Image object. + """ + raise NotImplementedError diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py new file mode 100644 index 000000000..0abd3524b --- /dev/null +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -0,0 +1,140 @@ +import asyncio +import os +from datetime import datetime +from typing import Optional + +from agno.media import Image +from loguru import logger +from playwright.async_api import Browser, Page, Playwright, async_playwright + +from .interfaces import BaseScreenshotDataSource + + +class PlaywrightScreenshotDataSource(BaseScreenshotDataSource): + """ + Concrete implementation using Playwright. + Implements Async Context Manager protocol for automatic setup and teardown. + """ + + def __init__(self, target_url: str, file_path: str): + """ + Initializes configuration. + """ + self.target_url = target_url + self.file_path = file_path + + self.playwright: Optional[Playwright] = None + self.browser: Optional[Browser] = None + self.page: Optional[Page] = None + + # Ensure dummy file exists if not present + if not os.path.exists(self.file_path): + logger.warning(f"File {self.file_path} not found. Creating empty JSON file.") + with open(self.file_path, "w") as f: + f.write("{}") + + async def __aenter__(self): + """ + Magic method for 'async with'. + Starts the browser, navigates to the URL, and performs the setup automation. + """ + try: + logger.info("Initializing Playwright session...") + self.playwright = await async_playwright().start() + self.browser = await self.playwright.chromium.launch(headless=True) + + context = await self.browser.new_context(viewport={"width": 1600, "height": 900}) + self.page = await context.new_page() + + logger.info(f"Navigating to {self.target_url}") + await self.page.goto(self.target_url, wait_until="networkidle") + + logger.info("Waiting for core UI elements...") + # Wait for the green menu button to ensure page load + menu_btn = self.page.locator("#menu .menu__button") + await menu_btn.wait_for(state="visible", timeout=60000) + + logger.info("Page loaded. Executing setup sequence.") + + # 1. Click Menu + await menu_btn.click() + + # 2. Click Settings + await self.page.get_by_text("Settings", exact=True).click() + + # 3. Click New + await self.page.locator("button").filter(has_text="New").click() + + # 4. Handle File Upload + logger.info("Uploading file...") + async with self.page.expect_file_chooser() as fc_info: + await self.page.get_by_text("Upload template file").click() + + file_chooser = await fc_info.value + await file_chooser.set_files(self.file_path) + + # Wait slightly for UI render + await asyncio.sleep(1) + + # 5. Click IMPORT + logger.info("Confirming import...") + import_btn = self.page.locator("button").filter(has_text="IMPORT") + await import_btn.wait_for(state="visible") + await import_btn.click() + + logger.info("Import successful. Waiting for modal to close...") + await asyncio.sleep(3) + + return self + + except Exception as e: + logger.error(f"Error during initialization: {e}") + # Ensure cleanup happens if initialization fails mid-way + await self._cleanup() + raise e + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """ + Magic method for 'async with'. + Handles cleanup of browser resources. + """ + if exc_type: + logger.error(f"Exiting session due to exception: {exc_val}") + + await self._cleanup() + logger.info("Session closed.") + + async def _cleanup(self): + """ + Internal helper to close browser resources. + """ + if self.browser: + await self.browser.close() + if self.playwright: + await self.playwright.stop() + + async def capture(self, *args, **kwargs) -> Image: + """ + Captures the current state of the page. + """ + if not self.page: + raise RuntimeError("Page is not initialized. Use 'async with' context.") + + try: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + logger.info(f"Capturing screenshot at {timestamp}...") + + # Capture screenshot bytes + screenshot_bytes = await self.page.screenshot(full_page=True) + + # Create agno Image object + # Assuming Image can be initialized with content/bytes. + # If agno.media.Image requires a file path, we would save it to disk first. + image_obj = Image(content=screenshot_bytes) + + logger.info("Screenshot captured successfully.") + return image_obj + + except Exception as e: + logger.error(f"Failed to capture screenshot: {e}") + raise e diff --git a/python/valuecell/agents/common/trading/features/configs/charts.json b/python/valuecell/agents/common/trading/features/configs/charts.json new file mode 100644 index 000000000..997032df6 --- /dev/null +++ b/python/valuecell/agents/common/trading/features/configs/charts.json @@ -0,0 +1,3150 @@ +{ + "version": 1, + "createdAt": 1764666200000, + "updatedAt": 1764666205739, + "name": "Charts", + "id": "charts", + "states": { + "panes": { + "_id": "panes", + "locked": false, + "layout": [ + { + "i": "chart", + "type": "chart", + "x": 0, + "y": 0, + "w": 24, + "h": 23, + "moved": false + }, + { + "i": "delta", + "type": "chart", + "x": 0, + "y": 23, + "w": 24, + "h": 18, + "moved": false + }, + { + "i": "chart copy 1", + "type": "chart", + "x": 0, + "y": 41, + "w": 24, + "h": 17, + "moved": false + } + ], + "panes": { + "chart": { + "id": "chart", + "name": "AGGR", + "type": "chart", + "markets": [ + "BINANCE_FUTURES:btcusd_perp", + "BINANCE_FUTURES:btcusdc", + "BINANCE_FUTURES:btcusdt", + "BINANCE:btcfdusd", + "BINANCE:btctusd", + "BINANCE:btcusdc", + "BINANCE:btcusdt", + "BITFINEX:BTCF0:USTF0", + "BITFINEX:BTCUSD", + "BITFINEX:BTCUST", + "BITGET:BTCPERP_CMCBL", + "BITGET:BTCUSD_DMCBL", + "BITGET:BTCUSDC", + "BITGET:BTCUSDT", + "BITGET:BTCUSDT_UMCBL", + "BITMEX:XBT_USDT", + "BITMEX:XBTUSD", + "BITMEX:XBTUSDT", + "BITSTAMP:btcusd", + "BITSTAMP:btcusdc", + "BITSTAMP:btcusdt", + "BYBIT:BTCUSD", + "BYBIT:BTCUSDC-SPOT", + "BYBIT:BTCUSDT", + "BYBIT:BTCUSDT-SPOT", + "COINBASE:BTC-USD", + "COINBASE:BTC-USDC", + "COINBASE:BTC-USDT", + "DERIBIT:BTC_USDC-PERPETUAL", + "DERIBIT:BTC-PERPETUAL", + "DYDX:BTC-USD", + "HUOBI:BTC-USD", + "HUOBI:BTC-USDT", + "HUOBI:btcusdc", + "HUOBI:btcusdd", + "HUOBI:btcusdt", + "KRAKEN:PF_XBTUSD", + "KRAKEN:PI_XBTUSD", + "KRAKEN:XBT/USD", + "KRAKEN:XBT/USDC", + "KRAKEN:XBT/USDT", + "KUCOIN:BTC-USDC", + "KUCOIN:BTC-USDT", + "KUCOIN:XBTUSDCM", + "KUCOIN:XBTUSDM", + "KUCOIN:XBTUSDTM", + "MEXC:BTC_USD", + "MEXC:BTC_USDT", + "MEXC:BTCUSDC", + "MEXC:BTCUSDT", + "OKEX:BTC-USD-SWAP", + "OKEX:BTC-USDC", + "OKEX:BTC-USDC-SWAP", + "OKEX:BTC-USDT", + "OKEX:BTC-USDT-SWAP", + "PHEMEX:BTCUSD", + "PHEMEX:BTCUSDT", + "POLONIEX:BTC_TUSD", + "POLONIEX:BTC_USDC", + "POLONIEX:BTC_USDD", + "POLONIEX:BTC_USDT" + ], + "zoom": 1 + }, + "delta": { + "id": "delta", + "name": "PAIRS", + "type": "chart", + "zoom": 1, + "settings": { + "indicatorsErrors": {}, + "indicators": { + "_rmg9m2zvgvdyfzgl": { + "enabled": true, + "name": "Liquidations", + "description": "Liquidations by side", + "script": "plothistogram(lbuy, color=options.upColor)\nplothistogram(-lsell, color=options.downColor)", + "options": { + "priceScaleId": "_rmg9m2zvgvdyfzgl", + "priceFormat": { + "type": "volume" + }, + "upColor": "rgb(100,181,246)", + "downColor": "rgb(240,98,146)", + "scaleMargins": { + "top": 0.87, + "bottom": 0 + }, + "baseLineVisible": false, + "lastValueVisible": true, + "priceLineVisible": false + }, + "id": "_rmg9m2zvgvdyfzgl", + "createdAt": 1648450392186, + "updatedAt": 1713565132907, + "uses": 2, + "series": [ + "liquidations", + "5hbxmosl" + ], + "displayName": "Liquidations", + "optionsDefinitions": {}, + "unsavedChanges": false, + "preview": {}, + "libraryId": "liquidations" + }, + "_7cd5k7cv9ka4qp0s": { + "id": "_7cd5k7cv9ka4qp0s", + "libraryId": "delta-binance-spot", + "name": "BINANCE SPOT USDT", + "script": "_vbuy = (BINANCE:btcusdt.vbuy)\n_vsell = (BINANCE:btcusdt.vsell)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\"BN.S”)", + "createdAt": 1649330197719, + "updatedAt": 1713583022144, + "options": { + "priceScaleId": "_7cd5k7cv9ka4qp0s", + "upColor": "rgb(100,181,246)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 14, + "upColorHighVol": "rgb(100,181,246)", + "upColorLowVol": "rgb(10,96,162)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "scaleMargins": { + "top": 0.14, + "bottom": 0.75 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "delta-binance-spot" + ], + "displayName": "BINANCE SPOT USDT", + "unsavedChanges": false + }, + "_kvcozjh87f50e1gs": { + "id": "_kvcozjh87f50e1gs", + "libraryId": "_7cd5k7cv9ka4qp0s", + "name": "BINANCE SPOT FDUSD", + "script": "_vbuy = (BINANCE:btcfdusd.vbuy)\n_vsell = (BINANCE:btcfdusd.vsell)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\"BN.FD”)", + "createdAt": 1713560579658, + "updatedAt": 1713570376824, + "options": { + "priceScaleId": "_kvcozjh87f50e1gs", + "upColor": "rgb(100,181,246)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 14, + "upColorHighVol": "rgb(100,181,246)", + "upColorLowVol": "rgb(10,96,162)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "scaleMargins": { + "top": 0.02, + "bottom": 0.86 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "_7cd5k7cv9ka4qp0s" + ], + "displayName": "BINANCE SPOT FDUSD", + "unsavedChanges": true + }, + "_mdzsxr1kv4zlvk1x": { + "id": "_mdzsxr1kv4zlvk1x", + "libraryId": "_kvcozjh87f50e1gs", + "name": "CB", + "script": "_vbuy = (COINBASE:BTC-USD.vbuy+COINBASE:BTC-USDC.vbuy+COINBASE:BTC-USDT.vbuy)\n_vsell = (COINBASE:BTC-USD.vsell+COINBASE:BTC-USDC.vsell+COINBASE:BTC-USDT.vsell)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\"CB\")", + "createdAt": 1713560951314, + "updatedAt": 1713568342377, + "options": { + "priceScaleId": "_mdzsxr1kv4zlvk1x", + "upColor": "rgb(100,181,246)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 14, + "upColorHighVol": "rgb(100,181,246)", + "upColorLowVol": "rgb(10,96,162)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "scaleMargins": { + "top": 0.25, + "bottom": 0.63 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "displayName": "CB", + "series": [ + "_kvcozjh87f50e1gs" + ], + "unsavedChanges": true + }, + "_krppkscmyqxrjfyd": { + "id": "_krppkscmyqxrjfyd", + "libraryId": "_mdzsxr1kv4zlvk1x", + "name": "BF", + "script": "_vbuy = (BITFINEX:BTCUSD.vbuy+BITFINEX:BTCUST.vbuy)\n_vsell = (BITFINEX:BTCUSD.vsell+BITFINEX:BTCUST.vsell)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\"BF\")", + "createdAt": 1713561083390, + "updatedAt": 1713561205371, + "options": { + "priceScaleId": "_krppkscmyqxrjfyd", + "upColor": "rgb(100,181,246)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 14, + "upColorHighVol": "rgb(100,181,246)", + "upColorLowVol": "rgb(10,96,162)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "scaleMargins": { + "top": 0.37, + "bottom": 0.51 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "_mdzsxr1kv4zlvk1x" + ], + "displayName": "BF", + "unsavedChanges": true + }, + "_9yf0uhfo6ak1ynmb": { + "id": "_9yf0uhfo6ak1ynmb", + "libraryId": "bns", + "name": "CVD BN.S", + "script": "plotline(cum(BINANCE:btcusdt.vbuy - BINANCE:btcusdt.vsell), title=BN.S)", + "createdAt": 1713582936533, + "updatedAt": 1713583364683, + "options": { + "priceScaleId": "_9yf0uhfo6ak1ynmb", + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "color": "rgb(100,181,246)", + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "visible": true + }, + "optionsDefinitions": {}, + "series": [ + "bns" + ], + "displayName": "CVD BN.S", + "unsavedChanges": false + }, + "_tvmy75hrwj7387ye": { + "id": "_tvmy75hrwj7387ye", + "libraryId": "bnfd", + "name": "CVD BN.FD", + "script": "plotline(cum(BINANCE:btcfdusd.vbuy - BINANCE:btcfdusd.vsell), title=BN.FD)", + "createdAt": 1713582982235, + "updatedAt": 1713644723785, + "options": { + "priceScaleId": "_tvmy75hrwj7387ye", + "color": "rgb(10,96,162)", + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "visible": true + }, + "optionsDefinitions": {}, + "series": [ + "bnfd" + ], + "displayName": "CVD BN.FD", + "unsavedChanges": false + }, + "_shfiapc2wfqzrlq1": { + "id": "_shfiapc2wfqzrlq1", + "libraryId": "cvd-cb", + "name": "CVD CB", + "script": "plotline(cum(COINBASE:BTC-USD.vbuy - COINBASE:BTC-USD.vsell), title=CB)", + "createdAt": 1713583125412, + "updatedAt": 1713583357548, + "options": { + "priceScaleId": "_shfiapc2wfqzrlq1", + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "color": "rgb(129,199,132)", + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "visible": true + }, + "optionsDefinitions": {}, + "series": [ + "cvd-cb" + ], + "displayName": "CVD CB", + "unsavedChanges": false + }, + "_igcwun84g630m5nk": { + "id": "_igcwun84g630m5nk", + "libraryId": "cvd-bnp", + "name": "CVD BN.P", + "script": "plotline(cum(BINANCE_FUTURES:btcusdt.vbuy - BINANCE_FUTURES:btcusdt.vsell), title=BN.P)", + "createdAt": 1713583209180, + "updatedAt": 1713583450753, + "options": { + "priceScaleId": "_igcwun84g630m5nk", + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "color": "rgb(232,219,125)", + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "visible": true + }, + "optionsDefinitions": {}, + "series": [ + "cvd-bnp" + ], + "displayName": "CVD BN.P", + "unsavedChanges": false + }, + "_f8y8f2f4126fpzpd": { + "id": "_f8y8f2f4126fpzpd", + "libraryId": "cvd-bbp", + "name": "CVD BB.P", + "script": "plotline(cum(BYBIT:BTCUSDT.vbuy - BYBIT:BTCUSDT.vsell), title=BB.P)", + "createdAt": 1713583272290, + "updatedAt": 1713583383128, + "options": { + "priceScaleId": "_f8y8f2f4126fpzpd", + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "color": "rgb(240,98,146)", + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "visible": true + }, + "optionsDefinitions": {}, + "series": [ + "cvd-bbp" + ], + "displayName": "CVD BB.P", + "unsavedChanges": false + }, + "_e1esdjt32movky8x": { + "id": "_e1esdjt32movky8x", + "libraryId": "price", + "name": "Price", + "script": "plotcandlestick(options.useHeikinAshi ? avg_heikinashi(bar) : options.useGaps ? avg_ohlc_with_gaps(bar) : avg_ohlc(bar))", + "createdAt": 1696203148430, + "updatedAt": 1713643790838, + "options": { + "priceScaleId": "_e1esdjt32movky8x", + "priceFormat": { + "auto": true, + "precision": 1, + "minMove": 0.1 + }, + "priceLineVisible": false, + "lastValueVisible": true, + "borderVisible": true, + "upColor": "rgba(255,255,255,0.5)", + "downColor": "rgba(255,255,255,0)", + "borderUpColor": "rgba(255,255,255,0.5)", + "borderDownColor": "rgba(255,255,255,0.5)", + "wickUpColor": "rgba(255,255,255,0.5)", + "wickDownColor": "rgba(255,255,255,0.5)", + "useGaps": false, + "useHeikinAshi": false, + "scaleMargins": { + "top": 0.49, + "bottom": 0.35 + }, + "priceLineColor": "rgba(255,255,255,0.5)", + "visible": true, + "baseLineVisible": false + }, + "optionsDefinitions": {}, + "series": [ + "price" + ], + "displayName": "Price" + } + }, + "indicatorOrder": [ + "_rmg9m2zvgvdyfzgl", + "_7cd5k7cv9ka4qp0s", + "_kvcozjh87f50e1gs", + "_mdzsxr1kv4zlvk1x", + "_krppkscmyqxrjfyd", + "_9yf0uhfo6ak1ynmb", + "_tvmy75hrwj7387ye", + "_shfiapc2wfqzrlq1", + "_igcwun84g630m5nk", + "_f8y8f2f4126fpzpd", + "_e1esdjt32movky8x" + ], + "priceScales": { + "right": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.35 + }, + "indicators": [ + "Liquidation Heatmap" + ] + }, + "volume_liquidations": { + "scaleMargins": { + "top": 0.1, + "bottom": 0.2 + }, + "indicators": [ + "Liquidations" + ] + }, + "volume": { + "scaleMargins": { + "top": 0.84, + "bottom": 0 + } + }, + "delta-binance-spot": { + "scaleMargins": { + "top": 0, + "bottom": 0.88 + }, + "indicators": [ + "BINANCE SPOT FDUSD" + ], + "mode": 0 + }, + "_7cd5k7cv9ka4qp0s": { + "scaleMargins": { + "top": 0.14, + "bottom": 0.75 + }, + "indicators": [ + "BINANCE SPOT USDT" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "_kvcozjh87f50e1gs": { + "scaleMargins": { + "top": 0.02, + "bottom": 0.86 + }, + "indicators": [ + "BINANCE SPOT FDUSD" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "_mdzsxr1kv4zlvk1x": { + "scaleMargins": { + "top": 0.25, + "bottom": 0.63 + }, + "indicators": [ + "CB" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "_krppkscmyqxrjfyd": { + "scaleMargins": { + "top": 0.37, + "bottom": 0.51 + }, + "indicators": [ + "BF" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "cvdperp": { + "scaleMargins": { + "top": 0.55, + "bottom": 0.17 + }, + "indicators": [ + "CVD (BTC PERP)" + ] + }, + "_rufs083y0fbvc7ay": { + "scaleMargins": { + "top": 0.54, + "bottom": 0.17 + }, + "indicators": [ + "CVD CB" + ] + }, + "_7dsa9mx4zmn1o9wi": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.22 + }, + "indicators": [ + "CVD CB" + ] + }, + "_5xwq57x2pjjgqg3a": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.22 + }, + "indicators": [ + "CVD BN.P" + ] + }, + "left": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.35 + } + }, + "_oq7moq2202szuuqs": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.22 + }, + "indicators": [ + "CVD BN.S" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": false + } + }, + "_9t6pmojrlx5wc9nt": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.22 + } + }, + "_wtj5a0lw9ypkovyp": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.22 + } + }, + "_blmgdmljx1z1o0tz": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.22 + } + }, + "_b9b5e0ifnnfuyomd": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.22 + } + }, + "_9yf0uhfo6ak1ynmb": { + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "indicators": [ + "CVD BN.S" + ] + }, + "_shfiapc2wfqzrlq1": { + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "indicators": [ + "CVD CB" + ] + }, + "_tvmy75hrwj7387ye": { + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "indicators": [ + "CVD BN.FD" + ] + }, + "_igcwun84g630m5nk": { + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "indicators": [ + "CVD BN.P" + ] + }, + "_f8y8f2f4126fpzpd": { + "scaleMargins": { + "top": 0.65, + "bottom": 0.14 + }, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "indicators": [ + "CVD BB.P" + ] + }, + "_aoqxqm6ynw4ta9gv": { + "scaleMargins": { + "top": 0.1, + "bottom": 0.2 + }, + "indicators": [ + "Price" + ] + }, + "_h6wn38lfbfomwaeb": { + "scaleMargins": { + "top": 0.1, + "bottom": 0.2 + } + }, + "_bdyzmtbhap9tidl2": { + "scaleMargins": { + "top": 0.1, + "bottom": 0.2 + }, + "indicators": [ + "cvd multi" + ] + }, + "_e1esdjt32movky8x": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.35 + }, + "indicators": [ + "Price" + ] + }, + "_rmg9m2zvgvdyfzgl": { + "scaleMargins": { + "top": 0.87, + "bottom": 0 + }, + "indicators": [ + "Liquidations" + ] + } + }, + "layouting": false, + "showIndicators": true, + "timeframe": "60", + "refreshRate": 1000, + "showAlerts": true, + "showAlertsLabel": true, + "showLegend": false, + "fillGapsWithEmpty": true, + "showHorizontalGridlines": false, + "horizontalGridlinesColor": "rgba(255,255,255,.1)", + "showVerticalGridlines": false, + "verticalGridlinesColor": "rgba(255,255,255,.1)", + "showWatermark": false, + "watermarkColor": "rgba(255,255,255,.1)", + "showBorder": true, + "borderColor": null, + "textColor": null, + "showLeftScale": false, + "showRightScale": true, + "showTimeScale": true, + "hiddenMarkets": { + "COINBASE:BTC-USD": false + }, + "barSpacing": 0.2599956189007877 + }, + "markets": [ + "BINANCE_FUTURES:btcusd_perp", + "BINANCE_FUTURES:btcusdc", + "BINANCE_FUTURES:btcusdt", + "BINANCE:btcfdusd", + "BINANCE:btctusd", + "BINANCE:btcusdc", + "BINANCE:btcusdt", + "BITFINEX:BTCF0:USTF0", + "BITFINEX:BTCUSD", + "BITFINEX:BTCUST", + "BITGET:BTCPERP_CMCBL", + "BITGET:BTCUSD_DMCBL", + "BITGET:BTCUSDC", + "BITGET:BTCUSDT", + "BITGET:BTCUSDT_UMCBL", + "BITMEX:XBT_USDT", + "BITMEX:XBTUSD", + "BITMEX:XBTUSDT", + "BITSTAMP:btcusd", + "BITSTAMP:btcusdc", + "BITSTAMP:btcusdt", + "BYBIT:BTCUSD", + "BYBIT:BTCUSDC-SPOT", + "BYBIT:BTCUSDT", + "BYBIT:BTCUSDT-SPOT", + "COINBASE:BTC-USD", + "COINBASE:BTC-USDC", + "COINBASE:BTC-USDT", + "DERIBIT:BTC_USDC-PERPETUAL", + "DERIBIT:BTC-PERPETUAL", + "DYDX:BTC-USD", + "HUOBI:BTC-USD", + "HUOBI:BTC-USDT", + "HUOBI:btcusdc", + "HUOBI:btcusdd", + "HUOBI:btcusdt", + "KRAKEN:PF_XBTUSD", + "KRAKEN:PI_XBTUSD", + "KRAKEN:XBT/USD", + "KRAKEN:XBT/USDC", + "KRAKEN:XBT/USDT", + "KUCOIN:BTC-USDC", + "KUCOIN:BTC-USDT", + "KUCOIN:XBTUSDCM", + "KUCOIN:XBTUSDM", + "KUCOIN:XBTUSDTM", + "MEXC:BTC_USD", + "MEXC:BTC_USDT", + "MEXC:BTCUSDC", + "MEXC:BTCUSDT", + "OKEX:BTC-USD-SWAP", + "OKEX:BTC-USDC", + "OKEX:BTC-USDC-SWAP", + "OKEX:BTC-USDT", + "OKEX:BTC-USDT-SWAP", + "PHEMEX:BTCUSD", + "PHEMEX:BTCUSDT", + "POLONIEX:BTC_TUSD", + "POLONIEX:BTC_USDC", + "POLONIEX:BTC_USDD", + "POLONIEX:BTC_USDT" + ] + }, + "chart copy 1": { + "id": "chart copy 1", + "name": "", + "type": "chart", + "markets": [ + "BINANCE_FUTURES:btcusd_perp", + "BINANCE_FUTURES:btcusdc", + "BINANCE_FUTURES:btcusdt", + "BINANCE:btcfdusd", + "BINANCE:btctusd", + "BINANCE:btcusdc", + "BINANCE:btcusdt", + "BITFINEX:BTCF0:USTF0", + "BITFINEX:BTCUSD", + "BITFINEX:BTCUST", + "BITGET:BTCPERP_CMCBL", + "BITGET:BTCUSD_DMCBL", + "BITGET:BTCUSDC", + "BITGET:BTCUSDT", + "BITGET:BTCUSDT_UMCBL", + "BITMEX:XBT_USDT", + "BITMEX:XBTUSD", + "BITMEX:XBTUSDT", + "BITSTAMP:btcusd", + "BITSTAMP:btcusdc", + "BITSTAMP:btcusdt", + "BYBIT:BTCUSD", + "BYBIT:BTCUSDC-SPOT", + "BYBIT:BTCUSDT", + "BYBIT:BTCUSDT-SPOT", + "COINBASE:BTC-USD", + "COINBASE:BTC-USDC", + "COINBASE:BTC-USDT", + "DERIBIT:BTC_USDC-PERPETUAL", + "DERIBIT:BTC-PERPETUAL", + "DYDX:BTC-USD", + "HUOBI:BTC-USD", + "HUOBI:BTC-USDT", + "HUOBI:btcusdc", + "HUOBI:btcusdd", + "HUOBI:btcusdt", + "KRAKEN:PF_XBTUSD", + "KRAKEN:PI_XBTUSD", + "KRAKEN:XBT/USD", + "KRAKEN:XBT/USDC", + "KRAKEN:XBT/USDT", + "KUCOIN:BTC-USDC", + "KUCOIN:BTC-USDT", + "KUCOIN:XBTUSDCM", + "KUCOIN:XBTUSDM", + "KUCOIN:XBTUSDTM", + "MEXC:BTC_USD", + "MEXC:BTC_USDT", + "MEXC:BTCUSDC", + "MEXC:BTCUSDT", + "OKEX:BTC-USD-SWAP", + "OKEX:BTC-USDC", + "OKEX:BTC-USDC-SWAP", + "OKEX:BTC-USDT", + "OKEX:BTC-USDT-SWAP", + "PHEMEX:BTCUSD", + "PHEMEX:BTCUSDT", + "POLONIEX:BTC_TUSD", + "POLONIEX:BTC_USDC", + "POLONIEX:BTC_USDD", + "POLONIEX:BTC_USDT" + ] + } + }, + "syncedWithParentFrame": [] + }, + "settings": { + "_id": "settings", + "preferQuoteCurrencySize": true, + "aggregationLength": 10, + "calculateSlippage": null, + "wsProxyUrl": null, + "disableAnimations": true, + "autoHideHeaders": true, + "autoHideNames": false, + "theme": "dark", + "backgroundColor": "rgb(25,25,25)", + "textColor": "", + "buyColor": "rgb(100, 157, 102)", + "sellColor": "rgb(239, 67, 82)", + "timezoneOffset": 7200000, + "useAudio": false, + "audioVolume": 0.1, + "audioFilters": { + "PingPongDelay": true, + "Compressor": false, + "Delay": false, + "HighPassFilter": true, + "LowPassFilter": false + }, + "sections": [ + "timeframe-minutes", + "search-type", + "timeframe-hours", + "indicator-right-scale", + "settings-trades", + "trades-display", + "trades-liquidations", + "search-extras", + "search-exchanges", + "search-quotes", + "settings-other", + "indicator-right-script", + "indicator-left-colors", + "indicator-left-script", + "indicator-left-other", + "indicator-right-default", + "indicator-right-format", + "indicator-right-colors", + "settings-workspaces" + ], + "searchTypes": { + "recentSearches": true, + "historical": false, + "spots": true, + "perpetuals": false, + "futures": false, + "normalize": false, + "mergeUsdt": false + }, + "searchQuotes": { + "USDT": false, + "USD": false, + "FDUSD": false, + "UST": false + }, + "previousSearchSelections": [ + { + "label": "COINBASE:BTCUSD", + "markets": [ + "COINBASE:BTC-USD", + "COINBASE:BTC-USDT" + ], + "count": 2 + }, + { + "label": "COINBASE:BTC-USD", + "markets": [ + "COINBASE:BTC-USD" + ], + "count": 0 + }, + { + "label": "BINANCE:BTCUSD", + "markets": [ + "BINANCE:btcfdusd", + "BINANCE:btctusd", + "BINANCE:btcusdt" + ], + "count": 3 + }, + { + "label": "BTCUSD", + "markets": [ + "BINANCE_FUTURES:btcusd_perp", + "BINANCE_FUTURES:btcusdc", + "BINANCE_FUTURES:btcusdt", + "BINANCE:btcfdusd", + "BINANCE:btctusd", + "BINANCE:btcusdc", + "BINANCE:btcusdt", + "BITFINEX:BTCF0:USTF0", + "BITFINEX:BTCUSD", + "BITFINEX:BTCUST", + "BITGET:BTCPERP_CMCBL", + "BITGET:BTCUSD_DMCBL", + "BITGET:BTCUSDC", + "BITGET:BTCUSDT", + "BITGET:BTCUSDT_UMCBL", + "BITMEX:XBT_USDT", + "BITMEX:XBTUSD", + "BITMEX:XBTUSDT", + "BITSTAMP:btcusd", + "BITSTAMP:btcusdc", + "BITSTAMP:btcusdt", + "BYBIT:BTCUSD", + "BYBIT:BTCUSDC-SPOT", + "BYBIT:BTCUSDT", + "BYBIT:BTCUSDT-SPOT", + "COINBASE:BTC-USD", + "COINBASE:BTC-USDC", + "COINBASE:BTC-USDT", + "DERIBIT:BTC_USDC-PERPETUAL", + "DERIBIT:BTC-PERPETUAL", + "DYDX:BTC-USD", + "HUOBI:BTC-USD", + "HUOBI:BTC-USDT", + "HUOBI:btcusdc", + "HUOBI:btcusdd", + "HUOBI:btcusdt", + "KRAKEN:PF_XBTUSD", + "KRAKEN:PI_XBTUSD", + "KRAKEN:XBT/USD", + "KRAKEN:XBT/USDC", + "KRAKEN:XBT/USDT", + "KUCOIN:BTC-USDC", + "KUCOIN:BTC-USDT", + "KUCOIN:XBTUSDCM", + "KUCOIN:XBTUSDM", + "KUCOIN:XBTUSDTM", + "MEXC:BTC_USD", + "MEXC:BTC_USDT", + "MEXC:BTCUSDC", + "MEXC:BTCUSDT", + "OKEX:BTC-USD-SWAP", + "OKEX:BTC-USDC", + "OKEX:BTC-USDC-SWAP", + "OKEX:BTC-USDT", + "OKEX:BTC-USDT-SWAP", + "PHEMEX:BTCUSD", + "PHEMEX:BTCUSDT", + "POLONIEX:BTC_TUSD", + "POLONIEX:BTC_USDC", + "POLONIEX:BTC_USDD", + "POLONIEX:BTC_USDT" + ], + "count": 61 + }, + { + "label": "BTCUSD", + "markets": [ + "BINANCE_FUTURES:btcusd_perp", + "BINANCE_FUTURES:btcusdc", + "BINANCE_FUTURES:btcusdt", + "BINANCE:btcusdc", + "BINANCE:btcusdt", + "BITFINEX:BTCF0:USTF0", + "BITFINEX:BTCUSD", + "BITFINEX:BTCUST", + "BITGET:BTCPERP_CMCBL", + "BITGET:BTCUSD_DMCBL", + "BITGET:BTCUSDC", + "BITGET:BTCUSDT", + "BITGET:BTCUSDT_UMCBL", + "BITMEX:XBT_USDT", + "BITMEX:XBTUSD", + "BITMEX:XBTUSDT", + "BITSTAMP:btcusd", + "BITSTAMP:btcusdc", + "BITSTAMP:btcusdt", + "BYBIT:BTCUSD", + "BYBIT:BTCUSDC-SPOT", + "BYBIT:BTCUSDT", + "BYBIT:BTCUSDT-SPOT", + "COINBASE:BTC-USD", + "COINBASE:BTC-USDC", + "COINBASE:BTC-USDT", + "DERIBIT:BTC_USDC-PERPETUAL", + "DERIBIT:BTC-PERPETUAL", + "DYDX:BTC-USD", + "HUOBI:BTC-USD", + "HUOBI:BTC-USDT", + "HUOBI:btcusdc", + "HUOBI:btcusdd", + "HUOBI:btcusdt", + "KRAKEN:PF_XBTUSD", + "KRAKEN:PI_XBTUSD", + "KRAKEN:XBT/USD", + "KRAKEN:XBT/USDC", + "KRAKEN:XBT/USDT", + "KUCOIN:BTC-USDC", + "KUCOIN:BTC-USDT", + "KUCOIN:XBTUSDCM", + "KUCOIN:XBTUSDM", + "KUCOIN:XBTUSDTM", + "MEXC:BTC_USD", + "MEXC:BTC_USDT", + "MEXC:BTCUSDC", + "MEXC:BTCUSDT", + "OKEX:BTC-USD-SWAP", + "OKEX:BTC-USDC", + "OKEX:BTC-USDC-SWAP", + "OKEX:BTC-USDT", + "OKEX:BTC-USDT-SWAP", + "PHEMEX:BTCUSD", + "PHEMEX:BTCUSDT", + "POLONIEX:BTC_USDC", + "POLONIEX:BTC_USDD", + "POLONIEX:BTC_USDT" + ], + "count": 58 + }, + { + "label": "AVAXUSD", + "markets": [ + "BINANCE_FUTURES:avaxusd_perp", + "BINANCE_FUTURES:avaxusdc", + "BINANCE_FUTURES:avaxusdt", + "BINANCE:avaxfdusd", + "BINANCE:avaxtusd", + "BINANCE:avaxusdc", + "BINANCE:avaxusdt", + "BITFINEX:AVAX:USD", + "BITFINEX:AVAX:UST", + "BITFINEX:AVAXF0:USTF0", + "BITGET:AVAXUSDC", + "BITGET:AVAXUSDT", + "BITGET:AVAXUSDT_UMCBL", + "BITMEX:AVAXUSD", + "BITMEX:AVAXUSDT", + "BITSTAMP:avaxusd", + "BYBIT:AVAXUSDC-SPOT", + "BYBIT:AVAXUSDT", + "BYBIT:AVAXUSDT-SPOT", + "COINBASE:AVAX-USD", + "COINBASE:AVAX-USDT", + "DERIBIT:AVAX_USDC-PERPETUAL", + "DYDX:AVAX-USD", + "HUOBI:AVAX-USDT", + "HUOBI:avaxusdc", + "HUOBI:avaxusdd", + "HUOBI:avaxusdt", + "KRAKEN:AVAX/USD", + "KRAKEN:AVAX/USDT", + "KRAKEN:PF_AVAXUSD", + "KUCOIN:AVAX-USDC", + "KUCOIN:AVAX-USDT", + "KUCOIN:AVAXUSDTM", + "MEXC:AVAX_USDT", + "MEXC:AVAXUSDC", + "MEXC:AVAXUSDT", + "OKEX:AVAX-USD-SWAP", + "OKEX:AVAX-USDC", + "OKEX:AVAX-USDT", + "OKEX:AVAX-USDT-SWAP", + "PHEMEX:AVAXUSD", + "PHEMEX:AVAXUSDT", + "POLONIEX:AVAX_USDC", + "POLONIEX:AVAX_USDT" + ], + "count": 44 + }, + { + "label": "BTCUSD", + "markets": [ + "BINANCE:btcfdusd", + "BINANCE:btcusdc", + "BINANCE:btcusdt", + "BITFINEX:BTCUSD", + "BITFINEX:BTCUST", + "BITGET:BTCUSDC", + "BITGET:BTCUSDT", + "BITSTAMP:btcusd", + "BITSTAMP:btcusdt", + "BYBIT:BTCUSDC-SPOT", + "BYBIT:BTCUSDT-SPOT", + "COINBASE:BTC-USD", + "COINBASE:BTC-USDT", + "HUOBI:btcusdc", + "HUOBI:btcusdt", + "KRAKEN:XBT/USD", + "KRAKEN:XBT/USDC", + "KRAKEN:XBT/USDT", + "KUCOIN:BTC-USDC", + "KUCOIN:BTC-USDT", + "MEXC:BTCUSDT", + "OKEX:BTC-USDC", + "OKEX:BTC-USDT", + "POLONIEX:BTC_USDC", + "POLONIEX:BTC_USDD", + "POLONIEX:BTC_USDT" + ], + "count": 26 + }, + { + "label": "BTCUSD", + "markets": [ + "BINANCE_FUTURES:btcusd_perp", + "BINANCE_FUTURES:btcusdt", + "BITFINEX:BTCF0:USTF0", + "BITGET:BTCPERP_CMCBL", + "BITGET:BTCUSD_DMCBL", + "BITGET:BTCUSDT_UMCBL", + "BITMEX:XBT_USDT", + "BITMEX:XBTUSD", + "BITMEX:XBTUSDT", + "BYBIT:BTCUSD", + "BYBIT:BTCUSDT", + "DERIBIT:BTC_USDC-PERPETUAL", + "DERIBIT:BTC-PERPETUAL", + "HUOBI:BTC-USD", + "HUOBI:BTC-USDT", + "KRAKEN:PI_XBTUSD", + "KUCOIN:XBTUSDM", + "KUCOIN:XBTUSDTM", + "MEXC:BTC_USD", + "MEXC:BTC_USDT", + "OKEX:BTC-USD-SWAP", + "OKEX:BTC-USDC-SWAP", + "OKEX:BTC-USDT-SWAP" + ], + "count": 23 + }, + { + "label": "BTCUSD", + "markets": [ + "BINANCE_FUTURES:btcusd_perp", + "BINANCE_FUTURES:btcusdt", + "BINANCE:btcfdusd", + "BINANCE:btcusdc", + "BINANCE:btcusdt", + "BITFINEX:BTCF0:USTF0", + "BITFINEX:BTCUSD", + "BITFINEX:BTCUST", + "BITGET:BTCPERP_CMCBL", + "BITGET:BTCUSD_DMCBL", + "BITGET:BTCUSDC", + "BITGET:BTCUSDT", + "BITGET:BTCUSDT_UMCBL", + "BITMEX:XBT_USDT", + "BITMEX:XBTUSD", + "BITMEX:XBTUSDT", + "BITSTAMP:btcusd", + "BITSTAMP:btcusdt", + "BYBIT:BTCUSD", + "BYBIT:BTCUSDC-SPOT", + "BYBIT:BTCUSDT", + "BYBIT:BTCUSDT-SPOT", + "COINBASE:BTC-USD", + "COINBASE:BTC-USDT", + "DERIBIT:BTC_USDC-PERPETUAL", + "DERIBIT:BTC-PERPETUAL", + "HUOBI:BTC-USD", + "HUOBI:BTC-USDT", + "HUOBI:btcusdc", + "HUOBI:btcusdt", + "KRAKEN:PI_XBTUSD", + "KRAKEN:XBT/USD", + "KRAKEN:XBT/USDC", + "KRAKEN:XBT/USDT", + "KUCOIN:BTC-USDC", + "KUCOIN:BTC-USDT", + "KUCOIN:XBTUSDM", + "KUCOIN:XBTUSDTM", + "MEXC:BTC_USD", + "MEXC:BTC_USDT", + "MEXC:BTCUSDT", + "OKEX:BTC-USD-SWAP", + "OKEX:BTC-USDC", + "OKEX:BTC-USDC-SWAP", + "OKEX:BTC-USDT", + "OKEX:BTC-USDT-SWAP", + "POLONIEX:BTC_USDC", + "POLONIEX:BTC_USDD", + "POLONIEX:BTC_USDT" + ], + "count": 49 + }, + { + "label": "BTCUSD", + "markets": [ + "BINANCE_FUTURES:btcusd_perp", + "BINANCE_FUTURES:btcusdt", + "BINANCE:btcusdt", + "BITFINEX:BTCF0:USTF0", + "BITFINEX:BTCUSD", + "BITFINEX:BTCUST", + "BITMEX:XBTUSD", + "BITMEX:XBTUSDT", + "BITSTAMP:btcusd", + "BYBIT:BTCUSD", + "BYBIT:BTCUSDT", + "COINBASE:BTC-USD", + "COINBASE:BTC-USDT", + "DERIBIT:BTC-PERPETUAL", + "KRAKEN:PI_XBTUSD", + "OKEX:BTC-USD-SWAP", + "OKEX:BTC-USDT-SWAP" + ], + "count": 17 + } + ], + "searchExchanges": { + "AGGR": false, + "BINANCE": false, + "BINANCE_FUTURES": false, + "BINANCE_US": false, + "BITFINEX": false, + "BITGET": false, + "BITMART": false, + "BITMEX": false, + "BITSTAMP": false, + "BYBIT": false, + "COINBASE": true, + "CRYPTOCOM": false, + "DERIBIT": false, + "DYDX": false, + "GATEIO": false, + "HITBTC": false, + "HUOBI": false, + "KRAKEN": false, + "KUCOIN": false, + "MEXC": false, + "OKEX": false, + "PHEMEX": false, + "POLONIEX": false, + "UNISWAP": false + }, + "timeframes": [ + { + "label": "1s", + "value": "1" + }, + { + "label": "3s", + "value": "3" + }, + { + "label": "5s", + "value": "5" + }, + { + "label": "10s", + "value": "10" + }, + { + "label": "15s", + "value": "15" + }, + { + "label": "30s", + "value": "30" + }, + { + "label": "1m", + "value": "60" + }, + { + "label": "3m", + "value": "180" + }, + { + "label": "5m", + "value": "300" + }, + { + "label": "15m", + "value": "900" + }, + { + "label": "21m", + "value": "1260" + }, + { + "label": "30m", + "value": "1800" + }, + { + "label": "1h", + "value": "3600" + }, + { + "label": "2h", + "value": "7200" + }, + { + "label": "4h", + "value": "14400" + }, + { + "label": "6h", + "value": "21600" + }, + { + "label": "8h", + "value": "28800" + }, + { + "label": "12h", + "value": "43200" + }, + { + "label": "1d", + "value": "86400" + }, + { + "label": "21 ticks", + "value": "21t" + }, + { + "label": "50 ticks", + "value": "50t" + }, + { + "label": "89 ticks", + "value": "89t" + }, + { + "label": "100 ticks", + "value": "100t" + }, + { + "label": "200 ticks", + "value": "200t" + }, + { + "label": "610 ticks", + "value": "610t" + }, + { + "label": "1000 ticks", + "value": "1000t" + }, + { + "label": "1597 ticks", + "value": "1597t" + } + ], + "favoriteTimeframes": { + "60": "1m", + "300": "5m", + "900": "15m", + "3600": "1h", + "7200": "2h", + "14400": "4h" + }, + "normalizeWatermarks": false, + "alerts": false, + "alertsColor": "rgb(0,255,0)", + "alertsLineStyle": 1, + "alertsLineWidth": 1, + "alertsClick": false, + "alertSound": null, + "showThresholdsAsTable": false, + "indicatorDialogNavigation": "{\"optionsQuery\":\"\",\"editorOptions\":{},\"columnWidth\":240,\"tab\":\"options\"}" + }, + "exchanges": { + "AGGR": { + "disabled": false + }, + "BINANCE": { + "disabled": false + }, + "BINANCE_FUTURES": { + "disabled": false + }, + "BINANCE_US": { + "disabled": true + }, + "BITFINEX": { + "disabled": false + }, + "BITGET": { + "disabled": false + }, + "BITMART": { + "disabled": true + }, + "BITMEX": { + "disabled": false + }, + "BITSTAMP": { + "disabled": false + }, + "BYBIT": { + "disabled": false + }, + "COINBASE": { + "disabled": false + }, + "CRYPTOCOM": { + "disabled": true + }, + "DERIBIT": { + "disabled": false + }, + "DYDX": { + "disabled": false + }, + "GATEIO": { + "disabled": true + }, + "HITBTC": { + "disabled": true + }, + "HUOBI": { + "disabled": false + }, + "KRAKEN": { + "disabled": false + }, + "KUCOIN": { + "disabled": false + }, + "MEXC": { + "disabled": false + }, + "OKEX": { + "disabled": false + }, + "PHEMEX": { + "disabled": false + }, + "POLONIEX": { + "disabled": false + }, + "UNISWAP": { + "disabled": true + }, + "_id": "exchanges" + }, + "chart copy 1": { + "indicatorsErrors": {}, + "indicators": { + "_yc5ewfpz6pjfe1ru": { + "enabled": true, + "name": "Price", + "script": "plotcandlestick(avg_ohlc(bar))", + "options": { + "priceScaleId": "right", + "priceFormat": { + "auto": true, + "precision": 1, + "minMove": 0.1 + }, + "priceLineVisible": false, + "lastValueVisible": false, + "borderVisible": true, + "upColor": "rgba(255,255,255,0.7)", + "downColor": "rgb(0,0,0)", + "borderUpColor": "rgba(255,255,255,0.5)", + "borderDownColor": "rgba(255,255,255,0.5)", + "wickUpColor": "rgba(255,255,255,0.5)", + "wickDownColor": "rgba(255,255,255,0.5)", + "useGaps": false, + "useHeikinAshi": false, + "visible": false, + "scaleMargins": { + "top": 0.03, + "bottom": 0.73 + } + }, + "id": "_yc5ewfpz6pjfe1ru", + "createdAt": 1697564385199, + "updatedAt": 1716533408000, + "libraryId": "price", + "series": [ + "price" + ], + "optionsDefinitions": {}, + "unsavedChanges": true + }, + "cvd-exchange-BINANCE": { + "id": "cvd-exchange-BINANCE", + "libraryId": "cvd-exchange-BINANCE", + "name": "cvd-exchange-BINANCE", + "script": "_vbuy=(BINANCE:btcusdt.vbuy+BINANCE:btcbusd.vbuy+BINANCE:btcusdc.vbuy)\n_vsell=(BINANCE:btcusdt.vsell+BINANCE:btcbusd.vsell+BINANCE:btcusdc.vsell)\nline(cum(_vbuy-_vsell), title=BINANCE)", + "createdAt": 1716531496274, + "updatedAt": 1726323528484, + "options": { + "priceScaleId": "cvd-exchange-BINANCE", + "scaleMargins": { + "top": 0.04, + "bottom": 0.74 + }, + "color": "rgb(245,127,23)", + "visible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "lastValueVisible": true + }, + "optionsDefinitions": {}, + "series": [ + "cvd-exchange-BINANCE" + ], + "displayName": "CVD Exchange BINANCE", + "unsavedChanges": false + }, + "cvd-exchange-BINANCE_FUTURES": { + "id": "cvd-exchange-BINANCE_FUTURES", + "libraryId": "cvd-exchange-BINANCE_FUTURES", + "name": "cvd-exchange-BINANCE_F", + "script": "_vbuy=(BINANCE_FUTURES:btcusdt.vbuy+BINANCE_FUTURES:btcbusd.vbuy+BINANCE_FUTURES:btcusd_perp.vbuy)\n_vsell=(BINANCE_FUTURES:btcusdt.vsell+BINANCE_FUTURES:btcbusd.vsell+BINANCE_FUTURES:btcusd_perp.vsell)\nline(cum(_vbuy-_vsell), title=BINANCE_FUTURES)", + "createdAt": 1716531496274, + "updatedAt": 1726323548438, + "options": { + "priceScaleId": "cvd-exchange-BINANCE_FUTURES", + "scaleMargins": { + "top": 0.05, + "bottom": 0.74 + }, + "visible": true, + "lastValueVisible": false + }, + "optionsDefinitions": {}, + "series": [ + "cvd-exchange-BINANCE_FUTURES" + ], + "displayName": "cvd-exchange-BINANCE_F", + "unsavedChanges": false + }, + "cvd-exchange-BYBIT": { + "id": "cvd-exchange-BYBIT", + "libraryId": "cvd-exchange-BYBIT", + "name": "cvd-exchange-BYBIT", + "script": "_vbuy=(BYBIT:BTCUSDT-SPOT.vbuy+BYBIT:BTCUSDC-SPOT.vbuy+BYBIT:BTCUSD.vbuy+BYBIT:BTCUSDT.vbuy)\n_vsell=(BYBIT:BTCUSDT-SPOT.vsell+BYBIT:BTCUSDC-SPOT.vsell+BYBIT:BTCUSD.vsell+BYBIT:BTCUSDT.vsell)\nline(cum(_vbuy-_vsell), title=BYBIT)", + "createdAt": 1716531496274, + "updatedAt": 1726323557557, + "options": { + "priceScaleId": "cvd-exchange-BYBIT", + "scaleMargins": { + "top": 0.29, + "bottom": 0.49 + }, + "color": "rgb(255,235,59)", + "visible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "lastValueVisible": true + }, + "optionsDefinitions": {}, + "series": [ + "cvd-exchange-BYBIT" + ], + "displayName": "CVD Exchange BYBIT", + "unsavedChanges": false + }, + "cvd-exchange-COINBASE": { + "id": "cvd-exchange-COINBASE", + "libraryId": "cvd-exchange-COINBASE", + "name": "cvd-exchange-COINBASE", + "script": "_vbuy=(COINBASE:BTC-USDT.vbuy+COINBASE:BTC-USD.vbuy)\n_vsell=(COINBASE:BTC-USDT.vsell+COINBASE:BTC-USD.vsell)\nline(cum(_vbuy-_vsell), title=COINBASE)", + "createdAt": 1716531496274, + "updatedAt": 1726323569625, + "options": { + "priceScaleId": "cvd-exchange-COINBASE", + "scaleMargins": { + "top": 0.54, + "bottom": 0.24 + }, + "color": "rgba(0,188,212,0.98)", + "visible": true, + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "cvd-exchange-COINBASE" + ], + "displayName": "CVD Exchange COINBASE", + "unsavedChanges": false + }, + "cvd-exchange-DERIBIT": { + "id": "cvd-exchange-DERIBIT", + "libraryId": "cvd-exchange-DERIBIT", + "name": "cvd-exchange-DERIBIT", + "script": "_vbuy=(DERIBIT:BTC-PERPETUAL.vbuy+DERIBIT:BTC_USDC-PERPETUAL.vbuy)\n_vsell=(DERIBIT:BTC-PERPETUAL.vsell+DERIBIT:BTC_USDC-PERPETUAL.vsell)\nline(cum(_vbuy-_vsell), title=DERIBIT)", + "createdAt": 1716531496274, + "updatedAt": 1718701896712, + "options": { + "priceScaleId": "cvd-exchange-DERIBIT", + "scaleMargins": { + "top": 0.29, + "bottom": 0.49 + }, + "color": "rgba(14,215,22,0.75)", + "visible": true + }, + "optionsDefinitions": {}, + "series": [ + "cvd-exchange-DERIBIT" + ], + "displayName": "CVD Exchange DERIBIT", + "unsavedChanges": false + }, + "_vrjve84cmmifpf2y": { + "id": "_vrjve84cmmifpf2y", + "libraryId": "bfs-cvd2", + "name": "BF.S CVD", + "script": "_vbuy = (BITFINEX:BTCUSD.vbuy+BITFINEX:BTCUST.vbuy)\n_vsell = (BITFINEX:BTCUSD.vsell+BITFINEX:BTCUST.vsell)\n\n\n\nline(cum(_vbuy - _vsell), title=bf.s )", + "createdAt": 1718763056740, + "updatedAt": 1726323584711, + "options": { + "priceScaleId": "_z516f0zwmtibbr76", + "scaleMargins": { + "top": 0.54, + "bottom": 0.24 + }, + "lastValueVisible": true, + "color": "rgb(41,98,255)", + "visible": true, + "priceLineVisible": false, + "baseLineVisible": false, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "bfs-cvd2" + ], + "displayName": "BF.S CVD", + "unsavedChanges": false + }, + "_tt3vmq14qq0q6zh3": { + "id": "_tt3vmq14qq0q6zh3", + "libraryId": "oks-cvd", + "name": "OK.S CVD", + "script": "_vbuy = (OKEX:BTC-USD.vbuy+OKEX:BTC-USDT.vbuy+OKEX:BTC-USDC.vbuy)\n_vsell = (OKEX:BTC-USD.vsell+OKEX:BTC-USDT.vsell+OKEX:BTC-USDC.vsell)\n\n\nline(cum(_vbuy - _vsell), title=ok.s )", + "createdAt": 1718762559127, + "updatedAt": 1726323595475, + "options": { + "priceScaleId": "_pshii8bunephrk55", + "scaleMargins": { + "top": 0.79, + "bottom": 0.01 + }, + "color": "rgb(91,156,246)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "oks-cvd" + ], + "displayName": "OK.S CVD", + "unsavedChanges": false + }, + "_a0uddpx1jizg6f4h": { + "id": "_a0uddpx1jizg6f4h", + "libraryId": "okp-cvd", + "name": "OK.P CVD", + "script": "_vbuy = (OKEX:BTC-USD-SWAP.vbuy+OKEX:BTC-USDT-SWAP.vbuy+OKEX:BTC-USDC-SWAP.vbuy)\n_vsell = (OKEX:BTC-USD-SWAP.vsell+OKEX:BTC-USDT-SWAP.vsell+OKEX:BTC-USDC-SWAP.vsell)\n\nline(cum(_vbuy - _vsell), title=ok.p )", + "createdAt": 1718762487919, + "updatedAt": 1726323856117, + "options": { + "priceScaleId": "_s99eqgk6zaglfjn7", + "scaleMargins": { + "top": 0.79, + "bottom": 0.01 + }, + "color": "rgb(152,111,223)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "okp-cvd" + ], + "displayName": "OK.P CVD", + "unsavedChanges": false + }, + "_zial46x5pdsip5ms": { + "id": "_zial46x5pdsip5ms", + "libraryId": "krakens-cvd", + "name": "KRAKEN.S CVD", + "script": "_vbuy = (KRAKEN:XBT/USD.vbuy)\n_vsell = (KRAKEN:XBT/USD.vsell)\n\n\n\nline(cum(_vbuy - _vsell), title=kraken.s )", + "createdAt": 1718763191990, + "updatedAt": 1726323606730, + "options": { + "priceScaleId": "_8d9u97zke39v6fz9", + "scaleMargins": { + "top": 0.53, + "bottom": 0.24 + }, + "color": "rgb(165,214,167)", + "visible": true, + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "krakens-cvd" + ], + "displayName": "KRAKEN.S CVD", + "unsavedChanges": false + }, + "_b8l0qkf2evpjbt1n": { + "id": "_b8l0qkf2evpjbt1n", + "libraryId": "cartel-fdusd", + "name": "Cartel FDUSD", + "script": "_vbuy=(BINANCE:btcfdusd.vbuy)\n_vsell=(BINANCE:btcfdusd.vsell)\nline(cum(_vbuy-_vsell), title='FDUSD')", + "createdAt": 1720292159337, + "updatedAt": 1726323368471, + "options": { + "priceScaleId": "cartel", + "scaleMargins": { + "top": 0.05, + "bottom": 0.72 + }, + "color": "rgb(255,8,0)", + "visible": true, + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "cartel-fdusd" + ], + "displayName": "Cartel FDUSD" + } + }, + "indicatorOrder": [ + "_yc5ewfpz6pjfe1ru", + "cvd-exchange-BINANCE", + "cvd-exchange-BINANCE_FUTURES", + "cvd-exchange-BYBIT", + "cvd-exchange-COINBASE", + "cvd-exchange-DERIBIT", + "_vrjve84cmmifpf2y", + "_tt3vmq14qq0q6zh3", + "_a0uddpx1jizg6f4h", + "_zial46x5pdsip5ms", + "_b8l0qkf2evpjbt1n" + ], + "priceScales": { + "right": { + "scaleMargins": { + "top": 0.03, + "bottom": 0.73 + }, + "indicators": [ + "Price" + ], + "priceFormat": { + "precision": 1, + "minMove": 0.1 + } + }, + "cvd": { + "scaleMargins": { + "top": 0.8, + "bottom": 0.02 + }, + "indicators": [ + "CVD" + ] + }, + "volume_liquidations": { + "scaleMargins": { + "top": 0.75, + "bottom": 0.17 + } + }, + "volume": { + "scaleMargins": { + "top": 0.84, + "bottom": 0 + } + }, + "cvdspot": { + "scaleMargins": { + "top": 0.1, + "bottom": 0.2 + } + }, + "_n6t8sd7vxozc5h4r": { + "scaleMargins": { + "top": 0.29, + "bottom": 0.54 + }, + "indicators": [ + "CVD Spot Big" + ] + }, + "big-order-cvd": { + "scaleMargins": { + "top": 0.29, + "bottom": 0.54 + }, + "indicators": [ + "CVD Spot Big" + ] + }, + "_gyovgnu9maq1uwle": { + "scaleMargins": { + "top": 0.29, + "bottom": 0.54 + }, + "indicators": [ + "CVD Spot Mid" + ] + }, + "_ioxhx4vgropry53p": { + "scaleMargins": { + "top": 0.29, + "bottom": 0.54 + }, + "indicators": [ + "CVD Spot Small" + ] + }, + "small-order-cvd": { + "scaleMargins": { + "top": 0.29, + "bottom": 0.54 + }, + "indicators": [ + "CVD Spot Small" + ] + }, + "_7fc9z73qoetc9k0y": { + "scaleMargins": { + "top": 0.51, + "bottom": 0.32 + }, + "indicators": [ + "CVD Perp Big" + ] + }, + "_5x0wmryc4x516pxm": { + "scaleMargins": { + "top": 0.51, + "bottom": 0.32 + }, + "indicators": [ + "CVD Perp Mid" + ] + }, + "_zjxpbo8j4zw1foku": { + "scaleMargins": { + "top": 0.51, + "bottom": 0.32 + }, + "indicators": [ + "CVD Perp Small" + ] + }, + "cvd-exchange": { + "scaleMargins": { + "top": 0.79, + "bottom": 0.03 + }, + "indicators": [ + "CVD Exchange" + ] + }, + "cvd-exchange-BINANCE": { + "scaleMargins": { + "top": 0.04, + "bottom": 0.74 + }, + "indicators": [ + "cvd-exchange-BINANCE" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "cvd-exchange-BINANCE_FUTURES": { + "scaleMargins": { + "top": 0.05, + "bottom": 0.74 + }, + "indicators": [ + "cvd-exchange-BINANCE_FUTURES" + ] + }, + "cvd-exchange-BYBIT": { + "scaleMargins": { + "top": 0.29, + "bottom": 0.49 + }, + "indicators": [ + "cvd-exchange-BYBIT" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "cvd-exchange-COINBASE": { + "scaleMargins": { + "top": 0.54, + "bottom": 0.24 + }, + "indicators": [ + "cvd-exchange-COINBASE" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "cvd-exchange-DERIBIT": { + "scaleMargins": { + "top": 0.29, + "bottom": 0.49 + }, + "indicators": [ + "cvd-exchange-DERIBIT" + ] + }, + "_z516f0zwmtibbr76": { + "scaleMargins": { + "top": 0.54, + "bottom": 0.24 + }, + "indicators": [ + "BF.S CVD2" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "_pshii8bunephrk55": { + "scaleMargins": { + "top": 0.79, + "bottom": 0.01 + }, + "indicators": [ + "OK.S CVD" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "_s99eqgk6zaglfjn7": { + "scaleMargins": { + "top": 0.79, + "bottom": 0.01 + }, + "indicators": [ + "OK.P CVD" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "_8d9u97zke39v6fz9": { + "scaleMargins": { + "top": 0.53, + "bottom": 0.24 + }, + "indicators": [ + "KRAKEN.S CVD" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "cartel": { + "scaleMargins": { + "top": 0.05, + "bottom": 0.72 + }, + "indicators": [ + "Cartel FDUSD" + ] + } + }, + "layouting": false, + "showIndicators": true, + "timeframe": "900", + "refreshRate": 1000, + "showAlerts": true, + "showAlertsLabel": true, + "showLegend": true, + "fillGapsWithEmpty": true, + "showHorizontalGridlines": false, + "horizontalGridlinesColor": "rgba(255,255,255,.1)", + "showVerticalGridlines": false, + "verticalGridlinesColor": "rgba(255,255,255,.1)", + "showWatermark": false, + "watermarkColor": "rgba(255,255,255,.1)", + "showBorder": true, + "borderColor": null, + "textColor": null, + "showLeftScale": false, + "showRightScale": true, + "showTimeScale": true, + "hiddenMarkets": {}, + "barSpacing": 6.218359926594028, + "_id": "chart copy 1" + }, + "delta": { + "indicatorsErrors": {}, + "indicators": { + "_jrw7wrmtet6jdmth": { + "id": "_jrw7wrmtet6jdmth", + "name": "Delta Binance Spot", + "options": { + "priceScaleId": "delta-binance-spot", + "upColor": "rgb(0,150,136)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 7, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "scaleMargins": { + "top": 0, + "bottom": 0.9 + } + }, + "script": "_vbuy = (\nBINANCE:btcfdusd.vbuy+\nBINANCE:btcusdc.vbuy+\nBINANCE:btcusdt.vbuy\n)\n\n_vsell = (\nBINANCE:btcfdusd.vsell+\nBINANCE:btcusdc.vsell+\nBINANCE:btcusdt.vsell\n)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\" BN(S)\")", + "createdAt": 1649330197719, + "updatedAt": 1719122536983, + "displayName": "Delta Binance Spot", + "description": null, + "author": null, + "preview": {}, + "libraryId": "delta-binance-spot3", + "optionsDefinitions": {}, + "series": [ + "delta-binance-spot3" + ], + "unsavedChanges": false + }, + "_ky9zklmwo7aud15n": { + "id": "_ky9zklmwo7aud15n", + "libraryId": "delta-okx-spot2", + "name": "Delta OKX Spot", + "script": "_vbuy = (\nOKEX:BTC-USDC.vbuy+\nOKEX:BTC-USDT.vbuy\n)\n\n_vsell = (\nOKEX:BTC-USDC.vsell+\nOKEX:BTC-USDT.vsell\n)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\" OKX(S)\")", + "createdAt": 1649330197719, + "updatedAt": 1719122547279, + "options": { + "priceScaleId": "_lcgvarzto6kaxhsi", + "upColor": "rgb(0,150,136)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 7, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "scaleMargins": { + "top": 0.1, + "bottom": 0.81 + } + }, + "optionsDefinitions": {}, + "series": [ + "delta-okx-spot2" + ], + "displayName": "Delta OKX Spot", + "unsavedChanges": false + }, + "_2w5s6578gp6z2of4": { + "id": "_2w5s6578gp6z2of4", + "libraryId": "delta-bybit-spot", + "name": "Delta ByBit Spot", + "script": "_vbuy = (\nBYBIT:BTCUSDC-SPOT.vbuy+\nBYBIT:BTCUSDT-SPOT.vbuy\n)\n\n_vsell = (\nBYBIT:BTCUSDC-SPOT.vsell+\nBYBIT:BTCUSDT-SPOT.vsell\n)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\" ByB(S)\")", + "createdAt": 1649330197719, + "updatedAt": 1719122558264, + "options": { + "priceScaleId": "_b4xic3uzz06rtv26", + "upColor": "rgb(0,150,136)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 7, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "scaleMargins": { + "top": 0.39, + "bottom": 0.52 + } + }, + "optionsDefinitions": {}, + "series": [ + "delta-bybit-spot" + ], + "displayName": "Delta ByBit Spot", + "unsavedChanges": false + }, + "_hcp8eo38prw5wqug": { + "id": "_hcp8eo38prw5wqug", + "libraryId": "delta-kraken-spot", + "name": "Delta KRAKEN Spot", + "script": "_vbuy = (\nKRAKEN:XBT/USD.vbuy+\nKRAKEN:XBT/USDC.vbuy+\nKRAKEN:XBT/USDT.vbuy\n)\n\n_vsell = (\nKRAKEN:XBT/USD.vsell+\nKRAKEN:XBT/USDC.vsell+\nKRAKEN:XBT/USDT.vsell\n)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\" KRAKEN(S)\")", + "createdAt": 1649330197719, + "updatedAt": 1719122573176, + "options": { + "priceScaleId": "_cn9x80j0ttweyqz6", + "upColor": "rgb(0,150,136)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 7, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "scaleMargins": { + "top": 0.19, + "bottom": 0.72 + } + }, + "optionsDefinitions": {}, + "series": [ + "delta-kraken-spot" + ], + "displayName": "Delta KRAKEN Spot", + "unsavedChanges": false + }, + "_r0rtgrchlud62lwx": { + "id": "_r0rtgrchlud62lwx", + "libraryId": "delta-coinbase-spot", + "name": "Delta CoinBASE Spot", + "script": "_vbuy = (\nCOINBASE:BTC-USD.vbuy+\nCOINBASE:BTC-USDT.vbuy\n)\n\n_vsell = (\nCOINBASE:BTC-USD.vsell+\nCOINBASE:BTC-USDT.vsell\n)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\" CB(S)\")", + "createdAt": 1649330197719, + "updatedAt": 1719122611545, + "options": { + "priceScaleId": "_zf4aekw47m3y53zx", + "upColor": "rgb(0,150,136)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 7, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "scaleMargins": { + "top": 0.27, + "bottom": 0.62 + } + }, + "optionsDefinitions": {}, + "series": [ + "delta-coinbase-spot" + ], + "displayName": "Delta CoinBASE Spot", + "unsavedChanges": false + }, + "_ag3apce9u97doq07": { + "id": "_ag3apce9u97doq07", + "libraryId": "delta-kraken-perp", + "name": "Delta KRAKEN Perp", + "script": "_vbuy = (\nKRAKEN:PI_XBTUSD.vbuy\n)\n\n_vsell = (\nKRAKEN:PI_XBTUSD.vsell\n)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\" KRAKEN(P)\")", + "createdAt": 1649330197719, + "updatedAt": 1719122718132, + "options": { + "priceScaleId": "_9zvs80vj4jeitgp5", + "upColor": "rgb(0,150,136)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 7, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "scaleMargins": { + "top": 0.78, + "bottom": 0.11 + } + }, + "optionsDefinitions": {}, + "series": [ + "delta-kraken-perp" + ], + "displayName": "Delta KRAKEN Perp", + "unsavedChanges": false + }, + "_sa3ysdyv7xplfmhc": { + "id": "_sa3ysdyv7xplfmhc", + "libraryId": "delta-bybit-perp", + "name": "Delta Bybit Perp", + "script": "_vbuy = (\nBYBIT:BTCUSD.vbuy+\nBYBIT:BTCUSDT.vbuy\n)\n\n_vsell = (\nBYBIT:BTCUSD.vsell+\nBYBIT:BTCUSDT.vsell\n)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\" ByB(P)\")", + "createdAt": 1649330197719, + "updatedAt": 1719122689369, + "options": { + "priceScaleId": "_r5cjw16ua6kobv5i", + "upColor": "rgb(0,150,136)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 7, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "scaleMargins": { + "top": 0.69, + "bottom": 0.25 + } + }, + "optionsDefinitions": {}, + "series": [ + "delta-bybit-perp" + ], + "displayName": "Delta Bybit Perp", + "unsavedChanges": false + }, + "_nttpc1117uhqv96p": { + "id": "_nttpc1117uhqv96p", + "libraryId": "delta-okx-perp", + "name": "Delta OKX Perp", + "script": "_vbuy = (\nOKEX:BTC-USD-SWAP.vbuy+\nOKEX:BTC-USDC-SWAP.vbuy+\nOKEX:BTC-USDT-SWAP.vbuy\n)\n\n_vsell = (\nOKEX:BTC-USD-SWAP.vsell+\nOKEX:BTC-USDC-SWAP.vsell+\nOKEX:BTC-USDT-SWAP.vsell\n)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\" OKX(P)\")", + "createdAt": 1649330197719, + "updatedAt": 1719122680162, + "options": { + "priceScaleId": "_y0s5gdub1mtsl5pv", + "upColor": "rgb(0,150,136)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 7, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "scaleMargins": { + "top": 0.58, + "bottom": 0.33 + } + }, + "optionsDefinitions": {}, + "series": [ + "delta-okx-perp" + ], + "displayName": "Delta OKX Perp", + "unsavedChanges": false + }, + "_hpzd9xihlf52lr85": { + "id": "_hpzd9xihlf52lr85", + "libraryId": "delta-binance-perp", + "name": "Delta Binance Perp", + "script": "_vbuy = (\nBINANCE_FUTURES:btcusd_perp.vbuy+\nBINANCE_FUTURES:btcusdt.vbuy\n)\n\n_vsell = (\nBINANCE_FUTURES:btcusd_perp.vsell+\nBINANCE_FUTURES:btcusdt.vsell\n)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\" BN(P)\")", + "createdAt": 1649330197719, + "updatedAt": 1719122668492, + "options": { + "priceScaleId": "_gvqnq2ca8tfq568n", + "upColor": "rgb(0,150,136)", + "downColor": "rgb(233,30,99)", + "visible": true, + "length": 7, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "lastValueVisible": true, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + }, + "scaleMargins": { + "top": 0.49, + "bottom": 0.43 + } + }, + "optionsDefinitions": {}, + "series": [ + "delta-binance-perp" + ], + "displayName": "Delta Binance Perp", + "unsavedChanges": false + }, + "_pdkc3oypu1l59j72": { + "id": "_pdkc3oypu1l59j72", + "libraryId": "bf2", + "name": "BF2", + "script": "_vbuy = (BITFINEX:BTCUSD.vbuy+BITFINEX:BTCUST.vbuy)\n_vsell = (BITFINEX:BTCUSD.vsell+BITFINEX:BTCUST.vsell)\n\nvolume = _vbuy+_vsell\na = sma(Math.pow(volume,2),options.length)\nb = Math.pow(sum(volume,options.length),2)/Math.pow(options.length,2)\nstdev = Math.sqrt(a - b)\nbasis = sma(volume, options.length)\ndev = 1 * stdev\ntreshold = basis + dev\n\ndelta = _vbuy - _vsell\n\nplothistogram({ time: time, value: (delta), color: delta > 0 ? ( volume > treshold ? options.upColorHighVol : options.upColorLowVol) : ( volume > treshold ? options.downColorHighVol : options.downColorLowVol)}, title=\"BF.S\")", + "createdAt": 1715187672296, + "updatedAt": 1720080344378, + "options": { + "priceScaleId": "_vcgrf1t9xv14mihs", + "scaleMargins": { + "top": 0.89, + "bottom": 0.02 + }, + "length": 14, + "upColorHighVol": "rgb(41,98,255)", + "upColorLowVol": "rgb(0,121,107)", + "downColorHighVol": "rgb(240,98,146)", + "downColorLowVol": "rgb(194,24,91)", + "visible": true, + "lastValueVisible": true, + "baseLineVisible": false, + "priceLineVisible": false, + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + }, + "optionsDefinitions": {}, + "series": [ + "bf2" + ], + "displayName": "BF2", + "unsavedChanges": false + } + }, + "indicatorOrder": [ + "_jrw7wrmtet6jdmth", + "_ky9zklmwo7aud15n", + "_2w5s6578gp6z2of4", + "_hcp8eo38prw5wqug", + "_r0rtgrchlud62lwx", + "_hpzd9xihlf52lr85", + "_nttpc1117uhqv96p", + "_sa3ysdyv7xplfmhc", + "_ag3apce9u97doq07", + "_pdkc3oypu1l59j72" + ], + "priceScales": { + "right": { + "scaleMargins": { + "top": 0.25, + "bottom": 0.19 + }, + "indicators": [ + "Price 🐶 GIGI" + ], + "mode": 0 + }, + "volume_liquidations": { + "scaleMargins": { + "top": 0.13, + "bottom": 0.8 + }, + "indicators": [ + "Liquidations" + ] + }, + "volume": { + "scaleMargins": { + "top": 0.93, + "bottom": 0 + }, + "indicators": [ + "Relative Volume" + ] + }, + "delta-binance-spot": { + "scaleMargins": { + "top": 0, + "bottom": 0.9 + }, + "indicators": [ + "Delta Binance Spot" + ], + "mode": 0 + }, + "cvd-coinbase": { + "scaleMargins": { + "top": 0.1, + "bottom": 0.2 + } + }, + "delta-coinbase": { + "scaleMargins": { + "top": 0.11, + "bottom": 0.74 + }, + "indicators": [ + "Delta Coinbase" + ], + "mode": 0 + }, + "delta-bybit-perps": { + "scaleMargins": { + "top": 0.57, + "bottom": 0.3 + }, + "indicators": [ + "Delta Bybit Perps" + ], + "mode": 0 + }, + "delta-bitmex": { + "scaleMargins": { + "top": 0.82, + "bottom": 0.12 + }, + "indicators": [ + "Delta BitMex" + ], + "mode": 0 + }, + "delta-ftx-spot": { + "scaleMargins": { + "top": 0.18, + "bottom": 0.76 + }, + "indicators": [ + "Delta FTX Spot" + ], + "mode": 0 + }, + "delta-ftx-perps": { + "scaleMargins": { + "top": 0.88, + "bottom": 0.06 + }, + "indicators": [ + "Delta FTX Perps" + ], + "mode": 0 + }, + "delta-bitfinex-spot": { + "scaleMargins": { + "top": 0.12, + "bottom": 0.88 + }, + "indicators": [ + "Delta BitFinex Spot" + ], + "mode": 0 + }, + "delta-bitfinex-perps2": { + "scaleMargins": { + "top": 0.89, + "bottom": 0.05 + }, + "indicators": [ + "Delta BitFinex Perps" + ], + "mode": 0 + }, + "delta-binance-perps": { + "scaleMargins": { + "top": 0.7, + "bottom": 0.17 + }, + "indicators": [ + "Delta Binance Perps" + ], + "mode": 0 + }, + "okx-spot": { + "scaleMargins": { + "top": 0.09, + "bottom": 0.84 + }, + "indicators": [ + "OKX Spot" + ] + }, + "price": { + "scaleMargins": { + "top": 0.11, + "bottom": 0.43 + }, + "indicators": [ + "Price", + "Price" + ], + "mode": 0, + "priceFormat": { + "precision": 1, + "minMove": 0.1 + } + }, + "delta-okx-spot2": { + "scaleMargins": { + "top": 0.24, + "bottom": 0.61 + }, + "indicators": [ + "Delta OKX Spot" + ] + }, + "delta-okx-perps2": { + "scaleMargins": { + "top": 0.83, + "bottom": 0.03 + }, + "indicators": [ + "Delta OKX Perps" + ] + }, + "cvd-btc-spot": { + "scaleMargins": { + "top": 0.56, + "bottom": 0.36 + }, + "indicators": [ + "CVD ETH Spot" + ] + }, + "cvd-btc-perp": { + "scaleMargins": { + "top": 0.56, + "bottom": 0.36 + }, + "indicators": [ + "CVD ETH Perp" + ] + }, + "relative-volume copy 1": { + "scaleMargins": { + "top": 0.55, + "bottom": 0.43 + }, + "indicators": [ + "Relative Volume" + ] + }, + "single-bar-delta-divergence": { + "scaleMargins": { + "top": 0, + "bottom": 0.06 + }, + "indicators": [ + "Single Bar Delta Divergence" + ] + }, + "cvd-eth-perp2": { + "scaleMargins": { + "top": 0.43, + "bottom": 0.49 + }, + "indicators": [ + "CVD ETH Perp" + ] + }, + "cvd-eth-spot22": { + "scaleMargins": { + "top": 0.43, + "bottom": 0.49 + }, + "indicators": [ + "CVD ETH Spot" + ] + }, + "cvd-okx-s": { + "scaleMargins": { + "top": 0.1, + "bottom": 0.2 + } + }, + "_lcgvarzto6kaxhsi": { + "scaleMargins": { + "top": 0.1, + "bottom": 0.81 + }, + "indicators": [ + "Delta OKX Spot" + ] + }, + "_b4xic3uzz06rtv26": { + "scaleMargins": { + "top": 0.39, + "bottom": 0.52 + }, + "indicators": [ + "Delta ByBit Spot" + ], + "mode": 0 + }, + "_cn9x80j0ttweyqz6": { + "scaleMargins": { + "top": 0.19, + "bottom": 0.72 + }, + "indicators": [ + "Delta KRAKEN Spot" + ] + }, + "_zf4aekw47m3y53zx": { + "scaleMargins": { + "top": 0.27, + "bottom": 0.62 + }, + "indicators": [ + "Delta CoinBASE Spot" + ] + }, + "_77ryrio4wutjl31c": { + "scaleMargins": { + "top": 0.4, + "bottom": 0.33 + }, + "indicators": [ + "CVD Spot" + ] + }, + "_s2ihajoqymk4gckd": { + "scaleMargins": { + "top": 0.41, + "bottom": 0.32 + }, + "indicators": [ + "CVD Perp" + ] + }, + "_9zvs80vj4jeitgp5": { + "scaleMargins": { + "top": 0.78, + "bottom": 0.11 + }, + "indicators": [ + "Delta KRAKEN Perp" + ] + }, + "_r5cjw16ua6kobv5i": { + "scaleMargins": { + "top": 0.69, + "bottom": 0.25 + }, + "indicators": [ + "Delta Bybit Perp" + ] + }, + "_y0s5gdub1mtsl5pv": { + "scaleMargins": { + "top": 0.58, + "bottom": 0.33 + }, + "indicators": [ + "Delta OKX Perp" + ] + }, + "_gvqnq2ca8tfq568n": { + "scaleMargins": { + "top": 0.49, + "bottom": 0.43 + }, + "indicators": [ + "Delta Binance Perp" + ] + }, + "_6ts9lvh8gqq5lx8q": { + "scaleMargins": { + "top": 0.24, + "bottom": 0.2 + }, + "indicators": [ + "Price 🐶 GIGI" + ], + "priceFormat": { + "precision": 1, + "minMove": 0.1 + } + }, + "cvd-spot": { + "scaleMargins": { + "top": 0.3, + "bottom": 0.45 + } + }, + "_vcgrf1t9xv14mihs": { + "scaleMargins": { + "top": 0.89, + "bottom": 0.02 + }, + "indicators": [ + "BF2" + ], + "priceFormat": { + "type": "volume", + "precision": 2, + "minMove": 0.01, + "auto": true + } + } + }, + "layouting": false, + "showIndicators": true, + "timeframe": "300", + "refreshRate": 200, + "showAlerts": true, + "showAlertsLabel": true, + "showLegend": true, + "fillGapsWithEmpty": true, + "showHorizontalGridlines": false, + "horizontalGridlinesColor": "rgba(255,255,255,.1)", + "showVerticalGridlines": false, + "verticalGridlinesColor": "rgba(255,255,255,.1)", + "showWatermark": false, + "watermarkColor": "rgba(255,255,255,.1)", + "showBorder": true, + "borderColor": null, + "textColor": null, + "showLeftScale": false, + "showRightScale": true, + "showTimeScale": true, + "hiddenMarkets": {}, + "barSpacing": 32.61306606314917, + "_id": "delta", + "forceNormalizePrice": false, + "navigationState": { + "tab": "options", + "optionsQuery": "", + "fontSizePx": 14 + } + }, + "chart": { + "indicatorsErrors": {}, + "indicators": { + "_pciob9dy9vlp6ksw": { + "id": "_pciob9dy9vlp6ksw", + "name": "CVD", + "options": { + "priceScaleId": "cvd", + "priceFormat": { + "type": "volume" + }, + "color": "#3BCA6D", + "scaleMargins": { + "top": 0.84, + "bottom": 0 + } + }, + "script": "line(cum(vbuy - vsell))", + "createdAt": 1749271162306, + "updatedAt": 1749271848162, + "description": "Cumulative Volume Delta", + "enabled": true, + "libraryId": "cvd", + "series": [ + "cvd" + ], + "optionsDefinitions": {} + }, + "_whcn9xf1lz3xx986": { + "enabled": true, + "name": "Liquidations", + "description": "Liquidations by side", + "script": "histogram(lbuy, color=options.upColor)\nhistogram(-lsell, color=options.downColor)", + "options": { + "priceFormat": { + "type": "volume" + }, + "priceScaleId": "volume_liquidations", + "upColor": "rgb(255,76,243)", + "downColor": "rgb(255,183,77)", + "scaleMargins": { + "top": 0.75, + "bottom": 0.17 + } + }, + "id": "_whcn9xf1lz3xx986", + "createdAt": 1749271162306, + "updatedAt": null, + "libraryId": "liquidations", + "series": [ + "liquidations", + "p3zlvktp" + ], + "optionsDefinitions": {} + }, + "_d2w8siglj8bhy2p3": { + "enabled": true, + "name": "Price", + "script": "var ohlc = options.useHeikinAshi ? \n avg_heikinashi(bar) : \n options.useGaps ? \n avg_ohlc_with_gaps(bar) : \n avg_ohlc(bar)\n\nplotcandlestick(ohlc)", + "options": { + "priceScaleId": "right", + "priceFormat": { + "auto": true, + "precision": 1, + "minMove": 0.1 + }, + "priceLineVisible": true, + "lastValueVisible": true, + "borderVisible": true, + "upColor": "rgb(59,202,109)", + "downColor": "rgb(214,40,40)", + "borderUpColor": "rgb(59,202,109)", + "borderDownColor": "rgb(239,67,82)", + "wickUpColor": "rgb(223,211,144)", + "wickDownColor": "rgb(239,67,82)", + "useGaps": false, + "useHeikinAshi": false, + "scaleMargins": { + "top": 0.04, + "bottom": 0.26 + } + }, + "id": "_d2w8siglj8bhy2p3", + "createdAt": 1749271162306, + "updatedAt": null, + "libraryId": "price", + "series": [ + "price" + ], + "optionsDefinitions": {}, + "unsavedChanges": true + }, + "_xmys71x0bt3xmrb1": { + "enabled": true, + "name": "Volume", + "description": "Volume + delta", + "script": "if (upColor === 0) {\n if (options.showDelta) {\n upColor = options.upBgColor\n downColor = options.downBgColor\n } else {\n upColor = options.upColor\n downColor = options.downColor\n }\n}\n\nif (options.showDelta) {\n histogram({ time: time, value: Math.abs(vbuy-vsell), color: vbuy - vsell > 0 ? options.upColor : options.downColor})\n}\n\nhistogram({ time: time, value: vbuy + vsell, color: vbuy > vsell ? upColor : downColor })", + "options": { + "priceFormat": { + "type": "volume" + }, + "upColor": "rgb(59,202,109)", + "downColor": "rgb(235,30,47)", + "priceScaleId": "volume", + "scaleMargins": { + "top": 0.84, + "bottom": 0 + }, + "showDelta": true, + "upBgColor": "rgba(59,202,109,0.5)", + "downBgColor": "rgba(235,30,47,0.5)" + }, + "id": "_xmys71x0bt3xmrb1", + "createdAt": 1749271162306, + "updatedAt": null, + "libraryId": "volume", + "series": [ + "volume", + "x4q2m953" + ], + "optionsDefinitions": {} + } + }, + "indicatorOrder": [ + "_pciob9dy9vlp6ksw", + "_whcn9xf1lz3xx986", + "_d2w8siglj8bhy2p3", + "_xmys71x0bt3xmrb1" + ], + "priceScales": { + "right": { + "scaleMargins": { + "top": 0.04, + "bottom": 0.26 + }, + "priceFormat": { + "precision": 1, + "minMove": 0.1 + } + }, + "cvd": { + "scaleMargins": { + "top": 0.84, + "bottom": 0 + } + }, + "volume_liquidations": { + "scaleMargins": { + "top": 0.75, + "bottom": 0.17 + } + }, + "volume": { + "scaleMargins": { + "top": 0.84, + "bottom": 0 + } + } + }, + "layouting": false, + "showIndicators": true, + "timeframe": 5, + "refreshRate": 1000, + "showAlerts": true, + "showAlertsLabel": true, + "showLegend": true, + "fillGapsWithEmpty": false, + "showHorizontalGridlines": false, + "horizontalGridlinesColor": "rgba(255,255,255,.1)", + "showVerticalGridlines": false, + "verticalGridlinesColor": "rgba(255,255,255,.1)", + "showWatermark": true, + "watermarkColor": "rgba(255,255,255,.033)", + "showBorder": true, + "borderColor": null, + "textColor": null, + "showLeftScale": false, + "showRightScale": true, + "showTimeScale": true, + "hiddenMarkets": {}, + "barSpacing": 4.6914145078209195, + "_id": "chart", + "navigationState": { + "tab": "script", + "optionsQuery": "", + "fontSizePx": 14 + }, + "forceNormalizePrice": false + } + } +} \ No newline at end of file diff --git a/python/valuecell/agents/common/trading/features/image.py b/python/valuecell/agents/common/trading/features/image.py new file mode 100644 index 000000000..00412058e --- /dev/null +++ b/python/valuecell/agents/common/trading/features/image.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from agno.agent import Agent +from loguru import logger + +from valuecell.utils import model as model_utils +from valuecell.utils.ts import get_current_timestamp_ms + +from ..constants import ( + FEATURE_GROUP_BY_IMAGE_ANALYSIS, + FEATURE_GROUP_BY_KEY, +) +from ..models import ( + FeatureVector, + UserRequest, +) +from .interfaces import ( + ImageBasedFeatureComputer, +) + +if TYPE_CHECKING: + from agno.media import Image + + +PROMPTS: str = """ +# Role +You are an expert High-Frequency Trader (HFT) and Order Flow Analyst specializing in crypto market microstructure. You are analyzing a dashboard from Aggr.trade. + +# Visual Context +The image displays three vertical panes: +1. **Top (Price & Global CVD):** 5s candles, Aggregate Volume, Liquidations (bright bars), and Global CVD line. +2. **Middle (Delta Grid):** Net Delta per exchange/pair (5m timeframe). Key: Spot (S) vs. Perps (P). +3. **Bottom (Exchange CVDs):** Cumulative Volume Delta lines for individual exchanges (15m timeframe). + * *Legend Assumption:* Cyan/Blue = Coinbase (Spot); Yellow/Red = Binance (Spot/Perps). + +# Analysis Objectives +Please analyze the order flow dynamics and provide a scalping strategy based on the following: + +1. **Spot vs. Perp Dynamics:** + * Is the price action driven by Spot demand (e.g., Coinbase buying) or Perp speculation? + * Identify any **"Spot Premium"** or **"Perp Discount"** behavior. + +2. **Absorption & Divergences (CRITICAL):** + * Look for **"Passive Absorption"**: Are we seeing aggressive selling (Red Delta/CVD) resulting in stable or rising prices? + * Look for **"CVD Divergences"**: Is Price making Higher Highs while Global/Binance CVD makes Lower Highs? + +3. **Exchange Specific Flows:** + * Compare **Coinbase Spot (Smart Money)** vs. **Binance Perps (Retail/Speculative)**. Are they correlated or fighting each other? + +# Output Format +Provide a concise professional report: +* **Market State:** (e.g., Spot-Led Grind, Short Squeeze, Liquidation Cascade) +* **Key Observation:** (One sentence on the most critical anomaly, e.g., "Coinbase bidding while Binance dumps.") +* **Trade Setup:** + * **Bias:** [LONG / SHORT / NEUTRAL] + * **Entry Trigger:** (e.g., "Enter on retest of VWAP with absorption.") + * **Invalidation:** (Where does the thesis fail?) +""" + + +class MLLMImageFeatureComputer(ImageBasedFeatureComputer): + """Image feature computer using an MLLM (Gemini via agno Agent). + + Consumes dashboard screenshots and extracts structured trading insights, + returning a single `FeatureVector` with textual features. Instrument is + left unset (market-wide analysis). + """ + + def __init__(self, agent: object): + """Initialize with a pre-built `Agent` instance. + + The agent's `.model` and `.model.id` are inspected for provider/model + metadata. + """ + self._agent = agent + self._model = getattr(agent, "model", None) + # default fallback model id when none available + self._model_id = getattr(self._model, "id", "gemini-2.5-flash") + + @classmethod + def from_request(cls, request: UserRequest) -> "MLLMImageFeatureComputer": + """Create an instance from an `LLMModelConfig`. + + Builds a model via `model_utils.create_model_with_provider` and + constructs an `Agent` that will be reused for image analysis. + """ + llm_cfg = request.llm_model_config + created_model = model_utils.create_model_with_provider( + provider=llm_cfg.provider, + model_id=llm_cfg.model_id, + api_key=llm_cfg.api_key, + ) + + # Validate that the model declares image support in config + if not model_utils.supports_model_images(llm_cfg.provider, llm_cfg.model_id): + raise RuntimeError( + f"Model {llm_cfg.model_id} from provider {llm_cfg.provider} does not declare support for images" + ) + + agent = Agent( + model=created_model, + markdown=True, + instructions=[PROMPTS], + ) + + return cls(agent=agent) + + async def compute_features( + self, + images: Optional[List["Image"]] = None, + meta: Optional[Dict[str, Any]] = None, + ) -> List[FeatureVector]: + if not images: + logger.warning("No images provided for image feature computation") + return [] + + logger.info("Running MLLM analysis on provided image") + resp = await self._agent.arun( + "analyze the trading dashboard configuration in the provided image and generate a brief report.", + images=images, + ) + + content: str = getattr(resp, "content", "") or "" + logger.info("MLLM analysis complete: {}", content) + + # Store only the raw markdown report as requested. + values: Dict[str, Any] = {"report_markdown": content} + meta = meta or {} + fv_meta = { + FEATURE_GROUP_BY_KEY: FEATURE_GROUP_BY_IMAGE_ANALYSIS, + **meta, + } + fv = FeatureVector( + ts=get_current_timestamp_ms(), + instrument=None, + values=values, + meta=fv_meta, + ) + return [fv] diff --git a/python/valuecell/agents/common/trading/features/interfaces.py b/python/valuecell/agents/common/trading/features/interfaces.py index 20569fc5b..db8e722da 100644 --- a/python/valuecell/agents/common/trading/features/interfaces.py +++ b/python/valuecell/agents/common/trading/features/interfaces.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, TYPE_CHECKING from valuecell.agents.common.trading.models import ( Candle, @@ -9,6 +9,10 @@ FeatureVector, ) +if TYPE_CHECKING: + # Only for type hints to avoid hard dependency at runtime + from agno.media import Image + # Contracts for feature computation (module-local abstract interfaces). # Plain ABCs (not Pydantic) to keep implementations lightweight. @@ -39,6 +43,31 @@ def compute_features( raise NotImplementedError +class ImageBasedFeatureComputer(ABC): + """Abstract base for image-based feature computers. + + Implementations consume one or more images (screenshots, dashboard panes) + and return domain FeatureVector objects. The concrete implementations may + call external vision/LLM services. + """ + + @abstractmethod + async def compute_features( + self, + images: Optional[List["Image"]] = None, + meta: Optional[Dict[str, Any]] = None, + ) -> List[FeatureVector]: + """Build feature vectors from the provided images. + + Args: + images: list of image objects. Implementations expect `agno.media.Image`. + meta: optional metadata such as instrument or timestamps. + Returns: + A list of `FeatureVector` items. + """ + raise NotImplementedError + + class BaseFeaturesPipeline(ABC): """Abstract pipeline that produces feature vectors (including market features).""" diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 547fd6ebd..186f2feb9 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -9,8 +9,8 @@ from __future__ import annotations import asyncio -import itertools from typing import List, Optional +from pathlib import Path from loguru import logger @@ -29,6 +29,8 @@ CandleBasedFeatureComputer, ) from .market_snapshot import MarketSnapshotFeatureComputer +from ..data.screenshot import PlaywrightScreenshotDataSource +from .image import MLLMImageFeatureComputer class DefaultFeaturesPipeline(BaseFeaturesPipeline): @@ -42,13 +44,16 @@ def __init__( candle_feature_computer: CandleBasedFeatureComputer, market_snapshot_computer: MarketSnapshotFeatureComputer, candle_configurations: Optional[List[CandleConfig]] = None, + screenshot_data_source: Optional[PlaywrightScreenshotDataSource] = None, + image_feature_computer: Optional[MLLMImageFeatureComputer] = None, ) -> None: self._request = request self._market_data_source = market_data_source self._candle_feature_computer = candle_feature_computer self._symbols = list(dict.fromkeys(request.trading_config.symbols)) self._market_snapshot_computer = market_snapshot_computer - self._candle_configurations = candle_configurations + self._screenshot_data_source = screenshot_data_source + self._image_feature_computer = image_feature_computer self._candle_configurations = candle_configurations or [ CandleConfig(interval="1s", lookback=60 * 3), CandleConfig(interval="1m", lookback=60 * 4), @@ -80,24 +85,56 @@ async def _fetch_market_features() -> List[FeatureVector]: logger.info( f"Starting concurrent data fetching for {len(self._candle_configurations)} candle sets and markets snapshot..." ) - tasks = [ - _fetch_candles(config.interval, config.lookback) - for config in self._candle_configurations - ] - tasks.append(_fetch_market_features()) - # results = [ [candle_features_1], [candle_features_2], ..., [market_features] ] - results = await asyncio.gather(*tasks) + # Create named tasks so we don't depend on result ordering. + tasks_map: dict[str, asyncio.Task] = {} + + for idx, config in enumerate(self._candle_configurations): + name = f"candles:{config.interval}:{idx}" + coro = _fetch_candles(config.interval, config.lookback) + tasks_map[name] = asyncio.create_task(coro) + + # market snapshot task + tasks_map["market"] = asyncio.create_task(_fetch_market_features()) + + # Optionally fetch and compute image-based features if providers are set + if ( + self._screenshot_data_source is not None + and self._image_feature_computer is not None + ): + + async def _fetch_image_features() -> List[FeatureVector]: + # Ensure the screenshot data source lifecycle is managed via async context + async with self._screenshot_data_source as ds: + img = await ds.capture() + + # image_feature_computer expects a list of agno.media.Image + return await self._image_feature_computer.compute_features(images=[img]) + + tasks_map["image"] = asyncio.create_task(_fetch_image_features()) + + # Await all tasks and then collect results by name + await asyncio.gather(*tasks_map.values()) logger.info("Concurrent data fetching complete.") - market_features: List[FeatureVector] = results.pop() + results_map: dict[str, List[FeatureVector]] = { + name: task.result() for name, task in tasks_map.items() + } - # Flatten the list of lists of candle features - candle_features: List[FeatureVector] = list( - itertools.chain.from_iterable(results) - ) + # Flatten candle features from all candle tasks + candle_features: List[FeatureVector] = [] + for name, feats in results_map.items(): + if name.startswith("candles:"): + candle_features.extend(feats) + + # Append market features if available + market_features: List[FeatureVector] = results_map.get("market", []) + + # Append image-derived features if available + image_features: List[FeatureVector] = results_map.get("image", []) candle_features.extend(market_features) + candle_features.extend(image_features) return FeaturesPipelineResult(features=candle_features) @@ -109,9 +146,26 @@ def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: ) candle_feature_computer = SimpleCandleFeatureComputer() market_snapshot_computer = MarketSnapshotFeatureComputer() + + try: + image_feature_computer = MLLMImageFeatureComputer.from_request(request) + charts_json = Path(__file__).parent / "configs" / "charts.json" + screenshot_data_source = PlaywrightScreenshotDataSource( + target_url="https://aggr.trade", + file_path=str(charts_json), + ) + except Exception as e: + logger.warning( + f"Image feature computer could not be initialized: {e}. Proceeding without image features." + ) + image_feature_computer = None + screenshot_data_source = None + return cls( request=request, market_data_source=market_data_source, candle_feature_computer=candle_feature_computer, market_snapshot_computer=market_snapshot_computer, + image_feature_computer=image_feature_computer, + screenshot_data_source=screenshot_data_source, ) diff --git a/python/valuecell/agents/common/trading/models.py b/python/valuecell/agents/common/trading/models.py index 983123674..19106e7b2 100644 --- a/python/valuecell/agents/common/trading/models.py +++ b/python/valuecell/agents/common/trading/models.py @@ -384,7 +384,7 @@ class FeatureVector(BaseModel): ..., description="Feature vector timestamp in ms", ) - instrument: InstrumentRef + instrument: Optional[InstrumentRef] values: Dict[CommonKeyType, CommonValueType | List[CommonValueType]] = Field( default_factory=dict, description="Feature name to numeric value" ) diff --git a/python/valuecell/utils/model.py b/python/valuecell/utils/model.py index 4e9e19bd5..cda85688a 100644 --- a/python/valuecell/utils/model.py +++ b/python/valuecell/utils/model.py @@ -327,6 +327,42 @@ def create_model_with_provider( return provider_instance.create_model(model_id, **kwargs) +def supports_model_images(provider: str, model_id: Optional[str]) -> bool: + """Return True if the given provider+model_id supports image inputs. + + This inspects the provider YAML loaded by `ConfigManager` and looks for a + `supported_inputs` entry on the matching model definition. If `model_id` is + None the provider's `default_model` is used. The check is case-insensitive + and tolerant of missing fields. + """ + try: + from valuecell.config.manager import get_config_manager + + cfg_mgr = get_config_manager() + prov = cfg_mgr.get_provider_config(provider) + if not prov: + return False + + models = prov.models or [] + target_id = model_id or prov.default_model + + for m in models: + mid = m.get("id") or m.get("name") or "" + if not mid: + continue + if target_id and target_id != "" and mid != target_id: + # continue searching for exact match + continue + inputs = m.get("supported_inputs") or [] + # normalize to lower-case + if any(str(i).lower() == "images" for i in inputs): + return True + return False + except Exception: + logger.debug("supports_model_images: failed to read provider config, assuming False") + return False + + # ============================================ # Embedding Functions # ============================================ From 89eed109e274c852362d44e8c1dae94a59a33acf Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:24:06 +0800 Subject: [PATCH 02/20] feat(pipeline): introduce open and close methods for managing long-lived resources in features pipeline --- .../common/trading/_internal/coordinator.py | 6 ++-- .../agents/common/trading/base_agent.py | 16 ++++++++++ .../agents/common/trading/data/screenshot.py | 24 ++++++++------ .../common/trading/features/interfaces.py | 21 ++++++++++++- .../common/trading/features/pipeline.py | 31 +++++++++++++------ python/valuecell/utils/model.py | 4 ++- 6 files changed, 78 insertions(+), 24 deletions(-) diff --git a/python/valuecell/agents/common/trading/_internal/coordinator.py b/python/valuecell/agents/common/trading/_internal/coordinator.py index 0932c6493..606e1a809 100644 --- a/python/valuecell/agents/common/trading/_internal/coordinator.py +++ b/python/valuecell/agents/common/trading/_internal/coordinator.py @@ -91,7 +91,7 @@ def __init__( self._request = request self.strategy_id = strategy_id self.portfolio_service = portfolio_service - self._features_pipeline = features_pipeline + self.features_pipeline = features_pipeline self._composer = composer self._execution_gateway = execution_gateway self._history_recorder = history_recorder @@ -144,7 +144,7 @@ async def run_once(self) -> DecisionCycleResult: if self._request.exchange_config.market_type == MarketType.SPOT: portfolio.buying_power = max(0.0, float(portfolio.account_balance)) - pipeline_result = await self._features_pipeline.build() + pipeline_result = await self.features_pipeline.build() features = list(pipeline_result.features or []) market_features = extract_market_snapshot_features(features) digest = self._digest_builder.build(self._history_recorder.get_records()) @@ -612,7 +612,7 @@ async def close_all_positions(self) -> List[TradeHistoryEntry]: market_features: List[FeatureVector] = [] if self._request.exchange_config.trading_mode == TradingMode.VIRTUAL: try: - pipeline_result = await self._features_pipeline.build() + pipeline_result = await self.features_pipeline.build() market_features = extract_market_snapshot_features( pipeline_result.features or [] ) diff --git a/python/valuecell/agents/common/trading/base_agent.py b/python/valuecell/agents/common/trading/base_agent.py index c1776e3b8..c59de2d1d 100644 --- a/python/valuecell/agents/common/trading/base_agent.py +++ b/python/valuecell/agents/common/trading/base_agent.py @@ -244,6 +244,15 @@ async def _run_background_decision( # Call user hook for custom initialization try: + # Initialize long-lived features resources if available + try: + await runtime.coordinator.features_pipeline.open() + except Exception: + logger.exception( + "Error initializing features pipeline resources for strategy {}", + strategy_id, + ) + await self._on_start(runtime, request) except Exception: logger.exception("Error in _on_start hook for strategy {}", strategy_id) @@ -336,6 +345,13 @@ async def _run_background_decision( ) # Finalize: close resources and mark stopped/paused/error + try: + await runtime.coordinator.features_pipeline.close() + except Exception: + logger.exception( + "Error closing features pipeline resources for strategy {}", + strategy_id, + ) await controller.finalize( runtime, reason=stop_reason, reason_detail=stop_reason_detail ) diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index 0abd3524b..6f8c0db64 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -22,14 +22,16 @@ def __init__(self, target_url: str, file_path: str): """ self.target_url = target_url self.file_path = file_path - + self.playwright: Optional[Playwright] = None self.browser: Optional[Browser] = None self.page: Optional[Page] = None # Ensure dummy file exists if not present if not os.path.exists(self.file_path): - logger.warning(f"File {self.file_path} not found. Creating empty JSON file.") + logger.warning( + f"File {self.file_path} not found. Creating empty JSON file." + ) with open(self.file_path, "w") as f: f.write("{}") @@ -42,8 +44,10 @@ async def __aenter__(self): logger.info("Initializing Playwright session...") self.playwright = await async_playwright().start() self.browser = await self.playwright.chromium.launch(headless=True) - - context = await self.browser.new_context(viewport={"width": 1600, "height": 900}) + + context = await self.browser.new_context( + viewport={"width": 1600, "height": 900} + ) self.page = await context.new_page() logger.info(f"Navigating to {self.target_url}") @@ -69,10 +73,10 @@ async def __aenter__(self): logger.info("Uploading file...") async with self.page.expect_file_chooser() as fc_info: await self.page.get_by_text("Upload template file").click() - + file_chooser = await fc_info.value await file_chooser.set_files(self.file_path) - + # Wait slightly for UI render await asyncio.sleep(1) @@ -100,7 +104,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): """ if exc_type: logger.error(f"Exiting session due to exception: {exc_val}") - + await self._cleanup() logger.info("Session closed.") @@ -126,12 +130,12 @@ async def capture(self, *args, **kwargs) -> Image: # Capture screenshot bytes screenshot_bytes = await self.page.screenshot(full_page=True) - + # Create agno Image object - # Assuming Image can be initialized with content/bytes. + # Assuming Image can be initialized with content/bytes. # If agno.media.Image requires a file path, we would save it to disk first. image_obj = Image(content=screenshot_bytes) - + logger.info("Screenshot captured successfully.") return image_obj diff --git a/python/valuecell/agents/common/trading/features/interfaces.py b/python/valuecell/agents/common/trading/features/interfaces.py index db8e722da..12f615e9b 100644 --- a/python/valuecell/agents/common/trading/features/interfaces.py +++ b/python/valuecell/agents/common/trading/features/interfaces.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, List, Optional from valuecell.agents.common.trading.models import ( Candle, @@ -80,3 +80,22 @@ async def build(self) -> FeaturesPipelineResult: into this call. """ raise NotImplementedError + + @abstractmethod + async def open(self) -> None: + """Optional one-time initialization for long-lived resources. + + Implementations may open network/browser sessions or warm caches here. + This method will be called by the runtime when available; callers may + ignore if not needed. + """ + raise NotImplementedError + + @abstractmethod + async def close(self) -> None: + """Optional cleanup for resources allocated in `open()`. + + Called by the runtime during shutdown to release resources (e.g., close + browser, stop background tasks). Implementations should make this idempotent. + """ + raise NotImplementedError diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 186f2feb9..6988bf0fa 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -9,8 +9,8 @@ from __future__ import annotations import asyncio -from typing import List, Optional from pathlib import Path +from typing import List, Optional from loguru import logger @@ -23,14 +23,14 @@ from ..data.interfaces import BaseMarketDataSource from ..data.market import SimpleMarketDataSource +from ..data.screenshot import PlaywrightScreenshotDataSource from .candle import SimpleCandleFeatureComputer +from .image import MLLMImageFeatureComputer from .interfaces import ( BaseFeaturesPipeline, CandleBasedFeatureComputer, ) from .market_snapshot import MarketSnapshotFeatureComputer -from ..data.screenshot import PlaywrightScreenshotDataSource -from .image import MLLMImageFeatureComputer class DefaultFeaturesPipeline(BaseFeaturesPipeline): @@ -53,12 +53,19 @@ def __init__( self._symbols = list(dict.fromkeys(request.trading_config.symbols)) self._market_snapshot_computer = market_snapshot_computer self._screenshot_data_source = screenshot_data_source + self._screenshot_ctx: Optional[PlaywrightScreenshotDataSource] = None self._image_feature_computer = image_feature_computer self._candle_configurations = candle_configurations or [ CandleConfig(interval="1s", lookback=60 * 3), CandleConfig(interval="1m", lookback=60 * 4), ] + async def open(self) -> None: + """Open any long-lived resources needed by the pipeline.""" + + if self._screenshot_data_source is not None: + self._screenshot_ctx = await self._screenshot_data_source.__aenter__() + async def build(self) -> FeaturesPipelineResult: """ Fetch candles and market snapshot, compute feature vectors concurrently, @@ -104,11 +111,7 @@ async def _fetch_market_features() -> List[FeatureVector]: ): async def _fetch_image_features() -> List[FeatureVector]: - # Ensure the screenshot data source lifecycle is managed via async context - async with self._screenshot_data_source as ds: - img = await ds.capture() - - # image_feature_computer expects a list of agno.media.Image + img = await self._screenshot_ctx.capture() return await self._image_feature_computer.compute_features(images=[img]) tasks_map["image"] = asyncio.create_task(_fetch_image_features()) @@ -138,6 +141,14 @@ async def _fetch_image_features() -> List[FeatureVector]: return FeaturesPipelineResult(features=candle_features) + async def close(self) -> None: + """Close any long-lived resources created by the pipeline.""" + if self._screenshot_ctx is not None: + try: + await self._screenshot_ctx.__aexit__(None, None, None) + finally: + self._screenshot_ctx = None + @classmethod def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: """Factory creating the default pipeline from a user request.""" @@ -148,7 +159,9 @@ def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: market_snapshot_computer = MarketSnapshotFeatureComputer() try: - image_feature_computer = MLLMImageFeatureComputer.from_request(request) + image_feature_computer = MLLMImageFeatureComputer.from_request( + request.llm_model_config + ) charts_json = Path(__file__).parent / "configs" / "charts.json" screenshot_data_source = PlaywrightScreenshotDataSource( target_url="https://aggr.trade", diff --git a/python/valuecell/utils/model.py b/python/valuecell/utils/model.py index cda85688a..a24a7c651 100644 --- a/python/valuecell/utils/model.py +++ b/python/valuecell/utils/model.py @@ -359,7 +359,9 @@ def supports_model_images(provider: str, model_id: Optional[str]) -> bool: return True return False except Exception: - logger.debug("supports_model_images: failed to read provider config, assuming False") + logger.debug( + "supports_model_images: failed to read provider config, assuming False" + ) return False From 7d7bad5ef547b278aa954fbb16287920b3ca2f15 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:34:32 +0800 Subject: [PATCH 03/20] feat(pipeline): refactor screenshot context management to use explicit open/close methods --- .../agents/common/trading/data/screenshot.py | 48 ++++++++++++------- .../common/trading/features/pipeline.py | 14 ++---- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index 6f8c0db64..1b20956be 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -40,6 +40,34 @@ async def __aenter__(self): Magic method for 'async with'. Starts the browser, navigates to the URL, and performs the setup automation. """ + # Delegate to explicit open() so callers can avoid repeated __aenter__ overhead + return await self.open() + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """ + Magic method for 'async with'. + Handles cleanup of browser resources. + """ + if exc_type: + logger.error(f"Exiting session due to exception: {exc_val}") + + await self.close() + + async def _cleanup(self): + """ + Internal helper to close browser resources. + """ + if self.browser: + await self.browser.close() + if self.playwright: + await self.playwright.stop() + + async def open(self): + """Explicit initialization to support one-time setup. + + Returns: + self: the initialized data source (same as __aenter__ would). + """ try: logger.info("Initializing Playwright session...") self.playwright = await async_playwright().start() @@ -97,26 +125,14 @@ async def __aenter__(self): await self._cleanup() raise e - async def __aexit__(self, exc_type, exc_val, exc_tb): - """ - Magic method for 'async with'. - Handles cleanup of browser resources. - """ - if exc_type: - logger.error(f"Exiting session due to exception: {exc_val}") + async def close(self): + """Explicit cleanup to support one-time teardown. + Calls internal cleanup helpers and logs session close. + """ await self._cleanup() logger.info("Session closed.") - async def _cleanup(self): - """ - Internal helper to close browser resources. - """ - if self.browser: - await self.browser.close() - if self.playwright: - await self.playwright.stop() - async def capture(self, *args, **kwargs) -> Image: """ Captures the current state of the page. diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 6988bf0fa..83a5ea6f1 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -53,7 +53,6 @@ def __init__( self._symbols = list(dict.fromkeys(request.trading_config.symbols)) self._market_snapshot_computer = market_snapshot_computer self._screenshot_data_source = screenshot_data_source - self._screenshot_ctx: Optional[PlaywrightScreenshotDataSource] = None self._image_feature_computer = image_feature_computer self._candle_configurations = candle_configurations or [ CandleConfig(interval="1s", lookback=60 * 3), @@ -64,7 +63,7 @@ async def open(self) -> None: """Open any long-lived resources needed by the pipeline.""" if self._screenshot_data_source is not None: - self._screenshot_ctx = await self._screenshot_data_source.__aenter__() + await self._screenshot_data_source.open() async def build(self) -> FeaturesPipelineResult: """ @@ -111,7 +110,7 @@ async def _fetch_market_features() -> List[FeatureVector]: ): async def _fetch_image_features() -> List[FeatureVector]: - img = await self._screenshot_ctx.capture() + img = await self._screenshot_data_source.capture() return await self._image_feature_computer.compute_features(images=[img]) tasks_map["image"] = asyncio.create_task(_fetch_image_features()) @@ -143,11 +142,8 @@ async def _fetch_image_features() -> List[FeatureVector]: async def close(self) -> None: """Close any long-lived resources created by the pipeline.""" - if self._screenshot_ctx is not None: - try: - await self._screenshot_ctx.__aexit__(None, None, None) - finally: - self._screenshot_ctx = None + if self._screenshot_data_source is not None: + await self._screenshot_data_source.close() @classmethod def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: @@ -160,7 +156,7 @@ def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: try: image_feature_computer = MLLMImageFeatureComputer.from_request( - request.llm_model_config + request ) charts_json = Path(__file__).parent / "configs" / "charts.json" screenshot_data_source = PlaywrightScreenshotDataSource( From 75e08088144e587498f1acaa953f035d066b7d78 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:44:07 +0800 Subject: [PATCH 04/20] feat(pipeline): implement error handling for image feature fetching in DefaultFeaturesPipeline --- .../agents/common/trading/features/interfaces.py | 4 ++-- .../valuecell/agents/common/trading/features/pipeline.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/python/valuecell/agents/common/trading/features/interfaces.py b/python/valuecell/agents/common/trading/features/interfaces.py index 12f615e9b..a77825af2 100644 --- a/python/valuecell/agents/common/trading/features/interfaces.py +++ b/python/valuecell/agents/common/trading/features/interfaces.py @@ -89,7 +89,7 @@ async def open(self) -> None: This method will be called by the runtime when available; callers may ignore if not needed. """ - raise NotImplementedError + pass @abstractmethod async def close(self) -> None: @@ -98,4 +98,4 @@ async def close(self) -> None: Called by the runtime during shutdown to release resources (e.g., close browser, stop background tasks). Implementations should make this idempotent. """ - raise NotImplementedError + pass diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 83a5ea6f1..075f060f1 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -110,8 +110,12 @@ async def _fetch_market_features() -> List[FeatureVector]: ): async def _fetch_image_features() -> List[FeatureVector]: - img = await self._screenshot_data_source.capture() - return await self._image_feature_computer.compute_features(images=[img]) + try: + img = await self._screenshot_data_source.capture() + return await self._image_feature_computer.compute_features(images=[img]) + except Exception as e: + logger.error(f"Failed to capture screenshot: {e}") + return [] tasks_map["image"] = asyncio.create_task(_fetch_image_features()) From 97032e149f471e99db8da36defb449de2b3360c0 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:50:53 +0800 Subject: [PATCH 05/20] feat(pipeline): enhance screenshot capture to return optional Image object and save to file --- .../agents/common/trading/data/interfaces.py | 4 +-- .../agents/common/trading/data/screenshot.py | 31 +++++++++++++------ .../common/trading/features/pipeline.py | 8 ++--- python/valuecell/utils/path.py | 12 +++++++ 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/interfaces.py b/python/valuecell/agents/common/trading/data/interfaces.py index f97f53233..a0708129a 100644 --- a/python/valuecell/agents/common/trading/data/interfaces.py +++ b/python/valuecell/agents/common/trading/data/interfaces.py @@ -52,8 +52,8 @@ class BaseScreenshotDataSource(ABC): """ @abstractmethod - async def capture(self, *args, **kwargs) -> Image: + async def capture(self, *args, **kwargs) -> Image | None: """ - Captures a screenshot and returns an agno.media.Image object. + Captures a screenshot and optionally returns an agno.media.Image object. """ raise NotImplementedError diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index 1b20956be..72c40c3af 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -3,10 +3,13 @@ from datetime import datetime from typing import Optional +import aiofiles from agno.media import Image from loguru import logger from playwright.async_api import Browser, Page, Playwright, async_playwright +from valuecell.utils.path import get_screenshot_path + from .interfaces import BaseScreenshotDataSource @@ -133,28 +136,38 @@ async def close(self): await self._cleanup() logger.info("Session closed.") - async def capture(self, *args, **kwargs) -> Image: + async def capture(self, *args, **kwargs) -> Image | None: """ Captures the current state of the page. """ if not self.page: - raise RuntimeError("Page is not initialized. Use 'async with' context.") + logger.error("Page is not initialized. Cannot capture screenshot.") + return None try: - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") logger.info(f"Capturing screenshot at {timestamp}...") # Capture screenshot bytes screenshot_bytes = await self.page.screenshot(full_page=True) - # Create agno Image object - # Assuming Image can be initialized with content/bytes. - # If agno.media.Image requires a file path, we would save it to disk first. - image_obj = Image(content=screenshot_bytes) + # Persist bytes to screenshots directory asynchronously using aiofiles + format = "png" + full_path = os.path.join( + get_screenshot_path(), f"screenshot_{timestamp}.{format}" + ) + + async with aiofiles.open(full_path, "wb") as fh: + await fh.write(screenshot_bytes) - logger.info("Screenshot captured successfully.") + # Create agno Image object and attach path for callers who prefer file-based access + image_obj = Image( + content=screenshot_bytes, filepath=full_path, format=format + ) + + logger.info("Screenshot captured and saved to %s", full_path) return image_obj except Exception as e: logger.error(f"Failed to capture screenshot: {e}") - raise e + return None diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 075f060f1..03e3a7578 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -112,7 +112,9 @@ async def _fetch_market_features() -> List[FeatureVector]: async def _fetch_image_features() -> List[FeatureVector]: try: img = await self._screenshot_data_source.capture() - return await self._image_feature_computer.compute_features(images=[img]) + return await self._image_feature_computer.compute_features( + images=[img] + ) except Exception as e: logger.error(f"Failed to capture screenshot: {e}") return [] @@ -159,9 +161,7 @@ def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: market_snapshot_computer = MarketSnapshotFeatureComputer() try: - image_feature_computer = MLLMImageFeatureComputer.from_request( - request - ) + image_feature_computer = MLLMImageFeatureComputer.from_request(request) charts_json = Path(__file__).parent / "configs" / "charts.json" screenshot_data_source = PlaywrightScreenshotDataSource( target_url="https://aggr.trade", diff --git a/python/valuecell/utils/path.py b/python/valuecell/utils/path.py index 26a6cb045..7b90d5f44 100644 --- a/python/valuecell/utils/path.py +++ b/python/valuecell/utils/path.py @@ -88,3 +88,15 @@ def get_knowledge_path() -> str: pass return str(new_path) + + +def get_screenshot_path() -> str: + """ + Returns the path to the screenshots directory located in the system application directory. + + Returns: + str: Absolute path of the screenshots directory + """ + screenshot_path = Path(get_system_env_dir()) / "screenshots" + screenshot_path.mkdir(parents=True, exist_ok=True) + return str(screenshot_path) From d32d4b245ac029b6286de3d8645afe774a4a8e97 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:01:16 +0800 Subject: [PATCH 06/20] feat(pipeline): update charts.json --- .../agents/common/trading/features/configs/charts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/valuecell/agents/common/trading/features/configs/charts.json b/python/valuecell/agents/common/trading/features/configs/charts.json index 997032df6..ba06c4cee 100644 --- a/python/valuecell/agents/common/trading/features/configs/charts.json +++ b/python/valuecell/agents/common/trading/features/configs/charts.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 7, "createdAt": 1764666200000, "updatedAt": 1764666205739, "name": "Charts", From 816f65613bfa8700fb624f3e0cb502ce128e167c Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:01:33 +0800 Subject: [PATCH 07/20] feat(pipeline): modify screenshot capture to create Image object without filepath --- python/valuecell/agents/common/trading/data/screenshot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index 72c40c3af..733020268 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -162,10 +162,10 @@ async def capture(self, *args, **kwargs) -> Image | None: # Create agno Image object and attach path for callers who prefer file-based access image_obj = Image( - content=screenshot_bytes, filepath=full_path, format=format + content=screenshot_bytes, format=format ) - logger.info("Screenshot captured and saved to %s", full_path) + logger.info("Screenshot captured and saved to {}", full_path) return image_obj except Exception as e: From 00e381dceb17668c1ae2ab689cfa0f832e024a44 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:10:05 +0800 Subject: [PATCH 08/20] feat(pipeline): optimize page navigation and wait times in screenshot capture --- python/valuecell/agents/common/trading/data/screenshot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index 733020268..771ceca90 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -82,7 +82,7 @@ async def open(self): self.page = await context.new_page() logger.info(f"Navigating to {self.target_url}") - await self.page.goto(self.target_url, wait_until="networkidle") + await self.page.goto(self.target_url) logger.info("Waiting for core UI elements...") # Wait for the green menu button to ensure page load @@ -118,7 +118,7 @@ async def open(self): await import_btn.click() logger.info("Import successful. Waiting for modal to close...") - await asyncio.sleep(3) + await asyncio.sleep(1) return self From c996f3473773725d607d4ab9194cb2d92e8076ad Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:20:18 +0800 Subject: [PATCH 09/20] feat(pipeline): streamline Image object creation in screenshot capture --- python/valuecell/agents/common/trading/data/screenshot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index 771ceca90..22a245494 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -161,9 +161,7 @@ async def capture(self, *args, **kwargs) -> Image | None: await fh.write(screenshot_bytes) # Create agno Image object and attach path for callers who prefer file-based access - image_obj = Image( - content=screenshot_bytes, format=format - ) + image_obj = Image(content=screenshot_bytes, format=format) logger.info("Screenshot captured and saved to {}", full_path) return image_obj From 4a2c551c9010a9288e9ee55d1c7fda975a0d7041 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Wed, 3 Dec 2025 16:56:39 +0800 Subject: [PATCH 10/20] feat(pipeline): remove default 1s candle configuration from pipeline --- python/valuecell/agents/common/trading/features/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 03e3a7578..8690d88e5 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -55,7 +55,6 @@ def __init__( self._screenshot_data_source = screenshot_data_source self._image_feature_computer = image_feature_computer self._candle_configurations = candle_configurations or [ - CandleConfig(interval="1s", lookback=60 * 3), CandleConfig(interval="1m", lookback=60 * 4), ] From 61dea8a744f2c8370c34942d175b10db2ce18c4f Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:59:51 +0800 Subject: [PATCH 11/20] feat(pipeline): introduce DataSourceImage model and update screenshot capture methods --- .../agents/common/trading/data/interfaces.py | 14 ++++++--- .../agents/common/trading/data/screenshot.py | 16 ++++++---- .../agents/common/trading/features/image.py | 31 ++++++++++++++----- .../common/trading/features/interfaces.py | 6 ++-- .../valuecell/agents/common/trading/models.py | 8 +++++ 5 files changed, 54 insertions(+), 21 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/interfaces.py b/python/valuecell/agents/common/trading/data/interfaces.py index a0708129a..fd918f741 100644 --- a/python/valuecell/agents/common/trading/data/interfaces.py +++ b/python/valuecell/agents/common/trading/data/interfaces.py @@ -3,9 +3,11 @@ from abc import ABC, abstractmethod from typing import List -from agno.media import Image - -from valuecell.agents.common.trading.models import Candle, MarketSnapShotType +from valuecell.agents.common.trading.models import ( + Candle, + DataSourceImage, + MarketSnapShotType, +) # Contracts for market data sources (module-local abstract interfaces). # These are plain ABCs (not Pydantic models) so implementations can be @@ -52,8 +54,10 @@ class BaseScreenshotDataSource(ABC): """ @abstractmethod - async def capture(self, *args, **kwargs) -> Image | None: + async def capture(self, *args, **kwargs) -> DataSourceImage | None: """ - Captures a screenshot and optionally returns an agno.media.Image object. + Captures a screenshot and returns a `DataSourceImage` describing the + captured bytes and/or saved file path. Implementations should standardize + on `DataSourceImage.content` (bytes) and/or `DataSourceImage.filepath`. """ raise NotImplementedError diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index 22a245494..dbfefa38f 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -4,10 +4,10 @@ from typing import Optional import aiofiles -from agno.media import Image from loguru import logger from playwright.async_api import Browser, Page, Playwright, async_playwright +from valuecell.agents.common.trading.models import DataSourceImage from valuecell.utils.path import get_screenshot_path from .interfaces import BaseScreenshotDataSource @@ -136,7 +136,7 @@ async def close(self): await self._cleanup() logger.info("Session closed.") - async def capture(self, *args, **kwargs) -> Image | None: + async def capture(self, *args, **kwargs) -> DataSourceImage | None: """ Captures the current state of the page. """ @@ -160,11 +160,15 @@ async def capture(self, *args, **kwargs) -> Image | None: async with aiofiles.open(full_path, "wb") as fh: await fh.write(screenshot_bytes) - # Create agno Image object and attach path for callers who prefer file-based access - image_obj = Image(content=screenshot_bytes, format=format) + # Build DataSourceImage with both bytes and file path + ds_image = DataSourceImage( + url=None, + filepath=full_path, + content=screenshot_bytes, + ) - logger.info("Screenshot captured and saved to {}", full_path) - return image_obj + logger.info(f"Screenshot captured and saved to {full_path}") + return ds_image except Exception as e: logger.error(f"Failed to capture screenshot: {e}") diff --git a/python/valuecell/agents/common/trading/features/image.py b/python/valuecell/agents/common/trading/features/image.py index 00412058e..7d28b228c 100644 --- a/python/valuecell/agents/common/trading/features/image.py +++ b/python/valuecell/agents/common/trading/features/image.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional from agno.agent import Agent +from agno.media import Image as AgnoImage from loguru import logger from valuecell.utils import model as model_utils @@ -21,7 +22,7 @@ ) if TYPE_CHECKING: - from agno.media import Image + from valuecell.agents.common.trading.models import DataSourceImage PROMPTS: str = """ @@ -68,16 +69,14 @@ class MLLMImageFeatureComputer(ImageBasedFeatureComputer): left unset (market-wide analysis). """ - def __init__(self, agent: object): + def __init__(self, agent: Agent): """Initialize with a pre-built `Agent` instance. The agent's `.model` and `.model.id` are inspected for provider/model metadata. """ self._agent = agent - self._model = getattr(agent, "model", None) - # default fallback model id when none available - self._model_id = getattr(self._model, "id", "gemini-2.5-flash") + self._model = agent.model @classmethod def from_request(cls, request: UserRequest) -> "MLLMImageFeatureComputer": @@ -109,7 +108,7 @@ def from_request(cls, request: UserRequest) -> "MLLMImageFeatureComputer": async def compute_features( self, - images: Optional[List["Image"]] = None, + images: Optional[List["DataSourceImage"]] = None, meta: Optional[Dict[str, Any]] = None, ) -> List[FeatureVector]: if not images: @@ -117,9 +116,27 @@ async def compute_features( return [] logger.info("Running MLLM analysis on provided image") + # Convert DataSourceImage -> agno.media.Image for the agent + agno_images: List[AgnoImage] = [] + for ds in images: + try: + if content := ds.content: + agno_images.append(AgnoImage(content=content)) + continue + + if filepath := ds.filepath: + agno_images.append(AgnoImage(filepath=filepath)) + continue + + if url := ds.url: + agno_images.append(AgnoImage(url=url)) + + except Exception as e: + logger.warning("Failed to convert DataSourceImage to agno.Image: {}", e) + resp = await self._agent.arun( "analyze the trading dashboard configuration in the provided image and generate a brief report.", - images=images, + images=agno_images, ) content: str = getattr(resp, "content", "") or "" diff --git a/python/valuecell/agents/common/trading/features/interfaces.py b/python/valuecell/agents/common/trading/features/interfaces.py index a77825af2..e8d08ba34 100644 --- a/python/valuecell/agents/common/trading/features/interfaces.py +++ b/python/valuecell/agents/common/trading/features/interfaces.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: # Only for type hints to avoid hard dependency at runtime - from agno.media import Image + from valuecell.agents.common.trading.models import DataSourceImage # Contracts for feature computation (module-local abstract interfaces). # Plain ABCs (not Pydantic) to keep implementations lightweight. @@ -54,13 +54,13 @@ class ImageBasedFeatureComputer(ABC): @abstractmethod async def compute_features( self, - images: Optional[List["Image"]] = None, + images: Optional[List["DataSourceImage"]] = None, meta: Optional[Dict[str, Any]] = None, ) -> List[FeatureVector]: """Build feature vectors from the provided images. Args: - images: list of image objects. Implementations expect `agno.media.Image`. + images: list of `DataSourceImage` objects produced by data sources. meta: optional metadata such as instrument or timestamps. Returns: A list of `FeatureVector` items. diff --git a/python/valuecell/agents/common/trading/models.py b/python/valuecell/agents/common/trading/models.py index 19106e7b2..964a70a19 100644 --- a/python/valuecell/agents/common/trading/models.py +++ b/python/valuecell/agents/common/trading/models.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from enum import Enum +from pathlib import Path from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field, field_validator, model_validator @@ -947,3 +948,10 @@ class DecisionCycleResult: history_records: List[HistoryRecord] digest: TradeDigest portfolio_view: PortfolioView + + +@dataclass +class DataSourceImage: + url: Optional[str] = None # Remote location + filepath: Optional[Path | str] = None # Local file path + content: Optional[bytes] = None # Raw image bytes (standardized to bytes) From c3cb0e20e0287e352f578c3ff558d929b32a48f1 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:08:36 +0800 Subject: [PATCH 12/20] feat(pipeline): enhance DataSourceImage to include instrument reference and update related components --- python/valuecell/agents/common/trading/data/screenshot.py | 5 ++++- python/valuecell/agents/common/trading/features/image.py | 5 +++-- python/valuecell/agents/common/trading/features/pipeline.py | 2 ++ python/valuecell/agents/common/trading/models.py | 5 +++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index dbfefa38f..c78d86b6d 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -10,6 +10,7 @@ from valuecell.agents.common.trading.models import DataSourceImage from valuecell.utils.path import get_screenshot_path +from ..models import InstrumentRef from .interfaces import BaseScreenshotDataSource @@ -19,12 +20,13 @@ class PlaywrightScreenshotDataSource(BaseScreenshotDataSource): Implements Async Context Manager protocol for automatic setup and teardown. """ - def __init__(self, target_url: str, file_path: str): + def __init__(self, target_url: str, file_path: str, instrument: Optional[InstrumentRef] = None): """ Initializes configuration. """ self.target_url = target_url self.file_path = file_path + self.instrument = instrument self.playwright: Optional[Playwright] = None self.browser: Optional[Browser] = None @@ -165,6 +167,7 @@ async def capture(self, *args, **kwargs) -> DataSourceImage | None: url=None, filepath=full_path, content=screenshot_bytes, + instrument=self.instrument, ) logger.info(f"Screenshot captured and saved to {full_path}") diff --git a/python/valuecell/agents/common/trading/features/image.py b/python/valuecell/agents/common/trading/features/image.py index 7d28b228c..000acf070 100644 --- a/python/valuecell/agents/common/trading/features/image.py +++ b/python/valuecell/agents/common/trading/features/image.py @@ -115,7 +115,7 @@ async def compute_features( logger.warning("No images provided for image feature computation") return [] - logger.info("Running MLLM analysis on provided image") + logger.info("Running MLLM analysis on provided images: {}", images) # Convert DataSourceImage -> agno.media.Image for the agent agno_images: List[AgnoImage] = [] for ds in images: @@ -149,9 +149,10 @@ async def compute_features( FEATURE_GROUP_BY_KEY: FEATURE_GROUP_BY_IMAGE_ANALYSIS, **meta, } + instrument = images[0].instrument fv = FeatureVector( ts=get_current_timestamp_ms(), - instrument=None, + instrument=instrument, values=values, meta=fv_meta, ) diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 8690d88e5..288c9ecbd 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -24,6 +24,7 @@ from ..data.interfaces import BaseMarketDataSource from ..data.market import SimpleMarketDataSource from ..data.screenshot import PlaywrightScreenshotDataSource +from ..models import InstrumentRef from .candle import SimpleCandleFeatureComputer from .image import MLLMImageFeatureComputer from .interfaces import ( @@ -165,6 +166,7 @@ def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: screenshot_data_source = PlaywrightScreenshotDataSource( target_url="https://aggr.trade", file_path=str(charts_json), + instrument=InstrumentRef(symbol="BTCUSD"), ) except Exception as e: logger.warning( diff --git a/python/valuecell/agents/common/trading/models.py b/python/valuecell/agents/common/trading/models.py index 964a70a19..bfc7b4ef4 100644 --- a/python/valuecell/agents/common/trading/models.py +++ b/python/valuecell/agents/common/trading/models.py @@ -955,3 +955,8 @@ class DataSourceImage: url: Optional[str] = None # Remote location filepath: Optional[Path | str] = None # Local file path content: Optional[bytes] = None # Raw image bytes (standardized to bytes) + + instrument: Optional[InstrumentRef] = None # Associated instrument, if any + + def __repr__(self): + return f"DataSourceImage(url={self.url}, filepath={self.filepath}, content={'' if self.content else None}, instrument={self.instrument})" From 95c262138b10bf9983cd442b78f744e9e0fe933e Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:19:04 +0800 Subject: [PATCH 13/20] feat(pipeline): update screenshot capture methods to return a list of DataSourceImage instances --- .../agents/common/trading/data/interfaces.py | 4 ++-- .../agents/common/trading/data/screenshot.py | 14 +++++++------- .../agents/common/trading/features/pipeline.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/interfaces.py b/python/valuecell/agents/common/trading/data/interfaces.py index fd918f741..c6710b850 100644 --- a/python/valuecell/agents/common/trading/data/interfaces.py +++ b/python/valuecell/agents/common/trading/data/interfaces.py @@ -54,9 +54,9 @@ class BaseScreenshotDataSource(ABC): """ @abstractmethod - async def capture(self, *args, **kwargs) -> DataSourceImage | None: + async def capture(self, *args, **kwargs) -> List[DataSourceImage]: """ - Captures a screenshot and returns a `DataSourceImage` describing the + Captures a screenshot and returns a list of `DataSourceImage` instances describing the captured bytes and/or saved file path. Implementations should standardize on `DataSourceImage.content` (bytes) and/or `DataSourceImage.filepath`. """ diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index c78d86b6d..cce56d3ea 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -1,7 +1,7 @@ import asyncio import os from datetime import datetime -from typing import Optional +from typing import List, Optional import aiofiles from loguru import logger @@ -138,13 +138,13 @@ async def close(self): await self._cleanup() logger.info("Session closed.") - async def capture(self, *args, **kwargs) -> DataSourceImage | None: + async def capture(self, *args, **kwargs) -> List[DataSourceImage]: """ Captures the current state of the page. """ if not self.page: logger.error("Page is not initialized. Cannot capture screenshot.") - return None + return [] try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") @@ -154,9 +154,9 @@ async def capture(self, *args, **kwargs) -> DataSourceImage | None: screenshot_bytes = await self.page.screenshot(full_page=True) # Persist bytes to screenshots directory asynchronously using aiofiles - format = "png" + fmt = "png" full_path = os.path.join( - get_screenshot_path(), f"screenshot_{timestamp}.{format}" + get_screenshot_path(), f"screenshot_{timestamp}.{fmt}" ) async with aiofiles.open(full_path, "wb") as fh: @@ -171,8 +171,8 @@ async def capture(self, *args, **kwargs) -> DataSourceImage | None: ) logger.info(f"Screenshot captured and saved to {full_path}") - return ds_image + return [ds_image] except Exception as e: logger.error(f"Failed to capture screenshot: {e}") - return None + return [] diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 288c9ecbd..38311b1d1 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -111,9 +111,9 @@ async def _fetch_market_features() -> List[FeatureVector]: async def _fetch_image_features() -> List[FeatureVector]: try: - img = await self._screenshot_data_source.capture() + images = await self._screenshot_data_source.capture() return await self._image_feature_computer.compute_features( - images=[img] + images=images ) except Exception as e: logger.error(f"Failed to capture screenshot: {e}") From ad287207b6391119130ed030806351d7b01fd54c Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:35:34 +0800 Subject: [PATCH 14/20] feat(pipeline): replace PlaywrightScreenshotDataSource with AggrScreenshotDataSource in the features pipeline --- .../agents/common/trading/data/screenshot.py | 114 +++++++++++------- .../common/trading/features/pipeline.py | 4 +- 2 files changed, 71 insertions(+), 47 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index cce56d3ea..8681fc769 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -20,26 +20,21 @@ class PlaywrightScreenshotDataSource(BaseScreenshotDataSource): Implements Async Context Manager protocol for automatic setup and teardown. """ - def __init__(self, target_url: str, file_path: str, instrument: Optional[InstrumentRef] = None): + def __init__( + self, + target_url: str, + instrument: Optional[InstrumentRef] = None, + ): """ Initializes configuration. """ self.target_url = target_url - self.file_path = file_path self.instrument = instrument self.playwright: Optional[Playwright] = None self.browser: Optional[Browser] = None self.page: Optional[Page] = None - # Ensure dummy file exists if not present - if not os.path.exists(self.file_path): - logger.warning( - f"File {self.file_path} not found. Creating empty JSON file." - ) - with open(self.file_path, "w") as f: - f.write("{}") - async def __aenter__(self): """ Magic method for 'async with'. @@ -67,6 +62,9 @@ async def _cleanup(self): if self.playwright: await self.playwright.stop() + async def _setup(self): + pass + async def open(self): """Explicit initialization to support one-time setup. @@ -86,41 +84,7 @@ async def open(self): logger.info(f"Navigating to {self.target_url}") await self.page.goto(self.target_url) - logger.info("Waiting for core UI elements...") - # Wait for the green menu button to ensure page load - menu_btn = self.page.locator("#menu .menu__button") - await menu_btn.wait_for(state="visible", timeout=60000) - - logger.info("Page loaded. Executing setup sequence.") - - # 1. Click Menu - await menu_btn.click() - - # 2. Click Settings - await self.page.get_by_text("Settings", exact=True).click() - - # 3. Click New - await self.page.locator("button").filter(has_text="New").click() - - # 4. Handle File Upload - logger.info("Uploading file...") - async with self.page.expect_file_chooser() as fc_info: - await self.page.get_by_text("Upload template file").click() - - file_chooser = await fc_info.value - await file_chooser.set_files(self.file_path) - - # Wait slightly for UI render - await asyncio.sleep(1) - - # 5. Click IMPORT - logger.info("Confirming import...") - import_btn = self.page.locator("button").filter(has_text="IMPORT") - await import_btn.wait_for(state="visible") - await import_btn.click() - - logger.info("Import successful. Waiting for modal to close...") - await asyncio.sleep(1) + await self._setup() return self @@ -176,3 +140,63 @@ async def capture(self, *args, **kwargs) -> List[DataSourceImage]: except Exception as e: logger.error(f"Failed to capture screenshot: {e}") return [] + + +class AggrScreenshotDataSource(PlaywrightScreenshotDataSource): + """ + Composite data source that aggregates multiple screenshot data sources. + """ + + def __init__( + self, + target_url: str, + file_path: str, + instrument: Optional[InstrumentRef] = None, + ): + super().__init__(target_url, instrument=instrument) + self.file_path = file_path + + # Ensure dummy file exists if not present + if not os.path.exists(self.file_path): + logger.warning( + f"File {self.file_path} not found. Creating empty JSON file." + ) + with open(self.file_path, "w") as f: + f.write("{}") + + async def _setup(self): + logger.info("Waiting for core UI elements...") + # Wait for the green menu button to ensure page load + menu_btn = self.page.locator("#menu .menu__button") + await menu_btn.wait_for(state="visible", timeout=60000) + + logger.info("Page loaded. Executing setup sequence.") + + # 1. Click Menu + await menu_btn.click() + + # 2. Click Settings + await self.page.get_by_text("Settings", exact=True).click() + + # 3. Click New + await self.page.locator("button").filter(has_text="New").click() + + # 4. Handle File Upload + logger.info("Uploading file...") + async with self.page.expect_file_chooser() as fc_info: + await self.page.get_by_text("Upload template file").click() + + file_chooser = await fc_info.value + await file_chooser.set_files(self.file_path) + + # Wait slightly for UI render + await asyncio.sleep(1) + + # 5. Click IMPORT + logger.info("Confirming import...") + import_btn = self.page.locator("button").filter(has_text="IMPORT") + await import_btn.wait_for(state="visible") + await import_btn.click() + + logger.info("Import successful. Waiting for modal to close...") + await asyncio.sleep(2) diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 38311b1d1..c5645e1bb 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -23,7 +23,7 @@ from ..data.interfaces import BaseMarketDataSource from ..data.market import SimpleMarketDataSource -from ..data.screenshot import PlaywrightScreenshotDataSource +from ..data.screenshot import AggrScreenshotDataSource, PlaywrightScreenshotDataSource from ..models import InstrumentRef from .candle import SimpleCandleFeatureComputer from .image import MLLMImageFeatureComputer @@ -163,7 +163,7 @@ def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: try: image_feature_computer = MLLMImageFeatureComputer.from_request(request) charts_json = Path(__file__).parent / "configs" / "charts.json" - screenshot_data_source = PlaywrightScreenshotDataSource( + screenshot_data_source = AggrScreenshotDataSource( target_url="https://aggr.trade", file_path=str(charts_json), instrument=InstrumentRef(symbol="BTCUSD"), From c2a2323fc53b8c6aeb8de12c493acf920e379d20 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:39:06 +0800 Subject: [PATCH 15/20] feat(pipeline): add TODO to include image metadata in MLLMImageFeatureComputer --- python/valuecell/agents/common/trading/features/image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/valuecell/agents/common/trading/features/image.py b/python/valuecell/agents/common/trading/features/image.py index 000acf070..ef7f3f88f 100644 --- a/python/valuecell/agents/common/trading/features/image.py +++ b/python/valuecell/agents/common/trading/features/image.py @@ -145,6 +145,7 @@ async def compute_features( # Store only the raw markdown report as requested. values: Dict[str, Any] = {"report_markdown": content} meta = meta or {} + # TODO: include image metadata such as filepath, url fv_meta = { FEATURE_GROUP_BY_KEY: FEATURE_GROUP_BY_IMAGE_ANALYSIS, **meta, From 2e33c2ff3d155a9b018ca53abc590acde553b09e Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:40:15 +0800 Subject: [PATCH 16/20] feat(pipeline): introduce TradingViewScreenshotDataSource and normalize symbol for TradingView --- .../agents/common/trading/data/screenshot.py | 94 ++++++++++++++++++- .../agents/common/trading/data/utils.py | 45 +++++++++ .../valuecell/agents/common/trading/utils.py | 34 +++++++ 3 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 python/valuecell/agents/common/trading/data/utils.py diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index 8681fc769..d14b3a73c 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -12,6 +12,7 @@ from ..models import InstrumentRef from .interfaces import BaseScreenshotDataSource +from .utils import generate_tradingview_html, normalize_symbol_tradingview class PlaywrightScreenshotDataSource(BaseScreenshotDataSource): @@ -22,13 +23,15 @@ class PlaywrightScreenshotDataSource(BaseScreenshotDataSource): def __init__( self, - target_url: str, + target_url: Optional[str] = None, + page_content: Optional[str] = None, instrument: Optional[InstrumentRef] = None, ): """ Initializes configuration. """ self.target_url = target_url + self.page_content = page_content self.instrument = instrument self.playwright: Optional[Playwright] = None @@ -63,7 +66,15 @@ async def _cleanup(self): await self.playwright.stop() async def _setup(self): - pass + if self.target_url: + logger.info(f"Navigating to {self.target_url}") + await self.page.goto(self.target_url) + return + + if self.page_content: + logger.info("Setting page content from provided HTML.") + await self.page.set_content(self.page_content) + return async def open(self): """Explicit initialization to support one-time setup. @@ -81,9 +92,6 @@ async def open(self): ) self.page = await context.new_page() - logger.info(f"Navigating to {self.target_url}") - await self.page.goto(self.target_url) - await self._setup() return self @@ -165,6 +173,9 @@ def __init__( f.write("{}") async def _setup(self): + logger.info(f"Navigating to {self.target_url}") + await self.page.goto(self.target_url) + logger.info("Waiting for core UI elements...") # Wait for the green menu button to ensure page load menu_btn = self.page.locator("#menu .menu__button") @@ -200,3 +211,76 @@ async def _setup(self): logger.info("Import successful. Waiting for modal to close...") await asyncio.sleep(2) + + +class TradingViewScreenshotDataSource(PlaywrightScreenshotDataSource): + """Screenshot data source that renders a TradingView widget for a given symbol + + It generates an HTML page containing the TradingView advanced-chart widget, + waits for the iframe and a short stabilization period, then captures only + the widget container element to produce a focused screenshot. + """ + + def __init__( + self, + symbol: str, + instrument: Optional[InstrumentRef] = None, + ) -> None: + symbol = normalize_symbol_tradingview(symbol) + page_content = generate_tradingview_html(symbol) + super().__init__( + target_url=None, page_content=page_content, instrument=instrument + ) + + self.symbol = symbol + + async def _setup(self): + # Build full HTML wrapper and set as page content + await self.page.set_content(self.page_content) + logger.info("Loading TradingView widget for %s", self.symbol) + + # Wait for the iframe to attach + await self.page.wait_for_selector("iframe", timeout=15000) + + # Wait for visual stability (widgets often animate/fetch data) + await self.page.wait_for_timeout(3000) + + # Ensure the container is visible + widget_locator = self.page.locator(".tradingview-widget-container") + await widget_locator.wait_for(state="visible", timeout=10000) + + async def capture(self, *args, **kwargs) -> List[DataSourceImage]: + if not self.page: + logger.error("Page is not initialized. Cannot capture screenshot.") + return [] + + try: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") + logger.info( + f"Capturing TradingView widget screenshot for {self.symbol} at {timestamp}..." + ) + + widget_locator = self.page.locator(".tradingview-widget-container") + # Capture only the widget element to reduce noise + png_bytes = await widget_locator.screenshot() + + fmt = "png" + full_path = os.path.join( + get_screenshot_path(), f"tradingview_{self.symbol}_{timestamp}.{fmt}" + ) + async with aiofiles.open(full_path, "wb") as fh: + await fh.write(png_bytes) + + ds_image = DataSourceImage( + url=None, + filepath=full_path, + content=png_bytes, + instrument=self.instrument, + ) + + logger.info(f"TradingView screenshot saved to {full_path}") + return [ds_image] + + except Exception as e: + logger.error(f"Failed to capture TradingView screenshot: {e}") + return [] diff --git a/python/valuecell/agents/common/trading/data/utils.py b/python/valuecell/agents/common/trading/data/utils.py new file mode 100644 index 000000000..f581f6166 --- /dev/null +++ b/python/valuecell/agents/common/trading/data/utils.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel, Field + + +class TradingViewConfig(BaseModel): + symbol: str + allow_symbol_change: bool = False + calendar: bool = False + details: bool = False + hide_side_toolbar: bool = True + hide_top_toolbar: bool = True + hide_legend: bool = False + hide_volume: bool = False + hotlist: bool = False + interval: str = "D" + locale: str = "en" + save_image: bool = False + style: str = "1" + theme: str = "light" + timezone: str = "Etc/UTC" + backgroundColor: str = "#ffffff" + gridColor: str = "rgba(46, 46, 46, 0.06)" + watchlist: list[str] = Field(default_factory=list) + withdateranges: bool = False + compareSymbols: list[str] = Field(default_factory=list) + studies: list[str] = Field(default_factory=list) + autosize: bool = True + + +def generate_tradingview_html(symbol: str) -> str: + widget_config = TradingViewConfig(symbol=symbol) + config_json = widget_config.model_dump_json(indent=2) + html_content = f""" + + +
+
+ + +
+ + +""" + return html_content diff --git a/python/valuecell/agents/common/trading/utils.py b/python/valuecell/agents/common/trading/utils.py index 6742cead2..f45c74d89 100644 --- a/python/valuecell/agents/common/trading/utils.py +++ b/python/valuecell/agents/common/trading/utils.py @@ -182,6 +182,40 @@ def normalize_symbol(symbol: str) -> str: return base_symbol +def normalize_symbol_tradingview(symbol: str) -> str: + """Normalize symbol format for TradingView widgets. + + Rules implemented: + - If the input contains a colon ("A/B:B" style), take the portion + to the left of the first colon, remove separators (`-` and `/`), + convert to upper-case and append the suffix `.P`. + Example: `BTC-USDT:USDT` or `BTC/USDT:PERP` -> `BTCUSDT.P`. + - Otherwise remove separators (`-` and `/`) and convert to upper-case. + Examples: `BTC-USD` -> `BTCUSD`, `ETH/USDT` -> `ETHUSDT`. + + Args: + symbol: Input symbol string (e.g., 'BTC-USD', 'BTC/USDT', 'BTC-USDT:USDT', 'BTC/USDT:PERP'). + + Returns: + Normalized TradingView symbol string in upper-case. The `.P` suffix + is used for inputs that include a colon to indicate the right-hand + side (perpetual/venue) was present and was stripped from the output. + """ + if not symbol: + return symbol + + s = str(symbol).upper() + + # If a colon is present, keep the left side, strip separators, and append .P + if ":" in s: + left, _ = s.split(":", 1) + left_clean = left.replace("-", "").replace("/", "") + return f"{left_clean}.P" + + # Otherwise remove common separators and return uppercase + return s.replace("-", "").replace("/", "") + + def get_exchange_cls(exchange_id: str): """Get CCXT exchange class by exchange ID.""" From 547c92c0a395c1f5789145f8fd27536f05086a75 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:50:04 +0800 Subject: [PATCH 17/20] fix path to charts.json in DefaultFeaturesPipeline to point to the correct directory structure --- .../trading/{features/configs => data/configs/aggr}/charts.json | 0 python/valuecell/agents/common/trading/features/pipeline.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename python/valuecell/agents/common/trading/{features/configs => data/configs/aggr}/charts.json (100%) diff --git a/python/valuecell/agents/common/trading/features/configs/charts.json b/python/valuecell/agents/common/trading/data/configs/aggr/charts.json similarity index 100% rename from python/valuecell/agents/common/trading/features/configs/charts.json rename to python/valuecell/agents/common/trading/data/configs/aggr/charts.json diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index c5645e1bb..ea0f87389 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -162,7 +162,7 @@ def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: try: image_feature_computer = MLLMImageFeatureComputer.from_request(request) - charts_json = Path(__file__).parent / "configs" / "charts.json" + charts_json = Path(__file__).parent.parent / "data" / "configs" / "aggr" / "charts.json" screenshot_data_source = AggrScreenshotDataSource( target_url="https://aggr.trade", file_path=str(charts_json), From 77345aafefb962662603f61cff84f0c5b3c36e2f Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:59:04 +0800 Subject: [PATCH 18/20] feat(pipeline): introduce AGGR_PROMPT for image analysis in MLLMImageFeatureComputer --- .../agents/common/trading/features/image.py | 62 ++++----------- .../common/trading/features/pipeline.py | 3 +- .../agents/common/trading/features/prompts.py | 75 +++++++++++++++++++ 3 files changed, 91 insertions(+), 49 deletions(-) create mode 100644 python/valuecell/agents/common/trading/features/prompts.py diff --git a/python/valuecell/agents/common/trading/features/image.py b/python/valuecell/agents/common/trading/features/image.py index ef7f3f88f..a7114f4f2 100644 --- a/python/valuecell/agents/common/trading/features/image.py +++ b/python/valuecell/agents/common/trading/features/image.py @@ -2,7 +2,8 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional -from agno.agent import Agent +from agno.agent import Agent as AgnoAgent +from agno.models import Model as AgnoModel from agno.media import Image as AgnoImage from loguru import logger @@ -25,42 +26,6 @@ from valuecell.agents.common.trading.models import DataSourceImage -PROMPTS: str = """ -# Role -You are an expert High-Frequency Trader (HFT) and Order Flow Analyst specializing in crypto market microstructure. You are analyzing a dashboard from Aggr.trade. - -# Visual Context -The image displays three vertical panes: -1. **Top (Price & Global CVD):** 5s candles, Aggregate Volume, Liquidations (bright bars), and Global CVD line. -2. **Middle (Delta Grid):** Net Delta per exchange/pair (5m timeframe). Key: Spot (S) vs. Perps (P). -3. **Bottom (Exchange CVDs):** Cumulative Volume Delta lines for individual exchanges (15m timeframe). - * *Legend Assumption:* Cyan/Blue = Coinbase (Spot); Yellow/Red = Binance (Spot/Perps). - -# Analysis Objectives -Please analyze the order flow dynamics and provide a scalping strategy based on the following: - -1. **Spot vs. Perp Dynamics:** - * Is the price action driven by Spot demand (e.g., Coinbase buying) or Perp speculation? - * Identify any **"Spot Premium"** or **"Perp Discount"** behavior. - -2. **Absorption & Divergences (CRITICAL):** - * Look for **"Passive Absorption"**: Are we seeing aggressive selling (Red Delta/CVD) resulting in stable or rising prices? - * Look for **"CVD Divergences"**: Is Price making Higher Highs while Global/Binance CVD makes Lower Highs? - -3. **Exchange Specific Flows:** - * Compare **Coinbase Spot (Smart Money)** vs. **Binance Perps (Retail/Speculative)**. Are they correlated or fighting each other? - -# Output Format -Provide a concise professional report: -* **Market State:** (e.g., Spot-Led Grind, Short Squeeze, Liquidation Cascade) -* **Key Observation:** (One sentence on the most critical anomaly, e.g., "Coinbase bidding while Binance dumps.") -* **Trade Setup:** - * **Bias:** [LONG / SHORT / NEUTRAL] - * **Entry Trigger:** (e.g., "Enter on retest of VWAP with absorption.") - * **Invalidation:** (Where does the thesis fail?) -""" - - class MLLMImageFeatureComputer(ImageBasedFeatureComputer): """Image feature computer using an MLLM (Gemini via agno Agent). @@ -69,17 +34,24 @@ class MLLMImageFeatureComputer(ImageBasedFeatureComputer): left unset (market-wide analysis). """ - def __init__(self, agent: Agent): + def __init__(self, model: AgnoModel, prompt: str) -> None: """Initialize with a pre-built `Agent` instance. The agent's `.model` and `.model.id` are inspected for provider/model metadata. """ - self._agent = agent - self._model = agent.model + self._model = model + self._prompt = prompt + self._agent = AgnoAgent( + model=model, + instructions=[self._prompt], + markdown=True, + ) @classmethod - def from_request(cls, request: UserRequest) -> "MLLMImageFeatureComputer": + def from_request( + cls, request: UserRequest, prompt: str + ) -> "MLLMImageFeatureComputer": """Create an instance from an `LLMModelConfig`. Builds a model via `model_utils.create_model_with_provider` and @@ -98,13 +70,7 @@ def from_request(cls, request: UserRequest) -> "MLLMImageFeatureComputer": f"Model {llm_cfg.model_id} from provider {llm_cfg.provider} does not declare support for images" ) - agent = Agent( - model=created_model, - markdown=True, - instructions=[PROMPTS], - ) - - return cls(agent=agent) + return cls(created_model, prompt) async def compute_features( self, diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index ea0f87389..3187e4289 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -32,6 +32,7 @@ CandleBasedFeatureComputer, ) from .market_snapshot import MarketSnapshotFeatureComputer +from .prompts import AGGR_PROMPT class DefaultFeaturesPipeline(BaseFeaturesPipeline): @@ -161,7 +162,7 @@ def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: market_snapshot_computer = MarketSnapshotFeatureComputer() try: - image_feature_computer = MLLMImageFeatureComputer.from_request(request) + image_feature_computer = MLLMImageFeatureComputer.from_request(request, prompt=AGGR_PROMPT) charts_json = Path(__file__).parent.parent / "data" / "configs" / "aggr" / "charts.json" screenshot_data_source = AggrScreenshotDataSource( target_url="https://aggr.trade", diff --git a/python/valuecell/agents/common/trading/features/prompts.py b/python/valuecell/agents/common/trading/features/prompts.py new file mode 100644 index 000000000..1ac010f49 --- /dev/null +++ b/python/valuecell/agents/common/trading/features/prompts.py @@ -0,0 +1,75 @@ +AGGR_PROMPT: str = """ +# Role +You are an expert High-Frequency Trader (HFT) and Order Flow Analyst specializing in crypto market microstructure. You are analyzing a dashboard from Aggr.trade. + +# Visual Context +The image displays three vertical panes: +1. **Top (Price & Global CVD):** 5s candles, Aggregate Volume, Liquidations (bright bars), and Global CVD line. +2. **Middle (Delta Grid):** Net Delta per exchange/pair (5m timeframe). Key: Spot (S) vs. Perps (P). +3. **Bottom (Exchange CVDs):** Cumulative Volume Delta lines for individual exchanges (15m timeframe). + * *Legend Assumption:* Cyan/Blue = Coinbase (Spot); Yellow/Red = Binance (Spot/Perps). + +# Analysis Objectives +Please analyze the order flow dynamics and provide a scalping strategy based on the following: + +1. **Spot vs. Perp Dynamics:** + * Is the price action driven by Spot demand (e.g., Coinbase buying) or Perp speculation? + * Identify any **"Spot Premium"** or **"Perp Discount"** behavior. + +2. **Absorption & Divergences (CRITICAL):** + * Look for **"Passive Absorption"**: Are we seeing aggressive selling (Red Delta/CVD) resulting in stable or rising prices? + * Look for **"CVD Divergences"**: Is Price making Higher Highs while Global/Binance CVD makes Lower Highs? + +3. **Exchange Specific Flows:** + * Compare **Coinbase Spot (Smart Money)** vs. **Binance Perps (Retail/Speculative)**. Are they correlated or fighting each other? + +# Output Format +Provide a concise professional report: +* **Market State:** (e.g., Spot-Led Grind, Short Squeeze, Liquidation Cascade) +* **Key Observation:** (One sentence on the most critical anomaly, e.g., "Coinbase bidding while Binance dumps.") +* **Trade Setup:** + * **Bias:** [LONG / SHORT / NEUTRAL] + * **Entry Trigger:** (e.g., "Enter on retest of VWAP with absorption.") + * **Invalidation:** (Where does the thesis fail?) +""" + +TRADINGVIEW_WIDGET_PROMPT: str = """ +# Role +You are an expert Technical Analyst and Swing Trader specializing in Price Action and Market Structure. You are analyzing a chart screenshot from a TradingView Widget. + +# Visual Context +The image displays a standard candlestick chart (likely Crypto or Stock asset). +1. **Price Action:** Candlesticks showing Open, High, Low, Close data. +2. **Volume:** Vertical bars at the bottom representing trading activity. +3. **Timeframe:** Identify the timeframe from the top left (e.g., 1D, 4H, 15m). +4. **Asset:** Identify the ticker symbol (e.g., DOGE/USDT, BTC/USD). + +# Analysis Objectives +Please analyze the chart structure and provide a trading plan based on the following pillars: + +1. **Market Structure & Trend Identification:** + * Determine the **Current Trend**: Is the market in an Uptrend (Higher Highs/Higher Lows), Downtrend (Lower Highs/Lower Lows), or Consolidation (Ranging)? + * Identify the **Market Phase**: Accumulation, Markup, Distribution, or Decline? + +2. **Key Levels (Support & Resistance):** + * Identify major **Horizontal Levels**: Where has price historically bounced or rejected? + * Identify **Supply/Demand Zones**: Look for areas of explosive moves that price is revisiting. + * *Optional:* Note any visible trendlines or chart patterns (e.g., Head & Shoulders, Flags, Wedges). + +3. **Volume & Candlestick Analysis (VSA):** + * **Volume Anomalies:** Is there high volume at lows (Stopping Volume) or high volume at highs (Selling Pressure)? + * **Candlestick Signals:** Identify specific patterns on the most recent candles (e.g., Pin Bar, Engulfing, Doji) that suggest reversal or continuation. + * **Momentum:** Are the candles getting larger (increasing momentum) or smaller (loss of momentum)? + +# Output Format +Provide a concise, actionable trading report: + +* **Market Context:** (e.g., "DOGE is consolidating at daily support after a 30% correction.") +* **Technical Signal:** (One sentence on the most important technical factor, e.g., "Bullish Engulfing candle formed on high volume at the 0.14 support level.") +* **Trade Setup:** + * **Bias:** [BULLISH / BEARISH / NEUTRAL] + * **Key Level:** (The price level that matters most right now). + * **Action Plan:** (e.g., "Wait for a daily close above 0.16 to confirm reversal," or "Short on rejection of 0.18.") + * **Invalidation:** (A price level that proves this thesis wrong, e.g., "Close below 0.13".) + +""" \ No newline at end of file From ce79d41313dae8346f89b5ade2346661d2ff4bf4 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:02:55 +0800 Subject: [PATCH 19/20] make format --- .../agents/common/trading/data/screenshot.py | 3 ++- .../agents/common/trading/features/image.py | 2 +- .../agents/common/trading/features/pipeline.py | 12 ++++++++++-- .../agents/common/trading/features/prompts.py | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index d14b3a73c..42ed01df1 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -11,8 +11,9 @@ from valuecell.utils.path import get_screenshot_path from ..models import InstrumentRef +from ..utils import normalize_symbol_tradingview from .interfaces import BaseScreenshotDataSource -from .utils import generate_tradingview_html, normalize_symbol_tradingview +from .utils import generate_tradingview_html class PlaywrightScreenshotDataSource(BaseScreenshotDataSource): diff --git a/python/valuecell/agents/common/trading/features/image.py b/python/valuecell/agents/common/trading/features/image.py index a7114f4f2..ab2aa8656 100644 --- a/python/valuecell/agents/common/trading/features/image.py +++ b/python/valuecell/agents/common/trading/features/image.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional from agno.agent import Agent as AgnoAgent -from agno.models import Model as AgnoModel from agno.media import Image as AgnoImage +from agno.models.base import Model as AgnoModel from loguru import logger from valuecell.utils import model as model_utils diff --git a/python/valuecell/agents/common/trading/features/pipeline.py b/python/valuecell/agents/common/trading/features/pipeline.py index 3187e4289..1dadd5f7f 100644 --- a/python/valuecell/agents/common/trading/features/pipeline.py +++ b/python/valuecell/agents/common/trading/features/pipeline.py @@ -162,8 +162,16 @@ def from_request(cls, request: UserRequest) -> DefaultFeaturesPipeline: market_snapshot_computer = MarketSnapshotFeatureComputer() try: - image_feature_computer = MLLMImageFeatureComputer.from_request(request, prompt=AGGR_PROMPT) - charts_json = Path(__file__).parent.parent / "data" / "configs" / "aggr" / "charts.json" + image_feature_computer = MLLMImageFeatureComputer.from_request( + request, prompt=AGGR_PROMPT + ) + charts_json = ( + Path(__file__).parent.parent + / "data" + / "configs" + / "aggr" + / "charts.json" + ) screenshot_data_source = AggrScreenshotDataSource( target_url="https://aggr.trade", file_path=str(charts_json), diff --git a/python/valuecell/agents/common/trading/features/prompts.py b/python/valuecell/agents/common/trading/features/prompts.py index 1ac010f49..8ae3ad233 100644 --- a/python/valuecell/agents/common/trading/features/prompts.py +++ b/python/valuecell/agents/common/trading/features/prompts.py @@ -72,4 +72,4 @@ * **Action Plan:** (e.g., "Wait for a daily close above 0.16 to confirm reversal," or "Short on rejection of 0.18.") * **Invalidation:** (A price level that proves this thesis wrong, e.g., "Close below 0.13".) -""" \ No newline at end of file +""" From 90ebd419f27cc0b2d9959328fce55d8f88e3c673 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:07:23 +0800 Subject: [PATCH 20/20] fix(pipeline): increase sleep duration after import to ensure modal closure --- python/valuecell/agents/common/trading/data/screenshot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/valuecell/agents/common/trading/data/screenshot.py b/python/valuecell/agents/common/trading/data/screenshot.py index 42ed01df1..24922e14a 100644 --- a/python/valuecell/agents/common/trading/data/screenshot.py +++ b/python/valuecell/agents/common/trading/data/screenshot.py @@ -211,7 +211,7 @@ async def _setup(self): await import_btn.click() logger.info("Import successful. Waiting for modal to close...") - await asyncio.sleep(2) + await asyncio.sleep(3) class TradingViewScreenshotDataSource(PlaywrightScreenshotDataSource):