From f4da40706cd88cdfcaab08c2cc92bb67c03cc593 Mon Sep 17 00:00:00 2001 From: Tim Olson Date: Mon, 2 Mar 2026 18:48:54 -0400 Subject: [PATCH] execute_python can load any data source --- backend/memory/system_prompt.md | 68 +++++++++++++++---- backend/src/agent/tools/chart_tools.py | 91 ++++++++++++++++++++++++-- backend/src/schema/chart_state.py | 16 +++-- web/src/App.vue | 45 ++++++++++++- 4 files changed, 197 insertions(+), 23 deletions(-) diff --git a/backend/memory/system_prompt.md b/backend/memory/system_prompt.md index 3e8abc3..1bffce0 100644 --- a/backend/memory/system_prompt.md +++ b/backend/memory/system_prompt.md @@ -64,21 +64,27 @@ You have access to: ### Chart Context Awareness When a user asks about "this chart", "the chart", "what I'm viewing", or similar references to their current view: 1. **Chart info is automatically available** — The dynamic system prompt includes current chart state (symbol, interval, timeframe) -2. **NEVER** ask the user to upload an image or tell you what symbol they're looking at -3. **Just use `execute_python()`** — It automatically loads the chart data from what they're viewing -4. Inside your Python script, `df` contains the data and `chart_context` has the metadata -5. Use `plot_ohlc(df)` to create beautiful candlestick charts +2. **Check if chart is visible** — If ChartStore fields (symbol, interval) are `None`, the user is on a narrow screen (mobile) and no chart is visible +3. **When chart is visible:** + - **NEVER** ask the user to upload an image or tell you what symbol they're looking at + - **Just use `execute_python()`** — It automatically loads the chart data from what they're viewing + - Inside your Python script, `df` contains the data and `chart_context` has the metadata + - Use `plot_ohlc(df)` to create beautiful candlestick charts +4. **When chart is NOT visible (symbol is None):** + - Let the user know they can view charts on a wider screen + - You can still help with analysis using `get_historical_data()` if they specify a symbol This applies to questions like: "Can you see this chart?", "What are the swing highs and lows?", "Is this in an uptrend?", "What's the current price?", "Analyze this chart", "What am I looking at?" ### Data Analysis Workflow -1. **Chart context is automatic** → Symbol, interval, and timeframe are in the dynamic system prompt -2. **Use `execute_python()`** → This is your PRIMARY analysis tool - - Automatically loads chart data into a pandas DataFrame `df` +1. **Chart context is automatic** → Symbol, interval, and timeframe are in the dynamic system prompt (if chart is visible) +2. **Check ChartStore** → If symbol/interval are `None`, no chart is visible (mobile view) +3. **Use `execute_python()`** → This is your PRIMARY analysis tool + - Automatically loads chart data into a pandas DataFrame `df` (if chart is visible) - Pre-imports numpy (`np`), pandas (`pd`), matplotlib (`plt`), and talib - Provides access to the indicator registry for computing indicators - Use `plot_ohlc(df)` helper for beautiful candlestick charts -3. **Only use `get_chart_data()`** → For simple data inspection without analysis +4. **Only use `get_chart_data()`** → For simple data inspection without analysis ### Python Analysis (`execute_python`) - Your Primary Tool @@ -90,10 +96,13 @@ This applies to questions like: "Can you see this chart?", "What are the swing h - Any computational analysis of price data **Why `execute_python()` is preferred:** -- Chart data (`df`) is automatically loaded from ChartStore (visible time range) +- Chart data (`df`) is automatically loaded from ChartStore (visible time range) when chart is visible +- If no chart is visible (symbol is None), `df` will be None - but you can still load alternative data! - Full pandas/numpy/talib stack pre-imported - Use `plot_ohlc(df)` for instant professional candlestick charts - Access to 150+ indicators via `indicator_registry` +- **Access to DataStores and registry** - order_store, chart_store, datasource_registry +- **Can load ANY symbol/timeframe** using datasource_registry even when df is None - **Results include plots as image URLs** that are automatically displayed to the user - Prints and return values are included in the response @@ -112,14 +121,14 @@ You MUST use `execute_python()` with `plot_ohlc()` or matplotlib whenever the us **Example workflows:** ```python -# Computing an indicator and plotting +# Computing an indicator and plotting (when chart is visible) execute_python(""" df['RSI'] = talib.RSI(df['close'], 14) fig = plot_ohlc(df, title='Price with RSI') df[['close', 'RSI']].tail(10) """) -# Multi-indicator analysis +# Multi-indicator analysis (when chart is visible) execute_python(""" df['SMA20'] = df['close'].rolling(20).mean() df['BB_upper'] = df['close'].rolling(20).mean() + 2 * df['close'].rolling(20).std() @@ -128,6 +137,41 @@ fig = plot_ohlc(df, title=f"{chart_context['symbol']} with Bollinger Bands") print(f"Current price: {df['close'].iloc[-1]:.2f}") print(f"20-period SMA: {df['SMA20'].iloc[-1]:.2f}") """) + +# Loading alternative data (works even when chart not visible or for different symbols) +execute_python(""" +from datetime import datetime, timedelta + +# Get data source +binance = datasource_registry.get_source('binance') + +# Load data for any symbol/timeframe +end_time = datetime.now() +start_time = end_time - timedelta(days=7) + +result = await binance.get_history( + symbol='ETH/USDT', + interval='1h', + start=int(start_time.timestamp()), + end=int(end_time.timestamp()) +) + +# Convert to DataFrame +rows = [{'time': pd.to_datetime(bar.time, unit='s'), **bar.data} for bar in result.bars] +eth_df = pd.DataFrame(rows).set_index('time') + +# Analyze and plot +eth_df['RSI'] = talib.RSI(eth_df['close'], 14) +fig = plot_ohlc(eth_df, title='ETH/USDT 1h - RSI Analysis') +print(f"ETH RSI: {eth_df['RSI'].iloc[-1]:.2f}") +""") + +# Access stores to see current state +execute_python(""" +print(f"Current symbol: {chart_store.chart_state.symbol}") +print(f"Current interval: {chart_store.chart_state.interval}") +print(f"Number of orders: {len(order_store.orders)}") +""") ``` **Only use `get_chart_data()` for:** @@ -143,8 +187,10 @@ print(f"20-period SMA: {df['SMA20'].iloc[-1]:.2f}") | "Is this bullish?" | `execute_python()` | Compute SMAs, trend, and analyze | | "Add Bollinger Bands" | `execute_python()` | Compute bands, use `plot_ohlc(df, title='BB')` | | "Find swing highs" | `execute_python()` | Use pandas logic to detect patterns | +| "Plot ETH even though I'm viewing BTC" | `execute_python()` | Use `datasource_registry.get_source('binance')` to load ETH data | | "What indicators exist?" | `search_indicators()` | Search by category or query | | "What chart am I viewing?" | N/A - automatic | Chart info is in dynamic system prompt | +| "Check my orders" | `execute_python()` | `print(order_store.orders)` | | "Read other stores" | `read_sync_state(store_name)` | For TraderState, StrategyState, etc. | ## Working with Users diff --git a/backend/src/agent/tools/chart_tools.py b/backend/src/agent/tools/chart_tools.py index 311593b..a9db79b 100644 --- a/backend/src/agent/tools/chart_tools.py +++ b/backend/src/agent/tools/chart_tools.py @@ -29,6 +29,22 @@ def _get_indicator_registry(): return _indicator_registry +def _get_order_store(): + """Get the global OrderStore instance.""" + registry = _get_registry() + if registry and "OrderStore" in registry.entries: + return registry.entries["OrderStore"].model + return None + + +def _get_chart_store(): + """Get the global ChartStore instance.""" + registry = _get_registry() + if registry and "ChartStore" in registry.entries: + return registry.entries["ChartStore"].model + return None + + async def _get_chart_data_impl(countback: Optional[int] = None): """Internal implementation for getting chart data. @@ -60,8 +76,13 @@ async def _get_chart_data_impl(countback: Optional[int] = None): start_time = chart_data.get("start_time") end_time = chart_data.get("end_time") - if not symbol: - raise ValueError("No symbol set in ChartStore - user may not have loaded a chart yet") + if not symbol or symbol is None: + raise ValueError( + "No chart visible - ChartStore symbol is None. " + "The user is likely on a narrow screen (mobile) where charts are hidden. " + "Let them know they can view charts on a wider screen, or use get_historical_data() " + "if they specify a symbol and timeframe." + ) # Parse the symbol to extract exchange/source and symbol name # Format is "EXCHANGE:SYMBOL" (e.g., "BINANCE:BTC/USDT", "DEMO:BTC/USD") @@ -142,6 +163,11 @@ async def get_chart_data(countback: Optional[int] = None) -> Dict[str, Any]: This is the preferred way to access chart data when helping the user analyze what they're looking at, since it automatically uses their current chart context. + **IMPORTANT**: This tool will fail if ChartStore.symbol is None (no chart visible). + This happens when the user is on a narrow screen (mobile) where charts are hidden. + In that case, let the user know charts are only visible on wider screens, or use + get_historical_data() if they specify a symbol and timeframe. + Args: countback: Optional limit on number of bars to return. If not specified, returns all bars in the visible time range. @@ -157,7 +183,7 @@ async def get_chart_data(countback: Optional[int] = None) -> Dict[str, Any]: Raises: ValueError: If ChartStore or DataSourceRegistry is not initialized, - or if the symbol format is invalid + if no chart is visible (symbol is None), or if the symbol format is invalid Example: # User is viewing BINANCE:BTC/USDT on 15min chart @@ -191,12 +217,26 @@ async def execute_python(code: str, countback: Optional[int] = None) -> Dict[str - `talib` : TA-Lib technical analysis library - `indicator_registry`: 150+ registered indicators - `plot_ohlc(df)` : Helper function for beautiful candlestick charts + - `registry` : SyncRegistry instance - access to all registered stores + - `datasource_registry`: DataSourceRegistry - access to data sources (binance, etc.) + - `order_store` : OrderStore instance - current orders list + - `chart_store` : ChartStore instance - current chart state - Auto-loaded when user has a chart open: + Auto-loaded when user has a chart visible (ChartStore.symbol is not None): - `df` : pandas DataFrame with DatetimeIndex and columns: open, high, low, close, volume (OHLCV data ready to use) - `chart_context` : dict with symbol, interval, start_time, end_time + When NO chart is visible (narrow screen/mobile): + - `df` : None + - `chart_context` : None + + If `df` is None, you can still load alternative data by: + - Using chart_store to see what symbol/timeframe is configured + - Using datasource_registry.get_source('binance') to access data sources + - Calling datasource.get_history(symbol, interval, start, end) to load any data + - This allows you to make plots of ANY chart even when not connected to chart view + The `plot_ohlc()` Helper: Create professional candlestick charts instantly: - `plot_ohlc(df)` - basic OHLC chart with volume @@ -250,6 +290,41 @@ async def execute_python(code: str, countback: Optional[int] = None) -> Dict[str print("Recent swing highs:") print(swing_highs) \"\"\") + + # Load alternative data when df is None or for different symbol/timeframe + execute_python(\"\"\" + from datetime import datetime, timedelta + + # Get data source + binance = datasource_registry.get_source('binance') + + # Load ETH data even if viewing BTC chart + end_time = datetime.now() + start_time = end_time - timedelta(days=7) + + result = await binance.get_history( + symbol='ETH/USDT', + interval='1h', + start=int(start_time.timestamp()), + end=int(end_time.timestamp()) + ) + + # Convert to DataFrame + rows = [{'time': pd.to_datetime(bar.time, unit='s'), **bar.data} for bar in result.bars] + eth_df = pd.DataFrame(rows).set_index('time') + + # Calculate RSI and plot + eth_df['RSI'] = talib.RSI(eth_df['close'], 14) + fig = plot_ohlc(eth_df, title='ETH/USDT 1h - RSI Analysis') + print(f"ETH RSI: {eth_df['RSI'].iloc[-1]:.2f}") + \"\"\") + + # Access chart store to see current state + execute_python(\"\"\" + print(f"Current symbol: {chart_store.chart_state.symbol}") + print(f"Current interval: {chart_store.chart_state.interval}") + print(f"Orders: {len(order_store.orders)}") + \"\"\") """ import pandas as pd import numpy as np @@ -292,6 +367,10 @@ async def execute_python(code: str, countback: Optional[int] = None) -> Dict[str # --- Get indicator registry --- indicator_registry = _get_indicator_registry() + # --- Get DataStores --- + order_store = _get_order_store() + chart_store = _get_chart_store() + # --- Build globals --- script_globals: Dict[str, Any] = { 'pd': pd, @@ -299,6 +378,10 @@ async def execute_python(code: str, countback: Optional[int] = None) -> Dict[str 'plt': plt, 'talib': talib, 'indicator_registry': indicator_registry, + 'registry': registry, + 'datasource_registry': datasource_registry, + 'order_store': order_store, + 'chart_store': chart_store, 'df': df, 'chart_context': chart_context, 'plot_ohlc': plot_ohlc, diff --git a/backend/src/schema/chart_state.py b/backend/src/schema/chart_state.py index 0f82235..2592ef0 100644 --- a/backend/src/schema/chart_state.py +++ b/backend/src/schema/chart_state.py @@ -4,18 +4,22 @@ from pydantic import BaseModel, Field class ChartState(BaseModel): """Tracks the user's current chart view state. - + This state is synchronized between the frontend and backend to allow the AI agent to understand what the user is currently viewing. + + All fields can be None when no chart is visible (e.g., on mobile/narrow screens). """ - + # Current symbol being viewed (e.g., "BINANCE:BTC/USDT", "BINANCE:ETH/USDT") - symbol: str = Field(default="BINANCE:BTC/USDT", description="Current trading pair symbol") - + # None when chart is not visible + symbol: Optional[str] = Field(default="BINANCE:BTC/USDT", description="Current trading pair symbol, or None if no chart visible") + # Time range currently visible on chart (Unix timestamps in seconds) # These represent the leftmost and rightmost visible candle times start_time: Optional[int] = Field(default=None, description="Start time of visible range (Unix timestamp in seconds)") end_time: Optional[int] = Field(default=None, description="End time of visible range (Unix timestamp in seconds)") - + # Optional: Chart interval/resolution - interval: str = Field(default="15", description="Chart interval (e.g., '1', '5', '15', '60', 'D')") + # None when chart is not visible + interval: Optional[str] = Field(default="15", description="Chart interval (e.g., '1', '5', '15', '60', 'D'), or None if no chart visible") diff --git a/web/src/App.vue b/web/src/App.vue index 81c32d0..2497764 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,5 +1,5 @@