execute_python can load any data source

This commit is contained in:
2026-03-02 18:48:54 -04:00
parent 3ffce97b3e
commit f4da40706c
4 changed files with 197 additions and 23 deletions

View File

@@ -64,21 +64,27 @@ You have access to:
### Chart Context Awareness ### Chart Context Awareness
When a user asks about "this chart", "the chart", "what I'm viewing", or similar references to their current view: 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) 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 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. **Just use `execute_python()`** — It automatically loads the chart data from what they're viewing 3. **When chart is visible:**
4. Inside your Python script, `df` contains the data and `chart_context` has the metadata - **NEVER** ask the user to upload an image or tell you what symbol they're looking at
5. Use `plot_ohlc(df)` to create beautiful candlestick charts - **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?" 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 ### Data Analysis Workflow
1. **Chart context is automatic** → Symbol, interval, and timeframe are in the dynamic system prompt 1. **Chart context is automatic** → Symbol, interval, and timeframe are in the dynamic system prompt (if chart is visible)
2. **Use `execute_python()`** → This is your PRIMARY analysis tool 2. **Check ChartStore** → If symbol/interval are `None`, no chart is visible (mobile view)
- Automatically loads chart data into a pandas DataFrame `df` 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 - Pre-imports numpy (`np`), pandas (`pd`), matplotlib (`plt`), and talib
- Provides access to the indicator registry for computing indicators - Provides access to the indicator registry for computing indicators
- Use `plot_ohlc(df)` helper for beautiful candlestick charts - 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 ### 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 - Any computational analysis of price data
**Why `execute_python()` is preferred:** **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 - Full pandas/numpy/talib stack pre-imported
- Use `plot_ohlc(df)` for instant professional candlestick charts - Use `plot_ohlc(df)` for instant professional candlestick charts
- Access to 150+ indicators via `indicator_registry` - 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 - **Results include plots as image URLs** that are automatically displayed to the user
- Prints and return values are included in the response - 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:** **Example workflows:**
```python ```python
# Computing an indicator and plotting # Computing an indicator and plotting (when chart is visible)
execute_python(""" execute_python("""
df['RSI'] = talib.RSI(df['close'], 14) df['RSI'] = talib.RSI(df['close'], 14)
fig = plot_ohlc(df, title='Price with RSI') fig = plot_ohlc(df, title='Price with RSI')
df[['close', 'RSI']].tail(10) df[['close', 'RSI']].tail(10)
""") """)
# Multi-indicator analysis # Multi-indicator analysis (when chart is visible)
execute_python(""" execute_python("""
df['SMA20'] = df['close'].rolling(20).mean() df['SMA20'] = df['close'].rolling(20).mean()
df['BB_upper'] = df['close'].rolling(20).mean() + 2 * df['close'].rolling(20).std() 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"Current price: {df['close'].iloc[-1]:.2f}")
print(f"20-period SMA: {df['SMA20'].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:** **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 | | "Is this bullish?" | `execute_python()` | Compute SMAs, trend, and analyze |
| "Add Bollinger Bands" | `execute_python()` | Compute bands, use `plot_ohlc(df, title='BB')` | | "Add Bollinger Bands" | `execute_python()` | Compute bands, use `plot_ohlc(df, title='BB')` |
| "Find swing highs" | `execute_python()` | Use pandas logic to detect patterns | | "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 indicators exist?" | `search_indicators()` | Search by category or query |
| "What chart am I viewing?" | N/A - automatic | Chart info is in dynamic system prompt | | "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. | | "Read other stores" | `read_sync_state(store_name)` | For TraderState, StrategyState, etc. |
## Working with Users ## Working with Users

View File

@@ -29,6 +29,22 @@ def _get_indicator_registry():
return _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): async def _get_chart_data_impl(countback: Optional[int] = None):
"""Internal implementation for getting chart data. """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") start_time = chart_data.get("start_time")
end_time = chart_data.get("end_time") end_time = chart_data.get("end_time")
if not symbol: if not symbol or symbol is None:
raise ValueError("No symbol set in ChartStore - user may not have loaded a chart yet") 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 # Parse the symbol to extract exchange/source and symbol name
# Format is "EXCHANGE:SYMBOL" (e.g., "BINANCE:BTC/USDT", "DEMO:BTC/USD") # 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 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. 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: Args:
countback: Optional limit on number of bars to return. If not specified, countback: Optional limit on number of bars to return. If not specified,
returns all bars in the visible time range. 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: Raises:
ValueError: If ChartStore or DataSourceRegistry is not initialized, 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: Example:
# User is viewing BINANCE:BTC/USDT on 15min chart # 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 - `talib` : TA-Lib technical analysis library
- `indicator_registry`: 150+ registered indicators - `indicator_registry`: 150+ registered indicators
- `plot_ohlc(df)` : Helper function for beautiful candlestick charts - `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: - `df` : pandas DataFrame with DatetimeIndex and columns:
open, high, low, close, volume (OHLCV data ready to use) open, high, low, close, volume (OHLCV data ready to use)
- `chart_context` : dict with symbol, interval, start_time, end_time - `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: The `plot_ohlc()` Helper:
Create professional candlestick charts instantly: Create professional candlestick charts instantly:
- `plot_ohlc(df)` - basic OHLC chart with volume - `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("Recent swing highs:")
print(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 pandas as pd
import numpy as np import numpy as np
@@ -292,6 +367,10 @@ async def execute_python(code: str, countback: Optional[int] = None) -> Dict[str
# --- Get indicator registry --- # --- Get indicator registry ---
indicator_registry = _get_indicator_registry() indicator_registry = _get_indicator_registry()
# --- Get DataStores ---
order_store = _get_order_store()
chart_store = _get_chart_store()
# --- Build globals --- # --- Build globals ---
script_globals: Dict[str, Any] = { script_globals: Dict[str, Any] = {
'pd': pd, 'pd': pd,
@@ -299,6 +378,10 @@ async def execute_python(code: str, countback: Optional[int] = None) -> Dict[str
'plt': plt, 'plt': plt,
'talib': talib, 'talib': talib,
'indicator_registry': indicator_registry, 'indicator_registry': indicator_registry,
'registry': registry,
'datasource_registry': datasource_registry,
'order_store': order_store,
'chart_store': chart_store,
'df': df, 'df': df,
'chart_context': chart_context, 'chart_context': chart_context,
'plot_ohlc': plot_ohlc, 'plot_ohlc': plot_ohlc,

View File

@@ -4,18 +4,22 @@ from pydantic import BaseModel, Field
class ChartState(BaseModel): class ChartState(BaseModel):
"""Tracks the user's current chart view state. """Tracks the user's current chart view state.
This state is synchronized between the frontend and backend to allow This state is synchronized between the frontend and backend to allow
the AI agent to understand what the user is currently viewing. 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") # 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) # Time range currently visible on chart (Unix timestamps in seconds)
# These represent the leftmost and rightmost visible candle times # 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)") 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)") end_time: Optional[int] = Field(default=None, description="End time of visible range (Unix timestamp in seconds)")
# Optional: Chart interval/resolution # 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")

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue' import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
import Splitter from 'primevue/splitter' import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel' import SplitterPanel from 'primevue/splitterpanel'
import ChartView from './components/ChartView.vue' import ChartView from './components/ChartView.vue'
@@ -14,13 +14,36 @@ const isAuthenticated = ref(false)
const needsConfirmation = ref(false) const needsConfirmation = ref(false)
const authError = ref<string>() const authError = ref<string>()
const isDragging = ref(false) const isDragging = ref(false)
const isMobile = ref(false)
let stateSyncCleanup: (() => void) | null = null let stateSyncCleanup: (() => void) | null = null
// Check screen width for mobile layout
const checkMobile = () => {
isMobile.value = window.innerWidth < 768
}
const chartStore = useChartStore()
// Watch mobile state and update ChartStore
watch(isMobile, (mobile) => {
if (mobile) {
// Set all chart state to null when chart is hidden
chartStore.chart_state.symbol = null as any
chartStore.chart_state.start_time = null
chartStore.chart_state.end_time = null
chartStore.chart_state.interval = null as any
}
})
// Check if we need password confirmation on first load // Check if we need password confirmation on first load
onMounted(async () => { onMounted(async () => {
// Check if secrets store is initialized by trying to fetch a status endpoint // Check if secrets store is initialized by trying to fetch a status endpoint
// For now, just default to false (user will see login screen) // For now, just default to false (user will see login screen)
needsConfirmation.value = false needsConfirmation.value = false
// Initialize mobile check
checkMobile()
window.addEventListener('resize', checkMobile)
}) })
const handleAuthenticate = async ( const handleAuthenticate = async (
@@ -75,6 +98,7 @@ onMounted(() => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', checkMobile)
if (stateSyncCleanup) { if (stateSyncCleanup) {
stateSyncCleanup() stateSyncCleanup()
} }
@@ -90,7 +114,7 @@ onBeforeUnmount(() => {
:error-message="authError" :error-message="authError"
@authenticate="handleAuthenticate" @authenticate="handleAuthenticate"
/> />
<Splitter v-else class="main-splitter"> <Splitter v-else-if="!isMobile" class="main-splitter">
<SplitterPanel :size="62" :minSize="40" class="chart-panel"> <SplitterPanel :size="62" :minSize="40" class="chart-panel">
<ChartView /> <ChartView />
</SplitterPanel> </SplitterPanel>
@@ -98,6 +122,9 @@ onBeforeUnmount(() => {
<ChatPanel /> <ChatPanel />
</SplitterPanel> </SplitterPanel>
</Splitter> </Splitter>
<div v-else class="mobile-layout">
<ChatPanel />
</div>
<!-- Transparent overlay to prevent iframe from capturing mouse events during drag --> <!-- Transparent overlay to prevent iframe from capturing mouse events during drag -->
<div v-if="isDragging" class="drag-overlay"></div> <div v-if="isDragging" class="drag-overlay"></div>
</div> </div>
@@ -153,4 +180,18 @@ onBeforeUnmount(() => {
cursor: col-resize; cursor: col-resize;
background: transparent; background: transparent;
} }
.mobile-layout {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
@media (max-width: 767px) {
.main-splitter {
display: none;
}
}
</style> </style>