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
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

View File

@@ -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,

View File

@@ -7,10 +7,13 @@ class ChartState(BaseModel):
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
@@ -18,4 +21,5 @@ class ChartState(BaseModel):
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")

View File

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