shape editing
This commit is contained in:
36
backend/memory/chart_context.md
Normal file
36
backend/memory/chart_context.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Chart Context Awareness
|
||||||
|
|
||||||
|
## When Users Reference "The Chart"
|
||||||
|
|
||||||
|
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. **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
|
||||||
|
|
||||||
|
## Common Questions This Applies To
|
||||||
|
|
||||||
|
- "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 (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
|
||||||
|
4. **Only use `get_chart_data()`** → For simple data inspection without analysis
|
||||||
115
backend/memory/python_analysis.md
Normal file
115
backend/memory/python_analysis.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Python Analysis Tool Reference
|
||||||
|
|
||||||
|
## Python Analysis (`execute_python`) - Your Primary Tool
|
||||||
|
|
||||||
|
**ALWAYS use `execute_python()` when the user asks for:**
|
||||||
|
- Technical indicators (RSI, MACD, Bollinger Bands, moving averages, etc.)
|
||||||
|
- Chart visualizations or plots
|
||||||
|
- Statistical calculations or market analysis
|
||||||
|
- Pattern detection or trend analysis
|
||||||
|
- Any computational analysis of price data
|
||||||
|
|
||||||
|
## Why `execute_python()` is preferred:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## CRITICAL: Plots are automatically shown to the user
|
||||||
|
When you create a matplotlib figure (via `plot_ohlc()` or `plt.figure()`), it is automatically:
|
||||||
|
1. Saved as a PNG image
|
||||||
|
2. Returned in the response as a URL (e.g., `/uploads/plot_abc123.png`)
|
||||||
|
3. **Displayed in the user's chat interface** - they see the image immediately
|
||||||
|
|
||||||
|
You MUST use `execute_python()` with `plot_ohlc()` or matplotlib whenever the user wants to see a chart or plot.
|
||||||
|
|
||||||
|
## IMPORTANT: Never use `get_historical_data()` for chart analysis
|
||||||
|
- `get_historical_data()` requires manual timestamp calculation and is only for custom queries
|
||||||
|
- When analyzing what the user is viewing, ALWAYS use `execute_python()` which automatically loads the correct data
|
||||||
|
- The `df` DataFrame in `execute_python()` is pre-loaded with the exact time range the user is viewing
|
||||||
|
|
||||||
|
## Example workflows:
|
||||||
|
|
||||||
|
### Computing an indicator and plotting (when chart is visible)
|
||||||
|
```python
|
||||||
|
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 (when chart is visible)
|
||||||
|
```python
|
||||||
|
execute_python("""
|
||||||
|
df['SMA20'] = df['close'].rolling(20).mean()
|
||||||
|
df['BB_upper'] = df['close'].rolling(20).mean() + 2 * df['close'].rolling(20).std()
|
||||||
|
df['BB_lower'] = df['close'].rolling(20).mean() - 2 * df['close'].rolling(20).std()
|
||||||
|
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)
|
||||||
|
```python
|
||||||
|
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
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
- Quick inspection of raw bar data
|
||||||
|
- When you just need the data structure without analysis
|
||||||
|
|
||||||
|
## Quick Reference: Common Tasks
|
||||||
|
|
||||||
|
| User Request | Tool to Use | Example |
|
||||||
|
|--------------|-------------|---------|
|
||||||
|
| "Show me RSI" | `execute_python()` | `df['RSI'] = talib.RSI(df['close'], 14); plot_ohlc(df)` |
|
||||||
|
| "What's the current price?" | `execute_python()` | `print(f"Current: {df['close'].iloc[-1]}")` |
|
||||||
|
| "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. |
|
||||||
@@ -59,140 +59,6 @@ You have access to:
|
|||||||
- Past strategy discussions and decisions
|
- Past strategy discussions and decisions
|
||||||
- Relevant context retrieved automatically based on current conversation
|
- Relevant context retrieved automatically based on current conversation
|
||||||
|
|
||||||
## Important Behavioral Rules
|
|
||||||
|
|
||||||
### 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. **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 (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
|
|
||||||
4. **Only use `get_chart_data()`** → For simple data inspection without analysis
|
|
||||||
|
|
||||||
### Python Analysis (`execute_python`) - Your Primary Tool
|
|
||||||
|
|
||||||
**ALWAYS use `execute_python()` when the user asks for:**
|
|
||||||
- Technical indicators (RSI, MACD, Bollinger Bands, moving averages, etc.)
|
|
||||||
- Chart visualizations or plots
|
|
||||||
- Statistical calculations or market analysis
|
|
||||||
- Pattern detection or trend analysis
|
|
||||||
- Any computational analysis of price data
|
|
||||||
|
|
||||||
**Why `execute_python()` is preferred:**
|
|
||||||
- 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
|
|
||||||
|
|
||||||
**CRITICAL: Plots are automatically shown to the user**
|
|
||||||
When you create a matplotlib figure (via `plot_ohlc()` or `plt.figure()`), it is automatically:
|
|
||||||
1. Saved as a PNG image
|
|
||||||
2. Returned in the response as a URL (e.g., `/uploads/plot_abc123.png`)
|
|
||||||
3. **Displayed in the user's chat interface** - they see the image immediately
|
|
||||||
|
|
||||||
You MUST use `execute_python()` with `plot_ohlc()` or matplotlib whenever the user wants to see a chart or plot.
|
|
||||||
|
|
||||||
**IMPORTANT: Never use `get_historical_data()` for chart analysis**
|
|
||||||
- `get_historical_data()` requires manual timestamp calculation and is only for custom queries
|
|
||||||
- When analyzing what the user is viewing, ALWAYS use `execute_python()` which automatically loads the correct data
|
|
||||||
- The `df` DataFrame in `execute_python()` is pre-loaded with the exact time range the user is viewing
|
|
||||||
|
|
||||||
**Example workflows:**
|
|
||||||
```python
|
|
||||||
# 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 (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()
|
|
||||||
df['BB_lower'] = df['close'].rolling(20).mean() - 2 * df['close'].rolling(20).std()
|
|
||||||
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:**
|
|
||||||
- Quick inspection of raw bar data
|
|
||||||
- When you just need the data structure without analysis
|
|
||||||
|
|
||||||
### Quick Reference: Common Tasks
|
|
||||||
|
|
||||||
| User Request | Tool to Use | Example |
|
|
||||||
|--------------|-------------|---------|
|
|
||||||
| "Show me RSI" | `execute_python()` | `df['RSI'] = talib.RSI(df['close'], 14); plot_ohlc(df)` |
|
|
||||||
| "What's the current price?" | `execute_python()` | `print(f"Current: {df['close'].iloc[-1]}")` |
|
|
||||||
| "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
|
## Working with Users
|
||||||
|
|
||||||
1. **Understand Intent**: Ask clarifying questions about strategy goals
|
1. **Understand Intent**: Ask clarifying questions about strategy goals
|
||||||
@@ -200,3 +66,7 @@ print(f"Number of orders: {len(order_store.orders)}")
|
|||||||
3. **Validate**: Ensure strategy makes sense before generating code
|
3. **Validate**: Ensure strategy makes sense before generating code
|
||||||
4. **Test**: Encourage backtesting and paper trading first
|
4. **Test**: Encourage backtesting and paper trading first
|
||||||
5. **Monitor**: Help users interpret live strategy behavior
|
5. **Monitor**: Help users interpret live strategy behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: Additional context documents are loaded automatically to provide detailed operational guidelines. See memory files for specifics on chart context, shape drawing, Python analysis, and more.
|
||||||
|
|||||||
612
backend/memory/tradingview_shapes.md
Normal file
612
backend/memory/tradingview_shapes.md
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
# TradingView Shapes and Drawings Reference
|
||||||
|
|
||||||
|
This document describes the various drawing shapes and studies available in TradingView charts, their properties, and control points. This information is useful for the AI agent to understand, create, and manipulate chart drawings.
|
||||||
|
|
||||||
|
## Shape Structure
|
||||||
|
|
||||||
|
All shapes follow a common structure:
|
||||||
|
- **id**: Unique identifier (string) - This is the TradingView-assigned ID after the shape is created
|
||||||
|
- **type**: Shape type identifier (string)
|
||||||
|
- **points**: Array of control points (each with `time` in Unix seconds and `price` as float)
|
||||||
|
- **color**: Color as hex string (e.g., '#FF0000') or color name (e.g., 'red')
|
||||||
|
- **line_width**: Line thickness in pixels (integer)
|
||||||
|
- **line_style**: One of: 'solid', 'dashed', 'dotted'
|
||||||
|
- **properties**: Dictionary of additional shape-specific properties
|
||||||
|
- **symbol**: Trading pair symbol (e.g., 'BINANCE:BTC/USDT')
|
||||||
|
- **created_at**: Creation timestamp (Unix seconds)
|
||||||
|
- **modified_at**: Last modification timestamp (Unix seconds)
|
||||||
|
- **original_id**: Optional string - The ID you requested when creating the shape, before TradingView assigned its own ID
|
||||||
|
|
||||||
|
## Understanding Shape ID Mapping
|
||||||
|
|
||||||
|
When you create a shape using `create_or_update_shape()`, there's an important ID mapping process:
|
||||||
|
|
||||||
|
1. **You specify an ID**: You provide a `shape_id` parameter (e.g., "my-support-line")
|
||||||
|
2. **TradingView assigns its own ID**: When the shape is rendered in TradingView, it gets a new internal ID (e.g., "shape_0x1a2b3c4d")
|
||||||
|
3. **ID remapping occurs**: The shape in the store is updated:
|
||||||
|
- The `id` field becomes TradingView's ID
|
||||||
|
- The `original_id` field preserves your requested ID
|
||||||
|
4. **Tracking your shapes**: To find shapes you created, search by `original_id`
|
||||||
|
|
||||||
|
### Example ID Mapping Flow
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Step 1: Agent creates a shape with a specific ID
|
||||||
|
await create_or_update_shape(
|
||||||
|
shape_id="agent-support-50k",
|
||||||
|
shape_type="horizontal_line",
|
||||||
|
points=[{"time": 1678886400, "price": 50000}],
|
||||||
|
color="#00FF00"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Shape is synced to client and created in TradingView
|
||||||
|
# TradingView assigns ID: "shape_0x1a2b3c4d"
|
||||||
|
|
||||||
|
# Step 3: Shape in store is updated with:
|
||||||
|
# {
|
||||||
|
# "id": "shape_0x1a2b3c4d", # TradingView's ID
|
||||||
|
# "original_id": "agent-support-50k", # Your requested ID
|
||||||
|
# "type": "horizontal_line",
|
||||||
|
# ...
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Step 4: To find your shape later, use shape_ids (searches both id and original_id)
|
||||||
|
my_shapes = search_shapes(
|
||||||
|
shape_ids=['agent-support-50k'],
|
||||||
|
symbol="BINANCE:BTC/USDT"
|
||||||
|
)
|
||||||
|
|
||||||
|
if my_shapes:
|
||||||
|
print(f"Found my support line!")
|
||||||
|
print(f"TradingView ID: {my_shapes[0]['id']}")
|
||||||
|
print(f"My requested ID: {my_shapes[0]['original_id']}")
|
||||||
|
|
||||||
|
# Or use the dedicated original_ids parameter
|
||||||
|
my_shapes = search_shapes(
|
||||||
|
original_ids=['agent-support-50k'],
|
||||||
|
symbol="BINANCE:BTC/USDT"
|
||||||
|
)
|
||||||
|
|
||||||
|
if my_shapes:
|
||||||
|
print(f"Found my support line!")
|
||||||
|
print(f"TradingView ID: {my_shapes[0]['id']}")
|
||||||
|
print(f"My requested ID: {my_shapes[0]['original_id']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why ID Mapping Matters
|
||||||
|
|
||||||
|
- **Shape identification**: You need to know which TradingView shape corresponds to the shape you created
|
||||||
|
- **Updates and deletions**: To modify or delete a shape, you need its TradingView ID (the `id` field)
|
||||||
|
- **Bidirectional sync**: The mapping ensures both the agent and TradingView can reference the same shape
|
||||||
|
|
||||||
|
### Best Practices for Shape IDs
|
||||||
|
|
||||||
|
1. **Use descriptive IDs**: Choose meaningful names like `support-btc-50k` or `trendline-daily-uptrend`
|
||||||
|
2. **Search by original ID**: Use `shape_ids` or `original_ids` parameters in `search_shapes()` to find your shapes
|
||||||
|
- `shape_ids` searches both the actual ID and original_id (more flexible)
|
||||||
|
- `original_ids` searches only the original_id field (more specific)
|
||||||
|
3. **Store important IDs**: If you need to reference a shape multiple times, store its TradingView ID after retrieval
|
||||||
|
4. **Understand the timing**: The ID remapping happens asynchronously after shape creation
|
||||||
|
|
||||||
|
## Common Shape Types
|
||||||
|
|
||||||
|
Use TradingView's native shape type names directly.
|
||||||
|
|
||||||
|
### 1. Trendline
|
||||||
|
**Type**: `trend_line`
|
||||||
|
**Control Points**: 2
|
||||||
|
- Point 1: Start of the line (time, price)
|
||||||
|
- Point 2: End of the line (time, price)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Support/resistance lines
|
||||||
|
- Trend identification
|
||||||
|
- Price channels (when paired)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "trendline-1",
|
||||||
|
"type": "trend_line",
|
||||||
|
"points": [
|
||||||
|
{"time": 1640000000, "price": 45000.0},
|
||||||
|
{"time": 1650000000, "price": 50000.0}
|
||||||
|
],
|
||||||
|
"color": "#2962FF",
|
||||||
|
"line_width": 2,
|
||||||
|
"line_style": "solid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Horizontal Line
|
||||||
|
**Type**: `horizontal_line`
|
||||||
|
**Control Points**: 1
|
||||||
|
- Point 1: Y-level (time can be any value, only price matters)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Support/resistance levels
|
||||||
|
- Price targets
|
||||||
|
- Stop-loss levels
|
||||||
|
- Key psychological levels
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `extend_left`: Boolean, extend line to the left
|
||||||
|
- `extend_right`: Boolean, extend line to the right
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "support-1",
|
||||||
|
"type": "horizontal_line",
|
||||||
|
"points": [{"time": 1640000000, "price": 42000.0}],
|
||||||
|
"color": "#089981",
|
||||||
|
"line_width": 2,
|
||||||
|
"line_style": "dashed",
|
||||||
|
"properties": {
|
||||||
|
"extend_left": true,
|
||||||
|
"extend_right": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Vertical Line
|
||||||
|
**Type**: `vertical_line`
|
||||||
|
**Control Points**: 1
|
||||||
|
- Point 1: X-time (price can be any value, only time matters)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Mark important events
|
||||||
|
- Session boundaries
|
||||||
|
- Earnings releases
|
||||||
|
- Economic data releases
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `extend_top`: Boolean, extend line upward
|
||||||
|
- `extend_bottom`: Boolean, extend line downward
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "event-marker-1",
|
||||||
|
"type": "vertical_line",
|
||||||
|
"points": [{"time": 1640000000, "price": 0}],
|
||||||
|
"color": "#787B86",
|
||||||
|
"line_width": 1,
|
||||||
|
"line_style": "dotted"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Rectangle
|
||||||
|
**Type**: `rectangle`
|
||||||
|
**Control Points**: 2
|
||||||
|
- Point 1: Top-left corner (time, price)
|
||||||
|
- Point 2: Bottom-right corner (time, price)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Consolidation zones
|
||||||
|
- Support/resistance zones
|
||||||
|
- Supply/demand areas
|
||||||
|
- Value areas
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `fill_color`: Fill color with opacity (e.g., '#2962FF33')
|
||||||
|
- `fill`: Boolean, whether to fill the rectangle
|
||||||
|
- `extend_left`: Boolean
|
||||||
|
- `extend_right`: Boolean
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "zone-1",
|
||||||
|
"type": "rectangle",
|
||||||
|
"points": [
|
||||||
|
{"time": 1640000000, "price": 50000.0},
|
||||||
|
{"time": 1650000000, "price": 48000.0}
|
||||||
|
],
|
||||||
|
"color": "#2962FF",
|
||||||
|
"line_width": 1,
|
||||||
|
"line_style": "solid",
|
||||||
|
"properties": {
|
||||||
|
"fill": true,
|
||||||
|
"fill_color": "#2962FF33"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Fibonacci Retracement
|
||||||
|
**Type**: `fib_retracement`
|
||||||
|
**Control Points**: 2
|
||||||
|
- Point 1: Start of the move (swing low or high)
|
||||||
|
- Point 2: End of the move (swing high or low)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Identify potential support/resistance levels
|
||||||
|
- Find retracement targets
|
||||||
|
- Measure pullback depth
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `levels`: Array of Fibonacci levels to display
|
||||||
|
- Default: [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0]
|
||||||
|
- `extend_lines`: Boolean, extend levels beyond the price range
|
||||||
|
- `reverse`: Boolean, reverse the direction
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "fib-1",
|
||||||
|
"type": "fib_retracement",
|
||||||
|
"points": [
|
||||||
|
{"time": 1640000000, "price": 42000.0},
|
||||||
|
{"time": 1650000000, "price": 52000.0}
|
||||||
|
],
|
||||||
|
"color": "#2962FF",
|
||||||
|
"line_width": 1,
|
||||||
|
"properties": {
|
||||||
|
"levels": [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0],
|
||||||
|
"extend_lines": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Fibonacci Extension
|
||||||
|
**Type**: `fib_trend_ext`
|
||||||
|
**Control Points**: 3
|
||||||
|
- Point 1: Start of initial move
|
||||||
|
- Point 2: End of initial move (retracement start)
|
||||||
|
- Point 3: End of retracement
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Project price targets
|
||||||
|
- Extension levels beyond 100%
|
||||||
|
- Measure continuation patterns
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `levels`: Array of extension levels
|
||||||
|
- Common: [0, 0.618, 1.0, 1.618, 2.618, 4.236]
|
||||||
|
|
||||||
|
### 7. Parallel Channel
|
||||||
|
**Type**: `parallel_channel`
|
||||||
|
**Control Points**: 3
|
||||||
|
- Point 1: First point on main trendline
|
||||||
|
- Point 2: Second point on main trendline
|
||||||
|
- Point 3: Point on parallel line (determines channel width)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Price channels
|
||||||
|
- Regression channels
|
||||||
|
- Pitchforks
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `extend_left`: Boolean
|
||||||
|
- `extend_right`: Boolean
|
||||||
|
- `fill`: Boolean, fill the channel
|
||||||
|
- `fill_color`: Fill color with opacity
|
||||||
|
|
||||||
|
### 8. Arrow
|
||||||
|
**Type**: `arrow`
|
||||||
|
**Control Points**: 2
|
||||||
|
- Point 1: Arrow start (time, price)
|
||||||
|
- Point 2: Arrow end (time, price)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Indicate price movement direction
|
||||||
|
- Mark entry/exit points
|
||||||
|
- Show relationships between events
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `arrow_style`: One of: 'simple', 'filled', 'hollow'
|
||||||
|
- `text`: Optional text label
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "entry-arrow",
|
||||||
|
"type": "arrow",
|
||||||
|
"points": [
|
||||||
|
{"time": 1640000000, "price": 44000.0},
|
||||||
|
{"time": 1641000000, "price": 48000.0}
|
||||||
|
],
|
||||||
|
"color": "#089981",
|
||||||
|
"line_width": 2,
|
||||||
|
"properties": {
|
||||||
|
"arrow_style": "filled",
|
||||||
|
"text": "Long Entry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Text/Label
|
||||||
|
**Type**: `text`
|
||||||
|
**Control Points**: 1
|
||||||
|
- Point 1: Text anchor position (time, price)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Annotations
|
||||||
|
- Notes
|
||||||
|
- Labels for patterns
|
||||||
|
- Mark key levels
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `text`: The text content (string)
|
||||||
|
- `font_size`: Font size in points (integer)
|
||||||
|
- `font_family`: Font family name
|
||||||
|
- `bold`: Boolean
|
||||||
|
- `italic`: Boolean
|
||||||
|
- `background`: Boolean, show background
|
||||||
|
- `background_color`: Background color
|
||||||
|
- `text_color`: Text color (can differ from line color)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "note-1",
|
||||||
|
"type": "text",
|
||||||
|
"points": [{"time": 1640000000, "price": 48000.0}],
|
||||||
|
"color": "#131722",
|
||||||
|
"properties": {
|
||||||
|
"text": "Resistance Zone",
|
||||||
|
"font_size": 14,
|
||||||
|
"bold": true,
|
||||||
|
"background": true,
|
||||||
|
"background_color": "#FFE600",
|
||||||
|
"text_color": "#131722"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Single-Point Markers
|
||||||
|
Various single-point marker shapes are available for annotating charts:
|
||||||
|
|
||||||
|
**Types**: `arrow_up` | `arrow_down` | `flag` | `emoji` | `icon` | `sticker` | `note` | `anchored_text` | `anchored_note` | `long_position` | `short_position`
|
||||||
|
|
||||||
|
**Control Points**: 1
|
||||||
|
- Point 1: Marker position (time, price)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Mark entry/exit points
|
||||||
|
- Flag important events
|
||||||
|
- Add visual markers to key levels
|
||||||
|
- Annotate patterns
|
||||||
|
- Track positions
|
||||||
|
|
||||||
|
**Properties** (vary by type):
|
||||||
|
- `text`: Text content for text-based markers
|
||||||
|
- `emoji`: Emoji character for emoji type
|
||||||
|
- `icon`: Icon identifier for icon type
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "long-entry-1",
|
||||||
|
"type": "long_position",
|
||||||
|
"points": [{"time": 1640000000, "price": 44000.0}],
|
||||||
|
"color": "#089981"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "flag-1",
|
||||||
|
"type": "flag",
|
||||||
|
"points": [{"time": 1640000000, "price": 50000.0}],
|
||||||
|
"color": "#F23645",
|
||||||
|
"properties": {
|
||||||
|
"text": "Important Event"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "note-1",
|
||||||
|
"type": "anchored_note",
|
||||||
|
"points": [{"time": 1640000000, "price": 48000.0}],
|
||||||
|
"color": "#FFE600",
|
||||||
|
"properties": {
|
||||||
|
"text": "Watch this level"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11. Circle/Ellipse
|
||||||
|
**Type**: `circle`
|
||||||
|
**Control Points**: 2 or 3
|
||||||
|
- 2 points: Defines bounding box (creates ellipse)
|
||||||
|
- 3 points: Center + radius points
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Highlight areas
|
||||||
|
- Markup patterns
|
||||||
|
- Mark consolidation zones
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `fill`: Boolean
|
||||||
|
- `fill_color`: Fill color with opacity
|
||||||
|
|
||||||
|
### 12. Path (Free Drawing)
|
||||||
|
**Type**: `path`
|
||||||
|
**Control Points**: Variable (3+)
|
||||||
|
- Multiple points defining a path
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Custom patterns
|
||||||
|
- Freeform markup
|
||||||
|
- Complex annotations
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `closed`: Boolean, whether to close the path
|
||||||
|
- `smooth`: Boolean, smooth the path with curves
|
||||||
|
|
||||||
|
### 13. Pitchfork (Andrew's Pitchfork)
|
||||||
|
**Type**: `pitchfork`
|
||||||
|
**Control Points**: 3
|
||||||
|
- Point 1: Pivot/starting point
|
||||||
|
- Point 2: First extreme (high or low)
|
||||||
|
- Point 3: Second extreme (opposite of point 2)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Trend channels
|
||||||
|
- Support/resistance levels
|
||||||
|
- Median line analysis
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `extend_lines`: Boolean
|
||||||
|
- `style`: One of: 'standard', 'schiff', 'modified_schiff'
|
||||||
|
|
||||||
|
### 14. Gann Fan
|
||||||
|
**Type**: `gannbox_fan`
|
||||||
|
**Control Points**: 2
|
||||||
|
- Point 1: Origin point
|
||||||
|
- Point 2: Defines the unit size/scale
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Time and price analysis
|
||||||
|
- Geometric angles (1x1, 1x2, 2x1, etc.)
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `angles`: Array of angles to display
|
||||||
|
- Default: [82.5, 75, 71.25, 63.75, 45, 26.25, 18.75, 15, 7.5]
|
||||||
|
|
||||||
|
### 15. Head and Shoulders
|
||||||
|
**Type**: `head_and_shoulders`
|
||||||
|
**Control Points**: 5
|
||||||
|
- Point 1: Left shoulder low
|
||||||
|
- Point 2: Left shoulder high
|
||||||
|
- Point 3: Head low
|
||||||
|
- Point 4: Right shoulder high
|
||||||
|
- Point 5: Right shoulder low (neckline point)
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Pattern recognition markup
|
||||||
|
- Reversal pattern identification
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
- `target_line`: Boolean, show target line
|
||||||
|
|
||||||
|
## Special Properties
|
||||||
|
|
||||||
|
### Time-Based Properties
|
||||||
|
- All times are Unix timestamps in seconds
|
||||||
|
- Use `Math.floor(Date.now() / 1000)` for current time in JavaScript
|
||||||
|
- Use `int(time.time())` for current time in Python
|
||||||
|
|
||||||
|
### Color Formats
|
||||||
|
- Hex: `#RRGGBB` (e.g., `#2962FF`)
|
||||||
|
- Hex with alpha: `#RRGGBBAA` (e.g., `#2962FF33` for 20% opacity)
|
||||||
|
- Named colors: `red`, `blue`, `green`, etc.
|
||||||
|
- RGB: `rgb(41, 98, 255)`
|
||||||
|
- RGBA: `rgba(41, 98, 255, 0.2)`
|
||||||
|
|
||||||
|
### Line Styles
|
||||||
|
- `solid`: Continuous line
|
||||||
|
- `dashed`: Dashed line (— — —)
|
||||||
|
- `dotted`: Dotted line (· · ·)
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **ID Naming**: Use descriptive IDs that indicate the purpose
|
||||||
|
- Good: `support-btc-42k`, `trendline-uptrend-1`
|
||||||
|
- Bad: `shape1`, `line`
|
||||||
|
|
||||||
|
2. **Color Consistency**: Use consistent colors for similar types
|
||||||
|
- Green (#089981) for bullish/support
|
||||||
|
- Red (#F23645) for bearish/resistance
|
||||||
|
- Blue (#2962FF) for neutral/informational
|
||||||
|
|
||||||
|
3. **Time Alignment**: Ensure times align with actual candles when possible
|
||||||
|
|
||||||
|
4. **Layer Management**: Use different line widths to indicate importance
|
||||||
|
- Key levels: 2-3px
|
||||||
|
- Secondary levels: 1px
|
||||||
|
- Reference lines: 1px dotted
|
||||||
|
|
||||||
|
5. **Symbol Association**: Always set the `symbol` field to associate shapes with specific charts
|
||||||
|
|
||||||
|
## Agent Usage Examples
|
||||||
|
|
||||||
|
### Drawing a Support Level
|
||||||
|
When user says "draw support at 42000":
|
||||||
|
```python
|
||||||
|
await create_or_update_shape(
|
||||||
|
shape_id=f"support-{int(time.time())}",
|
||||||
|
shape_type='horizontal_line',
|
||||||
|
points=[{'time': current_time, 'price': 42000.0}],
|
||||||
|
color='#089981',
|
||||||
|
line_width=2,
|
||||||
|
line_style='solid',
|
||||||
|
symbol=chart_store.chart_state.symbol,
|
||||||
|
properties={'extend_left': True, 'extend_right': True}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finding Shapes in Visible Range
|
||||||
|
When user asks "what drawings are on the chart?":
|
||||||
|
```python
|
||||||
|
shapes = search_shapes(
|
||||||
|
start_time=chart_store.chart_state.start_time,
|
||||||
|
end_time=chart_store.chart_state.end_time,
|
||||||
|
symbol=chart_store.chart_state.symbol
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Specific Shapes by ID
|
||||||
|
When user says "show me the details of trendline-1":
|
||||||
|
```python
|
||||||
|
# shape_ids parameter searches BOTH the actual ID and original_id fields
|
||||||
|
shapes = search_shapes(
|
||||||
|
shape_ids=['trendline-1']
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or to get selected shapes:
|
||||||
|
```python
|
||||||
|
selected_ids = chart_store.chart_state.selected_shapes
|
||||||
|
if selected_ids:
|
||||||
|
shapes = search_shapes(shape_ids=selected_ids)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finding Shapes by Original ID
|
||||||
|
When you need to find shapes you created using the original ID you specified:
|
||||||
|
```python
|
||||||
|
# Use the dedicated original_ids parameter
|
||||||
|
my_shapes = search_shapes(
|
||||||
|
original_ids=['my-support-line', 'my-trendline']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Or use shape_ids (which searches both id and original_id)
|
||||||
|
my_shapes = search_shapes(
|
||||||
|
shape_ids=['my-support-line', 'my-trendline']
|
||||||
|
)
|
||||||
|
|
||||||
|
for shape in my_shapes:
|
||||||
|
print(f"Original ID: {shape['original_id']}")
|
||||||
|
print(f"TradingView ID: {shape['id']}")
|
||||||
|
print(f"Type: {shape['type']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Searching Without Time Filter
|
||||||
|
When user asks "show me all support lines":
|
||||||
|
```python
|
||||||
|
support_lines = search_shapes(
|
||||||
|
shape_type='horizontal_line',
|
||||||
|
symbol=chart_store.chart_state.symbol
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drawing a Trendline
|
||||||
|
When user says "draw an uptrend from the lows":
|
||||||
|
```python
|
||||||
|
# Find swing lows using execute_python
|
||||||
|
# Then create trendline
|
||||||
|
await create_or_update_shape(
|
||||||
|
shape_id=f"trendline-{int(time.time())}",
|
||||||
|
shape_type='trend_line',
|
||||||
|
points=[
|
||||||
|
{'time': swing_low_1_time, 'price': swing_low_1_price},
|
||||||
|
{'time': swing_low_2_time, 'price': swing_low_2_price}
|
||||||
|
],
|
||||||
|
color='#2962FF',
|
||||||
|
line_width=2,
|
||||||
|
symbol=chart_store.chart_state.symbol
|
||||||
|
)
|
||||||
|
```
|
||||||
@@ -7,7 +7,7 @@ from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
|||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
from langgraph.prebuilt import create_react_agent
|
from langgraph.prebuilt import create_react_agent
|
||||||
|
|
||||||
from agent.tools import SYNC_TOOLS, DATASOURCE_TOOLS, INDICATOR_TOOLS, RESEARCH_TOOLS, CHART_TOOLS
|
from agent.tools import SYNC_TOOLS, DATASOURCE_TOOLS, INDICATOR_TOOLS, RESEARCH_TOOLS, CHART_TOOLS, SHAPE_TOOLS
|
||||||
from agent.memory import MemoryManager
|
from agent.memory import MemoryManager
|
||||||
from agent.session import SessionManager
|
from agent.session import SessionManager
|
||||||
from agent.prompts import build_system_prompt
|
from agent.prompts import build_system_prompt
|
||||||
@@ -65,10 +65,10 @@ class AgentExecutor:
|
|||||||
|
|
||||||
# Create agent without a static system prompt
|
# Create agent without a static system prompt
|
||||||
# We'll pass the dynamic system prompt via state_modifier at runtime
|
# We'll pass the dynamic system prompt via state_modifier at runtime
|
||||||
# Include all tool categories: sync, datasource, chart, indicator, and research
|
# Include all tool categories: sync, datasource, chart, indicator, shape, and research
|
||||||
self.agent = create_react_agent(
|
self.agent = create_react_agent(
|
||||||
self.llm,
|
self.llm,
|
||||||
SYNC_TOOLS + DATASOURCE_TOOLS + CHART_TOOLS + INDICATOR_TOOLS + RESEARCH_TOOLS,
|
SYNC_TOOLS + DATASOURCE_TOOLS + CHART_TOOLS + INDICATOR_TOOLS + SHAPE_TOOLS + RESEARCH_TOOLS,
|
||||||
checkpointer=checkpointer
|
checkpointer=checkpointer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ def _get_chart_store_context() -> str:
|
|||||||
interval = chart_data.get("interval", "N/A")
|
interval = chart_data.get("interval", "N/A")
|
||||||
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")
|
||||||
|
selected_shapes = chart_data.get("selected_shapes", [])
|
||||||
|
|
||||||
|
selected_info = ""
|
||||||
|
if selected_shapes:
|
||||||
|
selected_info = f"\n- **Selected Shapes**: {len(selected_shapes)} shape(s) selected (IDs: {', '.join(selected_shapes)})"
|
||||||
|
|
||||||
chart_context = f"""
|
chart_context = f"""
|
||||||
## Current Chart Context
|
## Current Chart Context
|
||||||
@@ -37,7 +42,7 @@ def _get_chart_store_context() -> str:
|
|||||||
The user is currently viewing a chart with the following settings:
|
The user is currently viewing a chart with the following settings:
|
||||||
- **Symbol**: {symbol}
|
- **Symbol**: {symbol}
|
||||||
- **Interval**: {interval}
|
- **Interval**: {interval}
|
||||||
- **Time Range**: {f"from {start_time} to {end_time}" if start_time and end_time else "not set"}
|
- **Time Range**: {f"from {start_time} to {end_time}" if start_time and end_time else "not set"}{selected_info}
|
||||||
|
|
||||||
This information is automatically available because you're connected via websocket.
|
This information is automatically available because you're connected via websocket.
|
||||||
When the user refers to "the chart", "this chart", or "what I'm viewing", this is what they mean.
|
When the user refers to "the chart", "this chart", or "what I'm viewing", this is what they mean.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ This package provides tools for:
|
|||||||
- Data sources and market data (datasource_tools)
|
- Data sources and market data (datasource_tools)
|
||||||
- Chart data access and analysis (chart_tools)
|
- Chart data access and analysis (chart_tools)
|
||||||
- Technical indicators (indicator_tools)
|
- Technical indicators (indicator_tools)
|
||||||
|
- Shape/drawing management (shape_tools)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Global registries that will be set by main.py
|
# Global registries that will be set by main.py
|
||||||
@@ -37,6 +38,7 @@ from .datasource_tools import DATASOURCE_TOOLS
|
|||||||
from .chart_tools import CHART_TOOLS
|
from .chart_tools import CHART_TOOLS
|
||||||
from .indicator_tools import INDICATOR_TOOLS
|
from .indicator_tools import INDICATOR_TOOLS
|
||||||
from .research_tools import RESEARCH_TOOLS
|
from .research_tools import RESEARCH_TOOLS
|
||||||
|
from .shape_tools import SHAPE_TOOLS
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"set_registry",
|
"set_registry",
|
||||||
@@ -47,4 +49,5 @@ __all__ = [
|
|||||||
"CHART_TOOLS",
|
"CHART_TOOLS",
|
||||||
"INDICATOR_TOOLS",
|
"INDICATOR_TOOLS",
|
||||||
"RESEARCH_TOOLS",
|
"RESEARCH_TOOLS",
|
||||||
|
"SHAPE_TOOLS",
|
||||||
]
|
]
|
||||||
|
|||||||
475
backend/src/agent/tools/shape_tools.py
Normal file
475
backend/src/agent/tools/shape_tools.py
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
"""Shape/drawing tools for chart analysis."""
|
||||||
|
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Map legacy/common shape type names to TradingView's native names
|
||||||
|
SHAPE_TYPE_ALIASES: Dict[str, str] = {
|
||||||
|
'trendline': 'trend_line',
|
||||||
|
'fibonacci': 'fib_retracement',
|
||||||
|
'fibonacci_extension': 'fib_trend_ext',
|
||||||
|
'gann_fan': 'gannbox_fan',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_registry():
|
||||||
|
"""Get the global registry instance."""
|
||||||
|
from . import _registry
|
||||||
|
return _registry
|
||||||
|
|
||||||
|
|
||||||
|
def _get_shape_store():
|
||||||
|
"""Get the global ShapeStore instance."""
|
||||||
|
registry = _get_registry()
|
||||||
|
if registry and "ShapeStore" in registry.entries:
|
||||||
|
return registry.entries["ShapeStore"].model
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def search_shapes(
|
||||||
|
start_time: Optional[int] = None,
|
||||||
|
end_time: Optional[int] = None,
|
||||||
|
shape_type: Optional[str] = None,
|
||||||
|
symbol: Optional[str] = None,
|
||||||
|
shape_ids: Optional[List[str]] = None,
|
||||||
|
original_ids: Optional[List[str]] = None
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Search for shapes/drawings using flexible filters.
|
||||||
|
|
||||||
|
This tool can search shapes by:
|
||||||
|
- Time range (finds shapes that overlap the range)
|
||||||
|
- Shape type (e.g., 'trendline', 'horizontal_line')
|
||||||
|
- Symbol (e.g., 'BINANCE:BTC/USDT')
|
||||||
|
- Specific shape IDs (TradingView's assigned IDs)
|
||||||
|
- Original IDs (the IDs you specified when creating shapes)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start_time: Optional start of time range (Unix timestamp in seconds)
|
||||||
|
end_time: Optional end of time range (Unix timestamp in seconds)
|
||||||
|
shape_type: Optional filter by shape type (e.g., 'trend_line', 'horizontal_line', 'rectangle')
|
||||||
|
symbol: Optional filter by symbol (e.g., 'BINANCE:BTC/USDT')
|
||||||
|
shape_ids: Optional list of specific shape IDs to retrieve (searches both id and original_id fields)
|
||||||
|
original_ids: Optional list of original IDs to search for (the IDs you specified when creating)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching shapes, each as a dictionary with:
|
||||||
|
- id: Shape identifier (TradingView's assigned ID)
|
||||||
|
- original_id: The ID you specified when creating the shape (if applicable)
|
||||||
|
- type: Shape type
|
||||||
|
- points: List of control points with time and price
|
||||||
|
- color, line_width, line_style: Visual properties
|
||||||
|
- properties: Additional shape-specific properties
|
||||||
|
- symbol: Symbol the shape is drawn on
|
||||||
|
- created_at, modified_at: Timestamps
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Find all shapes in the currently visible chart range
|
||||||
|
shapes = search_shapes(
|
||||||
|
start_time=chart_state.start_time,
|
||||||
|
end_time=chart_state.end_time
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find only trendlines in a specific time range
|
||||||
|
trendlines = search_shapes(
|
||||||
|
start_time=1640000000,
|
||||||
|
end_time=1650000000,
|
||||||
|
shape_type='trend_line'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find shapes for a specific symbol
|
||||||
|
btc_shapes = search_shapes(
|
||||||
|
start_time=1640000000,
|
||||||
|
end_time=1650000000,
|
||||||
|
symbol='BINANCE:BTC/USDT'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get specific shapes by TradingView ID or original ID
|
||||||
|
# This searches both the 'id' and 'original_id' fields
|
||||||
|
selected = search_shapes(
|
||||||
|
shape_ids=['trendline-1', 'support-42k', 'fib-retracement-1']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get shapes by the original IDs you specified when creating them
|
||||||
|
my_shapes = search_shapes(
|
||||||
|
original_ids=['my-support-line', 'my-resistance-line']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all trendlines (no time filter)
|
||||||
|
all_trendlines = search_shapes(shape_type='trend_line')
|
||||||
|
"""
|
||||||
|
shape_store = _get_shape_store()
|
||||||
|
if not shape_store:
|
||||||
|
raise ValueError("ShapeStore not initialized")
|
||||||
|
|
||||||
|
shapes_dict = shape_store.shapes
|
||||||
|
matching_shapes = []
|
||||||
|
|
||||||
|
# If specific shape IDs are requested, search by both id and original_id
|
||||||
|
if shape_ids:
|
||||||
|
for requested_id in shape_ids:
|
||||||
|
# First try direct ID lookup
|
||||||
|
shape = shapes_dict.get(requested_id)
|
||||||
|
if shape:
|
||||||
|
# Still apply other filters if specified
|
||||||
|
if symbol and shape.get('symbol') != symbol:
|
||||||
|
continue
|
||||||
|
if shape_type and shape.get('type') != shape_type:
|
||||||
|
continue
|
||||||
|
matching_shapes.append(shape)
|
||||||
|
else:
|
||||||
|
# If not found by ID, search by original_id
|
||||||
|
for shape_id, shape in shapes_dict.items():
|
||||||
|
if shape.get('original_id') == requested_id:
|
||||||
|
# Still apply other filters if specified
|
||||||
|
if symbol and shape.get('symbol') != symbol:
|
||||||
|
continue
|
||||||
|
if shape_type and shape.get('type') != shape_type:
|
||||||
|
continue
|
||||||
|
matching_shapes.append(shape)
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Found {len(matching_shapes)} shapes by ID filter (requested {len(shape_ids)} IDs)"
|
||||||
|
+ (f" for type '{shape_type}'" if shape_type else "")
|
||||||
|
+ (f" on symbol '{symbol}'" if symbol else "")
|
||||||
|
)
|
||||||
|
return matching_shapes
|
||||||
|
|
||||||
|
# If specific original IDs are requested, search by original_id only
|
||||||
|
if original_ids:
|
||||||
|
for original_id in original_ids:
|
||||||
|
for shape_id, shape in shapes_dict.items():
|
||||||
|
if shape.get('original_id') == original_id:
|
||||||
|
# Still apply other filters if specified
|
||||||
|
if symbol and shape.get('symbol') != symbol:
|
||||||
|
continue
|
||||||
|
if shape_type and shape.get('type') != shape_type:
|
||||||
|
continue
|
||||||
|
matching_shapes.append(shape)
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Found {len(matching_shapes)} shapes by original_id filter (requested {len(original_ids)} IDs)"
|
||||||
|
+ (f" for type '{shape_type}'" if shape_type else "")
|
||||||
|
+ (f" on symbol '{symbol}'" if symbol else "")
|
||||||
|
)
|
||||||
|
return matching_shapes
|
||||||
|
|
||||||
|
# Otherwise, search all shapes with filters
|
||||||
|
for shape_id, shape in shapes_dict.items():
|
||||||
|
# Filter by symbol if specified
|
||||||
|
if symbol and shape.get('symbol') != symbol:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter by type if specified
|
||||||
|
if shape_type and shape.get('type') != shape_type:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter by time range if specified
|
||||||
|
if start_time is not None and end_time is not None:
|
||||||
|
# Check if any control point falls within the time range
|
||||||
|
# or if the shape spans across the time range
|
||||||
|
points = shape.get('points', [])
|
||||||
|
if not points:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get min and max times from shape's control points
|
||||||
|
shape_times = [point['time'] for point in points]
|
||||||
|
shape_min_time = min(shape_times)
|
||||||
|
shape_max_time = max(shape_times)
|
||||||
|
|
||||||
|
# Check for overlap: shape overlaps if its range intersects with query range
|
||||||
|
if not (shape_max_time >= start_time and shape_min_time <= end_time):
|
||||||
|
continue
|
||||||
|
|
||||||
|
matching_shapes.append(shape)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Found {len(matching_shapes)} shapes"
|
||||||
|
+ (f" in time range {start_time}-{end_time}" if start_time and end_time else "")
|
||||||
|
+ (f" for type '{shape_type}'" if shape_type else "")
|
||||||
|
+ (f" on symbol '{symbol}'" if symbol else "")
|
||||||
|
)
|
||||||
|
|
||||||
|
return matching_shapes
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
async def create_or_update_shape(
|
||||||
|
shape_id: str,
|
||||||
|
shape_type: str,
|
||||||
|
points: List[Dict[str, Any]],
|
||||||
|
color: Optional[str] = None,
|
||||||
|
line_width: Optional[int] = None,
|
||||||
|
line_style: Optional[str] = None,
|
||||||
|
properties: Optional[Dict[str, Any]] = None,
|
||||||
|
symbol: Optional[str] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create a new shape or update an existing shape on the chart.
|
||||||
|
|
||||||
|
This tool allows the agent to draw shapes on the user's chart or modify
|
||||||
|
existing shapes. Shapes are synchronized to the frontend in real-time.
|
||||||
|
|
||||||
|
IMPORTANT - Shape ID Mapping:
|
||||||
|
When you create a shape, TradingView will assign its own internal ID that differs
|
||||||
|
from the shape_id you provide. The shape will be updated in the store with:
|
||||||
|
- id: TradingView's assigned ID
|
||||||
|
- original_id: The shape_id you provided
|
||||||
|
|
||||||
|
To find your shape later, use search_shapes() and filter by original_id field.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# Create a shape
|
||||||
|
await create_or_update_shape(shape_id='my-support', ...)
|
||||||
|
|
||||||
|
# Later, find it by original_id
|
||||||
|
shapes = search_shapes(symbol='BINANCE:BTC/USDT')
|
||||||
|
my_shape = next((s for s in shapes if s.get('original_id') == 'my-support'), None)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shape_id: Unique identifier for the shape (use existing ID to update, new ID to create)
|
||||||
|
Note: TradingView will assign its own ID; your ID will be stored in original_id
|
||||||
|
shape_type: Type of shape using TradingView's native names.
|
||||||
|
|
||||||
|
Single-point shapes (use 1 point):
|
||||||
|
- 'horizontal_line': Horizontal support/resistance line
|
||||||
|
- 'vertical_line': Vertical time marker
|
||||||
|
- 'text': Text label
|
||||||
|
- 'anchored_text': Anchored text annotation
|
||||||
|
- 'anchored_note': Anchored note
|
||||||
|
- 'note': Note annotation
|
||||||
|
- 'emoji': Emoji marker
|
||||||
|
- 'icon': Icon marker
|
||||||
|
- 'sticker': Sticker marker
|
||||||
|
- 'arrow_up': Upward arrow marker
|
||||||
|
- 'arrow_down': Downward arrow marker
|
||||||
|
- 'flag': Flag marker
|
||||||
|
- 'long_position': Long position marker
|
||||||
|
- 'short_position': Short position marker
|
||||||
|
|
||||||
|
Multi-point shapes (use 2+ points):
|
||||||
|
- 'trend_line': Trendline (2 points)
|
||||||
|
- 'rectangle': Rectangle (2 points: top-left, bottom-right)
|
||||||
|
- 'fib_retracement': Fibonacci retracement (2 points)
|
||||||
|
- 'fib_trend_ext': Fibonacci extension (3 points)
|
||||||
|
- 'parallel_channel': Parallel channel (3 points)
|
||||||
|
- 'arrow': Arrow (2 points)
|
||||||
|
- 'circle': Circle/ellipse (2-3 points)
|
||||||
|
- 'path': Free drawing path (3+ points)
|
||||||
|
- 'pitchfork': Andrew's pitchfork (3 points)
|
||||||
|
- 'gannbox_fan': Gann fan (2 points)
|
||||||
|
- 'head_and_shoulders': Head and shoulders pattern (5 points)
|
||||||
|
|
||||||
|
points: List of control points, each with 'time' (Unix seconds) and 'price' fields
|
||||||
|
color: Optional color (hex like '#FF0000' or name like 'red')
|
||||||
|
line_width: Optional line width in pixels (default: 1)
|
||||||
|
line_style: Optional line style: 'solid', 'dashed', 'dotted' (default: 'solid')
|
||||||
|
properties: Optional dict of additional shape-specific properties
|
||||||
|
symbol: Optional symbol to associate with the shape (defaults to current chart symbol)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with:
|
||||||
|
- status: 'created' or 'updated'
|
||||||
|
- shape: The complete shape object (initially with your ID, will be updated to TV ID)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Draw a trendline between two points
|
||||||
|
await create_or_update_shape(
|
||||||
|
shape_id='my-trendline-1',
|
||||||
|
shape_type='trend_line',
|
||||||
|
points=[
|
||||||
|
{'time': 1640000000, 'price': 45000.0},
|
||||||
|
{'time': 1650000000, 'price': 50000.0}
|
||||||
|
],
|
||||||
|
color='#00FF00',
|
||||||
|
line_width=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw a horizontal support line
|
||||||
|
await create_or_update_shape(
|
||||||
|
shape_id='support-1',
|
||||||
|
shape_type='horizontal_line',
|
||||||
|
points=[{'time': 1640000000, 'price': 42000.0}],
|
||||||
|
color='blue',
|
||||||
|
line_style='dashed'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find your shape after creation using original_id
|
||||||
|
shapes = search_shapes(symbol='BINANCE:BTC/USDT')
|
||||||
|
my_shape = next((s for s in shapes if s.get('original_id') == 'support-1'), None)
|
||||||
|
if my_shape:
|
||||||
|
print(f"TradingView assigned ID: {my_shape['id']}")
|
||||||
|
"""
|
||||||
|
from schema.shape import Shape, ControlPoint
|
||||||
|
import time as time_module
|
||||||
|
|
||||||
|
registry = _get_registry()
|
||||||
|
if not registry:
|
||||||
|
raise ValueError("SyncRegistry not initialized")
|
||||||
|
|
||||||
|
shape_store = _get_shape_store()
|
||||||
|
if not shape_store:
|
||||||
|
raise ValueError("ShapeStore not initialized")
|
||||||
|
|
||||||
|
# Normalize shape type (handle legacy names)
|
||||||
|
normalized_type = SHAPE_TYPE_ALIASES.get(shape_type, shape_type)
|
||||||
|
if normalized_type != shape_type:
|
||||||
|
logger.info(f"Normalized shape type '{shape_type}' -> '{normalized_type}'")
|
||||||
|
|
||||||
|
# Convert points to ControlPoint objects
|
||||||
|
control_points = []
|
||||||
|
for p in points:
|
||||||
|
point_data = {
|
||||||
|
'time': p['time'],
|
||||||
|
'price': p['price']
|
||||||
|
}
|
||||||
|
# Only include channel if it's actually provided
|
||||||
|
if 'channel' in p and p['channel'] is not None:
|
||||||
|
point_data['channel'] = p['channel']
|
||||||
|
control_points.append(ControlPoint(**point_data))
|
||||||
|
|
||||||
|
# Check if updating existing shape
|
||||||
|
existing_shape = shape_store.shapes.get(shape_id)
|
||||||
|
is_update = existing_shape is not None
|
||||||
|
|
||||||
|
# If symbol is not provided, try to get it from ChartStore
|
||||||
|
if symbol is None and "ChartStore" in registry.entries:
|
||||||
|
chart_store = registry.entries["ChartStore"].model
|
||||||
|
if hasattr(chart_store, 'chart_state') and hasattr(chart_store.chart_state, 'symbol'):
|
||||||
|
symbol = chart_store.chart_state.symbol
|
||||||
|
logger.info(f"Using current chart symbol for shape: {symbol}")
|
||||||
|
|
||||||
|
now = int(time_module.time())
|
||||||
|
|
||||||
|
# Create shape object
|
||||||
|
shape = Shape(
|
||||||
|
id=shape_id,
|
||||||
|
type=normalized_type,
|
||||||
|
points=control_points,
|
||||||
|
color=color,
|
||||||
|
line_width=line_width,
|
||||||
|
line_style=line_style,
|
||||||
|
properties=properties or {},
|
||||||
|
symbol=symbol,
|
||||||
|
created_at=existing_shape.get('created_at') if existing_shape else now,
|
||||||
|
modified_at=now
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the store
|
||||||
|
shape_store.shapes[shape_id] = shape.model_dump(mode="json")
|
||||||
|
|
||||||
|
# Trigger sync
|
||||||
|
await registry.push_all()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"{'Updated' if is_update else 'Created'} shape '{shape_id}' "
|
||||||
|
f"of type '{shape_type}' with {len(points)} points"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "updated" if is_update else "created",
|
||||||
|
"shape": shape.model_dump(mode="json")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
async def delete_shape(shape_id: str) -> Dict[str, str]:
|
||||||
|
"""Delete a shape from the chart.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shape_id: ID of the shape to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with status message
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If shape doesn't exist
|
||||||
|
|
||||||
|
Example:
|
||||||
|
await delete_shape('my-trendline-1')
|
||||||
|
"""
|
||||||
|
registry = _get_registry()
|
||||||
|
if not registry:
|
||||||
|
raise ValueError("SyncRegistry not initialized")
|
||||||
|
|
||||||
|
shape_store = _get_shape_store()
|
||||||
|
if not shape_store:
|
||||||
|
raise ValueError("ShapeStore not initialized")
|
||||||
|
|
||||||
|
if shape_id not in shape_store.shapes:
|
||||||
|
raise ValueError(f"Shape '{shape_id}' not found")
|
||||||
|
|
||||||
|
# Delete the shape
|
||||||
|
del shape_store.shapes[shape_id]
|
||||||
|
|
||||||
|
# Trigger sync
|
||||||
|
await registry.push_all()
|
||||||
|
|
||||||
|
logger.info(f"Deleted shape '{shape_id}'")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Shape '{shape_id}' deleted"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_shape(shape_id: str) -> Dict[str, Any]:
|
||||||
|
"""Get details of a specific shape by ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shape_id: ID of the shape to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing the shape data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If shape doesn't exist
|
||||||
|
|
||||||
|
Example:
|
||||||
|
shape = get_shape('my-trendline-1')
|
||||||
|
print(f"Shape type: {shape['type']}")
|
||||||
|
print(f"Points: {shape['points']}")
|
||||||
|
"""
|
||||||
|
shape_store = _get_shape_store()
|
||||||
|
if not shape_store:
|
||||||
|
raise ValueError("ShapeStore not initialized")
|
||||||
|
|
||||||
|
shape = shape_store.shapes.get(shape_id)
|
||||||
|
if not shape:
|
||||||
|
raise ValueError(f"Shape '{shape_id}' not found")
|
||||||
|
|
||||||
|
return shape
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def list_all_shapes() -> List[Dict[str, Any]]:
|
||||||
|
"""List all shapes currently on the chart.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all shapes as dictionaries
|
||||||
|
|
||||||
|
Example:
|
||||||
|
shapes = list_all_shapes()
|
||||||
|
print(f"Total shapes: {len(shapes)}")
|
||||||
|
for shape in shapes:
|
||||||
|
print(f" - {shape['id']}: {shape['type']}")
|
||||||
|
"""
|
||||||
|
shape_store = _get_shape_store()
|
||||||
|
if not shape_store:
|
||||||
|
raise ValueError("ShapeStore not initialized")
|
||||||
|
|
||||||
|
return list(shape_store.shapes.values())
|
||||||
|
|
||||||
|
|
||||||
|
SHAPE_TOOLS = [
|
||||||
|
search_shapes,
|
||||||
|
create_or_update_shape,
|
||||||
|
delete_shape,
|
||||||
|
get_shape,
|
||||||
|
list_all_shapes
|
||||||
|
]
|
||||||
@@ -23,6 +23,7 @@ from agent.core import create_agent
|
|||||||
from agent.tools import set_registry, set_datasource_registry, set_indicator_registry
|
from agent.tools import set_registry, set_datasource_registry, set_indicator_registry
|
||||||
from schema.order_spec import SwapOrder
|
from schema.order_spec import SwapOrder
|
||||||
from schema.chart_state import ChartState
|
from schema.chart_state import ChartState
|
||||||
|
from schema.shape import ShapeCollection
|
||||||
from datasource.registry import DataSourceRegistry
|
from datasource.registry import DataSourceRegistry
|
||||||
from datasource.subscription_manager import SubscriptionManager
|
from datasource.subscription_manager import SubscriptionManager
|
||||||
from datasource.websocket_handler import DatafeedWebSocketHandler
|
from datasource.websocket_handler import DatafeedWebSocketHandler
|
||||||
@@ -124,7 +125,7 @@ async def lifespan(app: FastAPI):
|
|||||||
chroma_db_path=config["memory"]["chroma_db"],
|
chroma_db_path=config["memory"]["chroma_db"],
|
||||||
embedding_model=config["memory"]["embedding_model"],
|
embedding_model=config["memory"]["embedding_model"],
|
||||||
context_docs_dir=config["agent"]["context_docs_dir"],
|
context_docs_dir=config["agent"]["context_docs_dir"],
|
||||||
base_dir=".." # Point to project root from backend/src
|
base_dir="." # backend/src is the working directory, so . goes to backend, where memory/ lives
|
||||||
)
|
)
|
||||||
|
|
||||||
await agent_executor.initialize()
|
await agent_executor.initialize()
|
||||||
@@ -159,13 +160,19 @@ class OrderStore(BaseModel):
|
|||||||
class ChartStore(BaseModel):
|
class ChartStore(BaseModel):
|
||||||
chart_state: ChartState = ChartState()
|
chart_state: ChartState = ChartState()
|
||||||
|
|
||||||
|
# ShapeStore model for synchronization
|
||||||
|
class ShapeStore(BaseModel):
|
||||||
|
shapes: dict[str, dict] = {} # Dictionary of shapes keyed by ID
|
||||||
|
|
||||||
# Initialize stores
|
# Initialize stores
|
||||||
order_store = OrderStore()
|
order_store = OrderStore()
|
||||||
chart_store = ChartStore()
|
chart_store = ChartStore()
|
||||||
|
shape_store = ShapeStore()
|
||||||
|
|
||||||
# Register with SyncRegistry
|
# Register with SyncRegistry
|
||||||
registry.register(order_store, store_name="OrderStore")
|
registry.register(order_store, store_name="OrderStore")
|
||||||
registry.register(chart_store, store_name="ChartStore")
|
registry.register(chart_store, store_name="ChartStore")
|
||||||
|
registry.register(shape_store, store_name="ShapeStore")
|
||||||
|
|
||||||
@app.websocket("/ws")
|
@app.websocket("/ws")
|
||||||
async def websocket_endpoint(websocket: WebSocket):
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
@@ -361,11 +368,14 @@ async def websocket_endpoint(websocket: WebSocket):
|
|||||||
elif msg_type == "patch":
|
elif msg_type == "patch":
|
||||||
patch_msg = PatchMessage(**message_json)
|
patch_msg = PatchMessage(**message_json)
|
||||||
logger.info(f"Patch message received for store: {patch_msg.store}, seq: {patch_msg.seq}")
|
logger.info(f"Patch message received for store: {patch_msg.store}, seq: {patch_msg.seq}")
|
||||||
await registry.apply_client_patch(
|
try:
|
||||||
store_name=patch_msg.store,
|
await registry.apply_client_patch(
|
||||||
client_base_seq=patch_msg.seq,
|
store_name=patch_msg.store,
|
||||||
patch=patch_msg.patch
|
client_base_seq=patch_msg.seq,
|
||||||
)
|
patch=patch_msg.patch
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error applying client patch: {e}. Client will receive snapshot to resync.", exc_info=True)
|
||||||
elif msg_type == "agent_user_message":
|
elif msg_type == "agent_user_message":
|
||||||
# Handle agent messages directly here
|
# Handle agent messages directly here
|
||||||
print(f"[DEBUG] Raw message_json: {message_json}")
|
print(f"[DEBUG] Raw message_json: {message_json}")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@@ -23,3 +23,6 @@ class ChartState(BaseModel):
|
|||||||
# Optional: Chart interval/resolution
|
# Optional: Chart interval/resolution
|
||||||
# None when chart is not visible
|
# 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")
|
interval: Optional[str] = Field(default="15", description="Chart interval (e.g., '1', '5', '15', '60', 'D'), or None if no chart visible")
|
||||||
|
|
||||||
|
# Selected shapes/drawings on the chart
|
||||||
|
selected_shapes: List[str] = Field(default_factory=list, description="Array of selected shape IDs")
|
||||||
|
|||||||
44
backend/src/schema/shape.py
Normal file
44
backend/src/schema/shape.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class ControlPoint(BaseModel):
|
||||||
|
"""A control point for a drawing shape.
|
||||||
|
|
||||||
|
Control points define the position and properties of a shape.
|
||||||
|
Different shapes have different numbers of control points.
|
||||||
|
"""
|
||||||
|
time: int = Field(..., description="Unix timestamp in seconds")
|
||||||
|
price: float = Field(..., description="Price level")
|
||||||
|
# Optional channel for multi-point shapes (e.g., parallel channels)
|
||||||
|
channel: Optional[str] = Field(default=None, description="Channel identifier for multi-point shapes")
|
||||||
|
|
||||||
|
|
||||||
|
class Shape(BaseModel):
|
||||||
|
"""A TradingView drawing shape/study.
|
||||||
|
|
||||||
|
Represents any drawing the user creates on the chart (trendlines,
|
||||||
|
horizontal lines, rectangles, Fibonacci retracements, etc.)
|
||||||
|
"""
|
||||||
|
id: str = Field(..., description="Unique identifier for the shape")
|
||||||
|
type: str = Field(..., description="Shape type (e.g., 'trendline', 'horizontal_line', 'rectangle', 'fibonacci')")
|
||||||
|
points: List[ControlPoint] = Field(default_factory=list, description="Control points that define the shape")
|
||||||
|
|
||||||
|
# Visual properties
|
||||||
|
color: Optional[str] = Field(default=None, description="Shape color (hex or color name)")
|
||||||
|
line_width: Optional[int] = Field(default=1, description="Line width in pixels")
|
||||||
|
line_style: Optional[str] = Field(default="solid", description="Line style: 'solid', 'dashed', 'dotted'")
|
||||||
|
|
||||||
|
# Shape-specific properties stored as flexible dict
|
||||||
|
properties: Dict[str, Any] = Field(default_factory=dict, description="Additional shape-specific properties")
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
symbol: Optional[str] = Field(default=None, description="Symbol this shape is drawn on")
|
||||||
|
created_at: Optional[int] = Field(default=None, description="Creation timestamp (Unix seconds)")
|
||||||
|
modified_at: Optional[int] = Field(default=None, description="Last modification timestamp (Unix seconds)")
|
||||||
|
original_id: Optional[str] = Field(default=None, description="Original ID from backend/agent before TradingView assigns its own ID")
|
||||||
|
|
||||||
|
|
||||||
|
class ShapeCollection(BaseModel):
|
||||||
|
"""Collection of all shapes/drawings on the chart."""
|
||||||
|
shapes: Dict[str, Shape] = Field(default_factory=dict, description="Dictionary of shapes keyed by ID")
|
||||||
@@ -105,67 +105,105 @@ class SyncRegistry:
|
|||||||
|
|
||||||
logger.info(f"apply_client_patch: Current backend seq={entry.seq}")
|
logger.info(f"apply_client_patch: Current backend seq={entry.seq}")
|
||||||
|
|
||||||
if client_base_seq == entry.seq:
|
try:
|
||||||
# No conflict
|
if client_base_seq == entry.seq:
|
||||||
logger.info("apply_client_patch: No conflict - applying patch directly")
|
# No conflict
|
||||||
current_state = entry.model.model_dump(mode="json")
|
logger.info("apply_client_patch: No conflict - applying patch directly")
|
||||||
logger.info(f"apply_client_patch: Current state before patch: {current_state}")
|
current_state = entry.model.model_dump(mode="json")
|
||||||
new_state = jsonpatch.apply_patch(current_state, patch)
|
logger.info(f"apply_client_patch: Current state before patch: {current_state}")
|
||||||
logger.info(f"apply_client_patch: New state after patch: {new_state}")
|
try:
|
||||||
self._update_model(entry.model, new_state)
|
new_state = jsonpatch.apply_patch(current_state, patch)
|
||||||
|
logger.info(f"apply_client_patch: New state after patch: {new_state}")
|
||||||
|
self._update_model(entry.model, new_state)
|
||||||
|
|
||||||
entry.commit_patch(patch)
|
entry.commit_patch(patch)
|
||||||
logger.info(f"apply_client_patch: Patch committed, new seq={entry.seq}")
|
logger.info(f"apply_client_patch: Patch committed, new seq={entry.seq}")
|
||||||
# Don't broadcast back to client - they already have this change
|
# Don't broadcast back to client - they already have this change
|
||||||
# Broadcasting would cause an infinite loop
|
# Broadcasting would cause an infinite loop
|
||||||
logger.info("apply_client_patch: Not broadcasting back to originating client")
|
logger.info("apply_client_patch: Not broadcasting back to originating client")
|
||||||
|
except jsonpatch.JsonPatchConflict as e:
|
||||||
elif client_base_seq < entry.seq:
|
logger.warning(f"apply_client_patch: Patch conflict on no-conflict path: {e}. Sending snapshot to resync.")
|
||||||
# Conflict! Frontend wins.
|
# Send snapshot to force resync
|
||||||
# 1. Get backend patches since client_base_seq
|
if self.websocket:
|
||||||
backend_patches = []
|
msg = SnapshotMessage(
|
||||||
for seq, p in entry.history:
|
store=entry.store_name,
|
||||||
if seq > client_base_seq:
|
seq=entry.seq,
|
||||||
backend_patches.append(p)
|
state=entry.model.model_dump(mode="json")
|
||||||
|
)
|
||||||
# 2. Apply frontend patch first to the state at client_base_seq
|
await self.websocket.send_json(msg.model_dump(mode="json"))
|
||||||
# But we only have the current authoritative model.
|
|
||||||
# "Apply the frontend patch first to the model (frontend wins)"
|
elif client_base_seq < entry.seq:
|
||||||
# "Re-apply the backend deltas that do not overlap the frontend's changed paths on top"
|
# Conflict! Frontend wins.
|
||||||
|
# 1. Get backend patches since client_base_seq
|
||||||
# Let's get the state as it was at client_base_seq if possible?
|
backend_patches = []
|
||||||
# No, history only has patches.
|
for seq, p in entry.history:
|
||||||
|
if seq > client_base_seq:
|
||||||
# Alternative: Apply frontend patch to current model.
|
backend_patches.append(p)
|
||||||
# Then re-apply backend patches, but discard parts that overlap.
|
|
||||||
|
# 2. Apply frontend patch first to the state at client_base_seq
|
||||||
frontend_paths = {p['path'] for p in patch}
|
# But we only have the current authoritative model.
|
||||||
|
# "Apply the frontend patch first to the model (frontend wins)"
|
||||||
current_state = entry.model.model_dump(mode="json")
|
# "Re-apply the backend deltas that do not overlap the frontend's changed paths on top"
|
||||||
# Apply frontend patch
|
|
||||||
new_state = jsonpatch.apply_patch(current_state, patch)
|
# Let's get the state as it was at client_base_seq if possible?
|
||||||
|
# No, history only has patches.
|
||||||
# Re-apply backend patches that don't overlap
|
|
||||||
for b_patch in backend_patches:
|
# Alternative: Apply frontend patch to current model.
|
||||||
filtered_b_patch = [op for op in b_patch if op['path'] not in frontend_paths]
|
# Then re-apply backend patches, but discard parts that overlap.
|
||||||
if filtered_b_patch:
|
|
||||||
new_state = jsonpatch.apply_patch(new_state, filtered_b_patch)
|
frontend_paths = {p['path'] for p in patch}
|
||||||
|
|
||||||
self._update_model(entry.model, new_state)
|
current_state = entry.model.model_dump(mode="json")
|
||||||
|
# Apply frontend patch
|
||||||
# Commit the result as a single new patch
|
try:
|
||||||
# We need to compute what changed from last_snapshot to new_state
|
new_state = jsonpatch.apply_patch(current_state, patch)
|
||||||
final_patch = jsonpatch.make_patch(entry.last_snapshot, new_state).patch
|
except jsonpatch.JsonPatchConflict as e:
|
||||||
if final_patch:
|
logger.warning(f"apply_client_patch: Failed to apply client patch during conflict resolution: {e}. Sending snapshot to resync.")
|
||||||
entry.commit_patch(final_patch)
|
# Send snapshot to force resync
|
||||||
# Broadcast resolved state as snapshot to converge
|
if self.websocket:
|
||||||
if self.websocket:
|
msg = SnapshotMessage(
|
||||||
msg = SnapshotMessage(
|
store=entry.store_name,
|
||||||
store=entry.store_name,
|
seq=entry.seq,
|
||||||
seq=entry.seq,
|
state=entry.model.model_dump(mode="json")
|
||||||
state=entry.model.model_dump(mode="json")
|
)
|
||||||
)
|
await self.websocket.send_json(msg.model_dump(mode="json"))
|
||||||
await self.websocket.send_json(msg.model_dump(mode="json"))
|
return
|
||||||
|
|
||||||
|
# Re-apply backend patches that don't overlap
|
||||||
|
for b_patch in backend_patches:
|
||||||
|
filtered_b_patch = [op for op in b_patch if op['path'] not in frontend_paths]
|
||||||
|
if filtered_b_patch:
|
||||||
|
try:
|
||||||
|
new_state = jsonpatch.apply_patch(new_state, filtered_b_patch)
|
||||||
|
except jsonpatch.JsonPatchConflict as e:
|
||||||
|
logger.warning(f"apply_client_patch: Failed to apply backend patch during conflict resolution: {e}. Skipping this patch.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._update_model(entry.model, new_state)
|
||||||
|
|
||||||
|
# Commit the result as a single new patch
|
||||||
|
# We need to compute what changed from last_snapshot to new_state
|
||||||
|
final_patch = jsonpatch.make_patch(entry.last_snapshot, new_state).patch
|
||||||
|
if final_patch:
|
||||||
|
entry.commit_patch(final_patch)
|
||||||
|
# Broadcast resolved state as snapshot to converge
|
||||||
|
if self.websocket:
|
||||||
|
msg = SnapshotMessage(
|
||||||
|
store=entry.store_name,
|
||||||
|
seq=entry.seq,
|
||||||
|
state=entry.model.model_dump(mode="json")
|
||||||
|
)
|
||||||
|
await self.websocket.send_json(msg.model_dump(mode="json"))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"apply_client_patch: Unexpected error: {e}. Sending snapshot to resync.", exc_info=True)
|
||||||
|
# Send snapshot to force resync
|
||||||
|
if self.websocket:
|
||||||
|
msg = SnapshotMessage(
|
||||||
|
store=entry.store_name,
|
||||||
|
seq=entry.seq,
|
||||||
|
state=entry.model.model_dump(mode="json")
|
||||||
|
)
|
||||||
|
await self.websocket.send_json(msg.model_dump(mode="json"))
|
||||||
|
|
||||||
def _update_model(self, model: BaseModel, new_data: Dict[str, Any]):
|
def _update_model(self, model: BaseModel, new_data: Dict[str, Any]):
|
||||||
# Update model using model_validate for potentially nested models
|
# Update model using model_validate for potentially nested models
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ChatPanel from './components/ChatPanel.vue'
|
|||||||
import LoginScreen from './components/LoginScreen.vue'
|
import LoginScreen from './components/LoginScreen.vue'
|
||||||
import { useOrderStore } from './stores/orders'
|
import { useOrderStore } from './stores/orders'
|
||||||
import { useChartStore } from './stores/chart'
|
import { useChartStore } from './stores/chart'
|
||||||
|
import { useShapeStore } from './stores/shapes'
|
||||||
import { useStateSync } from './composables/useStateSync'
|
import { useStateSync } from './composables/useStateSync'
|
||||||
import { wsManager } from './composables/useWebSocket'
|
import { wsManager } from './composables/useWebSocket'
|
||||||
|
|
||||||
@@ -63,9 +64,11 @@ const handleAuthenticate = async (
|
|||||||
// Initialize state sync after successful authentication
|
// Initialize state sync after successful authentication
|
||||||
const orderStore = useOrderStore()
|
const orderStore = useOrderStore()
|
||||||
const chartStore = useChartStore()
|
const chartStore = useChartStore()
|
||||||
|
const shapeStore = useShapeStore()
|
||||||
const stateSync = useStateSync({
|
const stateSync = useStateSync({
|
||||||
OrderStore: orderStore,
|
OrderStore: orderStore,
|
||||||
ChartStore: chartStore
|
ChartStore: chartStore,
|
||||||
|
ShapeStore: shapeStore
|
||||||
})
|
})
|
||||||
stateSyncCleanup = stateSync.cleanup
|
stateSyncCleanup = stateSync.cleanup
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import Card from 'primevue/card'
|
import Card from 'primevue/card'
|
||||||
import { createTradingViewDatafeed } from '../composables/useTradingViewDatafeed'
|
import { createTradingViewDatafeed } from '../composables/useTradingViewDatafeed'
|
||||||
|
import { useTradingViewShapes } from '../composables/useTradingViewShapes'
|
||||||
import { useChartStore } from '../stores/chart'
|
import { useChartStore } from '../stores/chart'
|
||||||
import type { IChartingLibraryWidget } from '../types/tradingview'
|
import type { IChartingLibraryWidget } from '../types/tradingview'
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ const chartStore = useChartStore()
|
|||||||
let tvWidget: IChartingLibraryWidget | null = null
|
let tvWidget: IChartingLibraryWidget | null = null
|
||||||
let datafeed: any = null
|
let datafeed: any = null
|
||||||
let isUpdatingFromChart = false // Flag to prevent circular updates
|
let isUpdatingFromChart = false // Flag to prevent circular updates
|
||||||
|
let shapeCleanup: (() => void) | null = null // Cleanup function for shape sync
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!chartContainer.value) return
|
if (!chartContainer.value) return
|
||||||
@@ -49,6 +51,10 @@ onMounted(() => {
|
|||||||
setupStoreWatchers()
|
setupStoreWatchers()
|
||||||
// Initialize visible range on chart load
|
// Initialize visible range on chart load
|
||||||
initializeVisibleRange()
|
initializeVisibleRange()
|
||||||
|
// Setup shape synchronization
|
||||||
|
if (tvWidget) {
|
||||||
|
shapeCleanup = useTradingViewShapes(tvWidget)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize TradingView widget:', error)
|
console.error('Failed to initialize TradingView widget:', error)
|
||||||
@@ -165,6 +171,12 @@ function setupStoreWatchers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
// Cleanup shape synchronization
|
||||||
|
if (shapeCleanup) {
|
||||||
|
shapeCleanup()
|
||||||
|
shapeCleanup = null
|
||||||
|
}
|
||||||
|
|
||||||
if (tvWidget) {
|
if (tvWidget) {
|
||||||
tvWidget.remove()
|
tvWidget.remove()
|
||||||
tvWidget = null
|
tvWidget = null
|
||||||
|
|||||||
@@ -271,9 +271,9 @@ const chatStyles = computed(() => JSON.stringify({
|
|||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
background: '#161e35',
|
background: '#161e35',
|
||||||
backgroundMe: '#00d4aa',
|
backgroundMe: '#1e2d4f',
|
||||||
color: '#cdd6e8',
|
color: '#cdd6e8',
|
||||||
colorMe: '#0a0e1a'
|
colorMe: '#cdd6e8'
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
735
web/src/composables/useTradingViewShapes.ts
Normal file
735
web/src/composables/useTradingViewShapes.ts
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
import { watch } from 'vue'
|
||||||
|
import type { IChartingLibraryWidget } from '../types/tradingview'
|
||||||
|
import { useShapeStore } from '../stores/shapes'
|
||||||
|
import { useChartStore } from '../stores/chart'
|
||||||
|
import type { Shape, ControlPoint } from '../stores/shapes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map TradingView internal tool names (LineToolXXX) to TradingView's native shape type names
|
||||||
|
* Used when converting user-drawn shapes to our Shape format
|
||||||
|
*/
|
||||||
|
const TV_TOOL_TO_SHAPE_TYPE: Record<string, string> = {
|
||||||
|
'LineToolTrendLine': 'trend_line',
|
||||||
|
'LineToolHorzLine': 'horizontal_line',
|
||||||
|
'LineToolVertLine': 'vertical_line',
|
||||||
|
'LineToolRectangle': 'rectangle',
|
||||||
|
'LineToolFibRetracement': 'fib_retracement',
|
||||||
|
'LineToolFibExtension': 'fib_trend_ext',
|
||||||
|
'LineToolParallelChannel': 'parallel_channel',
|
||||||
|
'LineToolArrow': 'arrow',
|
||||||
|
'LineToolText': 'text',
|
||||||
|
'LineToolCallout': 'text',
|
||||||
|
'LineToolCircle': 'circle',
|
||||||
|
'LineToolPath': 'path',
|
||||||
|
'LineToolPitchfork': 'pitchfork',
|
||||||
|
'LineToolGannFan': 'gannbox_fan',
|
||||||
|
'LineToolHeadAndShoulders': 'head_and_shoulders'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map line styles between our format and TradingView format
|
||||||
|
*/
|
||||||
|
const LINE_STYLE_TO_TV: Record<string, number> = {
|
||||||
|
'solid': 0,
|
||||||
|
'dashed': 1,
|
||||||
|
'dotted': 2
|
||||||
|
}
|
||||||
|
|
||||||
|
const TV_TO_LINE_STYLE: Record<number, string> = {
|
||||||
|
0: 'solid',
|
||||||
|
1: 'dashed',
|
||||||
|
2: 'dotted'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert TradingView shape to our Shape format
|
||||||
|
*/
|
||||||
|
function convertTVShapeToShape(tvShape: any, symbol: string, shapeId?: string, shapeName?: string): Shape {
|
||||||
|
// Get shape ID - try multiple ways
|
||||||
|
let id = shapeId || tvShape.id
|
||||||
|
if (!id && typeof tvShape.getId === 'function') {
|
||||||
|
try {
|
||||||
|
id = tvShape.getId()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[TradingView Shapes] Error calling getId:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get shape name/type - use provided name first, then try multiple properties
|
||||||
|
if (!shapeName) {
|
||||||
|
shapeName = tvShape.name
|
||||||
|
}
|
||||||
|
if (!shapeName && typeof tvShape.getName === 'function') {
|
||||||
|
try {
|
||||||
|
shapeName = tvShape.getName()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[TradingView Shapes] Error calling getName:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to checking shape type or tool name
|
||||||
|
if (!shapeName) {
|
||||||
|
shapeName = tvShape.toolName || tvShape.type || 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const standardType = TV_TOOL_TO_SHAPE_TYPE[shapeName] || shapeName
|
||||||
|
|
||||||
|
// Extract control points from TradingView shape
|
||||||
|
const points: ControlPoint[] = []
|
||||||
|
|
||||||
|
// Try multiple ways to get points from the shape
|
||||||
|
let pointsData = tvShape.points || []
|
||||||
|
|
||||||
|
// If getPoints method exists, use it
|
||||||
|
if (typeof tvShape.getPoints === 'function') {
|
||||||
|
try {
|
||||||
|
pointsData = tvShape.getPoints() || []
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[TradingView Shapes] Error calling getPoints:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert points to our format
|
||||||
|
if (Array.isArray(pointsData)) {
|
||||||
|
for (const point of pointsData) {
|
||||||
|
if (point && typeof point === 'object') {
|
||||||
|
const controlPoint: ControlPoint = {
|
||||||
|
time: Math.floor(point.time),
|
||||||
|
price: point.price
|
||||||
|
}
|
||||||
|
// Only include channel if it's actually set
|
||||||
|
if (point.channel !== undefined && point.channel !== null) {
|
||||||
|
controlPoint.channel = point.channel
|
||||||
|
}
|
||||||
|
points.push(controlPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[TradingView Shapes] Converting shape ${id} (${shapeName}), found ${points.length} points`, points)
|
||||||
|
|
||||||
|
// Extract properties
|
||||||
|
let properties = tvShape.properties || {}
|
||||||
|
|
||||||
|
// If getProperties method exists, use it
|
||||||
|
if (typeof tvShape.getProperties === 'function') {
|
||||||
|
try {
|
||||||
|
properties = tvShape.getProperties() || properties
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[TradingView Shapes] Error calling getProperties:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = properties.linecolor || '#2962FF'
|
||||||
|
const lineWidth = properties.linewidth || 1
|
||||||
|
const lineStyle = TV_TO_LINE_STYLE[properties.linestyle] || 'solid'
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: standardType,
|
||||||
|
points,
|
||||||
|
color,
|
||||||
|
line_width: lineWidth,
|
||||||
|
line_style: lineStyle,
|
||||||
|
properties: {
|
||||||
|
...properties,
|
||||||
|
// Store original TradingView shape name for reference
|
||||||
|
tv_shape_name: shapeName
|
||||||
|
},
|
||||||
|
symbol,
|
||||||
|
created_at: now,
|
||||||
|
modified_at: now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert interval string to seconds
|
||||||
|
*/
|
||||||
|
function intervalToSeconds(interval: string): number {
|
||||||
|
// Handle plain numbers (TradingView uses integers for sub-hour intervals in minutes)
|
||||||
|
const numericInterval = parseInt(interval)
|
||||||
|
if (!isNaN(numericInterval) && interval === numericInterval.toString()) {
|
||||||
|
return numericInterval * 60 // Plain number means minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle formatted intervals like "15M", "1H", "1D"
|
||||||
|
const match = interval.match(/^(\d+)([SMHDW])$/)
|
||||||
|
if (!match) {
|
||||||
|
console.warn('[TradingView Shapes] Unknown interval format:', interval)
|
||||||
|
return 60 // Default to 1 minute
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = parseInt(match[1])
|
||||||
|
const unit = match[2]
|
||||||
|
|
||||||
|
const multipliers: Record<string, number> = {
|
||||||
|
'S': 1,
|
||||||
|
'M': 60,
|
||||||
|
'H': 3600,
|
||||||
|
'D': 86400,
|
||||||
|
'W': 604800
|
||||||
|
}
|
||||||
|
|
||||||
|
return value * (multipliers[unit] || 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonicalize timestamp to candle boundary
|
||||||
|
* TradingView requires timestamps to align exactly with candle start times
|
||||||
|
*/
|
||||||
|
function canonicalizeTimestamp(timestamp: number, intervalSeconds: number): number {
|
||||||
|
// Round down to nearest candle boundary
|
||||||
|
return Math.floor(timestamp / intervalSeconds) * intervalSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup bidirectional synchronization between TradingView shapes and ShapeStore
|
||||||
|
*/
|
||||||
|
export function useTradingViewShapes(tvWidget: IChartingLibraryWidget) {
|
||||||
|
const shapeStore = useShapeStore()
|
||||||
|
const chartStore = useChartStore()
|
||||||
|
|
||||||
|
let isApplyingTVUpdate = false // Prevent circular updates when TV modifies shapes
|
||||||
|
let isUpdatingStore = false // Prevent circular updates when we update the store from TV
|
||||||
|
let isChartReady = false // Track if chart API is ready
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drawing events from TradingView
|
||||||
|
*/
|
||||||
|
function handleDrawingEvent(shapeId: string, eventType: string) {
|
||||||
|
if (isApplyingTVUpdate) return
|
||||||
|
|
||||||
|
console.log('[TradingView Shapes] Drawing event:', eventType, shapeId)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chart = tvWidget.activeChart()
|
||||||
|
if (!chart) return
|
||||||
|
|
||||||
|
const currentSymbol = chartStore.chart_state.symbol
|
||||||
|
|
||||||
|
if (eventType === 'remove') {
|
||||||
|
isUpdatingStore = true
|
||||||
|
shapeStore.removeShape(shapeId)
|
||||||
|
isUpdatingStore = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// First get the shape name from getAllShapes (which has id and name)
|
||||||
|
let shapeName: string | undefined
|
||||||
|
const allShapes = chart.getAllShapes()
|
||||||
|
if (allShapes) {
|
||||||
|
const shapeInfo = allShapes.find((s: any) => s.id === shapeId)
|
||||||
|
if (shapeInfo) {
|
||||||
|
shapeName = shapeInfo.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then get the actual shape entity using getShapeById (which has methods)
|
||||||
|
let tvShape
|
||||||
|
try {
|
||||||
|
tvShape = chart.getShapeById(shapeId)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[TradingView Shapes] Could not get shape:', shapeId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tvShape) return
|
||||||
|
|
||||||
|
// Debug: log on first encounter
|
||||||
|
if (eventType === 'create') {
|
||||||
|
console.log('[TradingView Shapes] Shape entity methods:', Object.keys(tvShape))
|
||||||
|
console.log('[TradingView Shapes] Shape name from getAllShapes:', shapeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'create') {
|
||||||
|
// Get points immediately - don't create shape without them
|
||||||
|
const points = tvShape.getPoints()
|
||||||
|
const controlPoints = points && points.length > 0 ? points.map(p => {
|
||||||
|
const cp: ControlPoint = {
|
||||||
|
time: Math.floor(p.time),
|
||||||
|
price: p.price
|
||||||
|
}
|
||||||
|
if (p.channel !== undefined && p.channel !== null) {
|
||||||
|
cp.channel = p.channel
|
||||||
|
}
|
||||||
|
return cp
|
||||||
|
}) : []
|
||||||
|
|
||||||
|
// Create shape with complete data
|
||||||
|
const shape = convertTVShapeToShape(tvShape, currentSymbol, shapeId, shapeName)
|
||||||
|
// Override points with the ones we just fetched
|
||||||
|
shape.points = controlPoints
|
||||||
|
|
||||||
|
console.log('[TradingView Shapes] Creating shape:', shapeId, shape.type, 'with', shape.points.length, 'points')
|
||||||
|
isUpdatingStore = true
|
||||||
|
shapeStore.addShape(shape)
|
||||||
|
isUpdatingStore = false
|
||||||
|
} else if (eventType === 'points_changed') {
|
||||||
|
// Points updated - get latest points and create NEW array
|
||||||
|
const points = tvShape.getPoints()
|
||||||
|
if (points) {
|
||||||
|
// Create a completely new array to ensure it's treated as a replacement
|
||||||
|
const controlPoints = Array.from(points.map(p => {
|
||||||
|
const cp: ControlPoint = {
|
||||||
|
time: Math.floor(p.time),
|
||||||
|
price: p.price
|
||||||
|
}
|
||||||
|
if (p.channel !== undefined && p.channel !== null) {
|
||||||
|
cp.channel = p.channel
|
||||||
|
}
|
||||||
|
return cp
|
||||||
|
}))
|
||||||
|
console.log('[TradingView Shapes] Points changed:', shapeId, 'now has', controlPoints.length, 'points')
|
||||||
|
isUpdatingStore = true
|
||||||
|
shapeStore.updateShape(shapeId, { points: controlPoints })
|
||||||
|
isUpdatingStore = false
|
||||||
|
}
|
||||||
|
} else if (eventType === 'properties_changed') {
|
||||||
|
// Properties updated - get latest properties
|
||||||
|
const properties = tvShape.getProperties()
|
||||||
|
if (properties) {
|
||||||
|
const color = properties.linecolor || '#2962FF'
|
||||||
|
const lineWidth = properties.linewidth || 1
|
||||||
|
const lineStyle = TV_TO_LINE_STYLE[properties.linestyle] || 'solid'
|
||||||
|
console.log('[TradingView Shapes] Properties changed:', shapeId)
|
||||||
|
isUpdatingStore = true
|
||||||
|
shapeStore.updateShape(shapeId, {
|
||||||
|
color,
|
||||||
|
line_width: lineWidth,
|
||||||
|
line_style: lineStyle,
|
||||||
|
properties
|
||||||
|
})
|
||||||
|
isUpdatingStore = false
|
||||||
|
}
|
||||||
|
} else if (eventType === 'move') {
|
||||||
|
// Shape moved - update points with NEW array
|
||||||
|
const points = tvShape.getPoints()
|
||||||
|
if (points) {
|
||||||
|
const controlPoints = Array.from(points.map(p => {
|
||||||
|
const cp: ControlPoint = {
|
||||||
|
time: Math.floor(p.time),
|
||||||
|
price: p.price
|
||||||
|
}
|
||||||
|
if (p.channel !== undefined && p.channel !== null) {
|
||||||
|
cp.channel = p.channel
|
||||||
|
}
|
||||||
|
return cp
|
||||||
|
}))
|
||||||
|
console.log('[TradingView Shapes] Shape moved:', shapeId)
|
||||||
|
isUpdatingStore = true
|
||||||
|
shapeStore.updateShape(shapeId, { points: controlPoints })
|
||||||
|
isUpdatingStore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TradingView Shapes] Error handling drawing event:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup listeners for TradingView shape events
|
||||||
|
*/
|
||||||
|
function setupShapeListeners() {
|
||||||
|
try {
|
||||||
|
const chart = tvWidget.activeChart()
|
||||||
|
if (!chart) {
|
||||||
|
console.warn('[TradingView Shapes] Active chart not available yet')
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
isChartReady = true
|
||||||
|
|
||||||
|
console.log('[TradingView Shapes] Setting up drawing event subscriptions')
|
||||||
|
|
||||||
|
// Subscribe to the drawing event which handles all shape events
|
||||||
|
const unsubscribe = tvWidget.subscribe('drawing_event', (shapeId: string, eventType: string) => {
|
||||||
|
handleDrawingEvent(shapeId, eventType)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initial sync on data load
|
||||||
|
const dataLoadedSubscription = chart.onDataLoaded().subscribe(null, () => {
|
||||||
|
console.log('[TradingView Shapes] Chart data loaded, performing initial sync')
|
||||||
|
syncShapesFromTV()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cleanup function to unsubscribe
|
||||||
|
return () => {
|
||||||
|
console.log('[TradingView Shapes] Cleaning up event subscriptions')
|
||||||
|
try {
|
||||||
|
if (typeof unsubscribe === 'function') {
|
||||||
|
unsubscribe()
|
||||||
|
} else {
|
||||||
|
console.warn('[TradingView Shapes] unsubscribe is not a function:', typeof unsubscribe)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLoadedSubscription && typeof dataLoadedSubscription.unsubscribe === 'function') {
|
||||||
|
dataLoadedSubscription.unsubscribe()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TradingView Shapes] Error during cleanup:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TradingView Shapes] Error setting up shape listeners:', error)
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan TradingView for shapes and sync to store
|
||||||
|
*/
|
||||||
|
function syncShapesFromTV() {
|
||||||
|
if (!isChartReady) {
|
||||||
|
return // Skip if chart isn't ready yet
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chart = tvWidget.activeChart()
|
||||||
|
if (!chart) {
|
||||||
|
return // Chart not available
|
||||||
|
}
|
||||||
|
|
||||||
|
const allShapes = chart.getAllShapes()
|
||||||
|
|
||||||
|
if (!allShapes) return
|
||||||
|
|
||||||
|
const currentSymbol = chartStore.chart_state.symbol
|
||||||
|
|
||||||
|
// Track which shape IDs we've seen
|
||||||
|
const seenIds = new Set<string>()
|
||||||
|
|
||||||
|
isUpdatingStore = true
|
||||||
|
try {
|
||||||
|
for (const tvShape of allShapes) {
|
||||||
|
seenIds.add(tvShape.id)
|
||||||
|
|
||||||
|
const existingShape = shapeStore.getShape(tvShape.id)
|
||||||
|
const newShape = convertTVShapeToShape(tvShape, currentSymbol)
|
||||||
|
|
||||||
|
if (!existingShape) {
|
||||||
|
// New shape - add it
|
||||||
|
console.log('[TradingView Shapes] New shape detected:', tvShape.id, tvShape.name, 'with', newShape.points.length, 'points')
|
||||||
|
shapeStore.addShape(newShape)
|
||||||
|
} else {
|
||||||
|
// Check if shape has changed
|
||||||
|
const pointsChanged = JSON.stringify(existingShape.points) !== JSON.stringify(newShape.points)
|
||||||
|
const hasChanged =
|
||||||
|
pointsChanged ||
|
||||||
|
existingShape.color !== newShape.color ||
|
||||||
|
existingShape.line_width !== newShape.line_width ||
|
||||||
|
existingShape.line_style !== newShape.line_style
|
||||||
|
|
||||||
|
if (hasChanged) {
|
||||||
|
if (pointsChanged) {
|
||||||
|
console.log('[TradingView Shapes] Shape points changed:', tvShape.id,
|
||||||
|
'from', existingShape.points.length, 'to', newShape.points.length, 'points')
|
||||||
|
} else {
|
||||||
|
console.log('[TradingView Shapes] Shape properties modified:', tvShape.id)
|
||||||
|
}
|
||||||
|
shapeStore.updateShape(tvShape.id, {
|
||||||
|
points: newShape.points,
|
||||||
|
color: newShape.color,
|
||||||
|
line_width: newShape.line_width,
|
||||||
|
line_style: newShape.line_style,
|
||||||
|
properties: newShape.properties
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for deleted shapes
|
||||||
|
const allStoreShapes = shapeStore.getAllShapes()
|
||||||
|
for (const storeShape of allStoreShapes) {
|
||||||
|
if (storeShape.symbol === currentSymbol && !seenIds.has(storeShape.id)) {
|
||||||
|
console.log('[TradingView Shapes] Shape deleted:', storeShape.id)
|
||||||
|
shapeStore.removeShape(storeShape.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isUpdatingStore = false
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TradingView Shapes] Error syncing shapes from TV:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup watchers for ShapeStore changes to apply to TradingView
|
||||||
|
*/
|
||||||
|
function setupStoreWatchers() {
|
||||||
|
// Watch for shape store changes and apply to TradingView
|
||||||
|
watch(
|
||||||
|
() => shapeStore.shapes,
|
||||||
|
async (newShapes, oldShapes) => {
|
||||||
|
if (isUpdatingStore || !isChartReady) return
|
||||||
|
|
||||||
|
isApplyingTVUpdate = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chart = tvWidget.activeChart()
|
||||||
|
if (!chart) return
|
||||||
|
|
||||||
|
const currentSymbol = chartStore.chart_state.symbol
|
||||||
|
|
||||||
|
// Find added shapes
|
||||||
|
for (const [id, shape] of Object.entries(newShapes)) {
|
||||||
|
// Only apply shapes for current symbol (or shapes with null symbol)
|
||||||
|
if (shape.symbol && shape.symbol !== currentSymbol) continue
|
||||||
|
|
||||||
|
if (!oldShapes || !oldShapes[id]) {
|
||||||
|
// Shape was added to store
|
||||||
|
await createTVShape(shape)
|
||||||
|
} else {
|
||||||
|
// Check if shape exists in TradingView
|
||||||
|
let tvShape = null
|
||||||
|
try {
|
||||||
|
tvShape = chart.getShapeById(id)
|
||||||
|
} catch (e) {
|
||||||
|
// Shape doesn't exist in TV
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tvShape) {
|
||||||
|
// Shape is in store but not in TradingView - create it
|
||||||
|
await createTVShape(shape)
|
||||||
|
} else if (JSON.stringify(shape) !== JSON.stringify(oldShapes[id])) {
|
||||||
|
// Shape was modified
|
||||||
|
updateTVShape(shape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find removed shapes
|
||||||
|
if (oldShapes) {
|
||||||
|
for (const [id, shape] of Object.entries(oldShapes)) {
|
||||||
|
if (shape.symbol && shape.symbol !== currentSymbol) continue
|
||||||
|
|
||||||
|
if (!newShapes[id]) {
|
||||||
|
removeTVShape(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TradingView Shapes] Error applying store changes:', error)
|
||||||
|
} finally {
|
||||||
|
isApplyingTVUpdate = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a shape in TradingView
|
||||||
|
*/
|
||||||
|
async function createTVShape(shape: Shape) {
|
||||||
|
if (!isChartReady) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chart = tvWidget.activeChart()
|
||||||
|
if (!chart) return
|
||||||
|
|
||||||
|
const currentSymbol = chartStore.chart_state.symbol
|
||||||
|
|
||||||
|
// Get current chart interval and convert to seconds for timestamp canonicalization
|
||||||
|
const interval = chartStore.chart_state.interval
|
||||||
|
const intervalSeconds = intervalToSeconds(interval)
|
||||||
|
|
||||||
|
// Convert points to TradingView format and canonicalize timestamps to candle boundaries
|
||||||
|
const tvPoints = shape.points.map(p => {
|
||||||
|
const canonicalTime = canonicalizeTimestamp(p.time, intervalSeconds)
|
||||||
|
const point: any = {
|
||||||
|
time: canonicalTime,
|
||||||
|
price: p.price
|
||||||
|
}
|
||||||
|
// Only include channel if it has a value (for parallel channels, etc.)
|
||||||
|
if (p.channel != null) {
|
||||||
|
point.channel = p.channel
|
||||||
|
}
|
||||||
|
return point
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build overrides - merge color/width/style with properties
|
||||||
|
const overrides: any = {
|
||||||
|
...shape.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape.color) {
|
||||||
|
overrides.linecolor = shape.color
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape.line_width) {
|
||||||
|
overrides.linewidth = shape.line_width
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape.line_style) {
|
||||||
|
overrides.linestyle = LINE_STYLE_TO_TV[shape.line_style] || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
shape: shape.type,
|
||||||
|
zOrder: 'top',
|
||||||
|
lock: false,
|
||||||
|
disableSelection: false,
|
||||||
|
disableSave: false,
|
||||||
|
disableUndo: false,
|
||||||
|
overrides
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[TradingView Shapes] Creating shape type="${shape.type}" with ${tvPoints.length} points:`, tvPoints)
|
||||||
|
|
||||||
|
// Create the shape - use createShape for single points, createMultipointShape for multiple
|
||||||
|
let entityIdPromise
|
||||||
|
if (tvPoints.length === 1) {
|
||||||
|
console.log(`[TradingView Shapes] Using createShape() for single point`)
|
||||||
|
entityIdPromise = chart.createShape(tvPoints[0], options)
|
||||||
|
} else {
|
||||||
|
console.log(`[TradingView Shapes] Using createMultipointShape() for ${tvPoints.length} points`)
|
||||||
|
entityIdPromise = chart.createMultipointShape(tvPoints, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityId = await entityIdPromise
|
||||||
|
|
||||||
|
// If TradingView assigned a different ID, update the store
|
||||||
|
if (entityId && entityId !== shape.id) {
|
||||||
|
isUpdatingStore = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
|
||||||
|
try {
|
||||||
|
shapeStore.removeShape(shape.id)
|
||||||
|
|
||||||
|
const updatedShape: Shape = {
|
||||||
|
...shape,
|
||||||
|
id: entityId,
|
||||||
|
original_id: shape.original_id || shape.id,
|
||||||
|
symbol: shape.symbol || currentSymbol
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeStore.addShape(updatedShape)
|
||||||
|
} finally {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
isUpdatingStore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bring shape to front to ensure visibility
|
||||||
|
try {
|
||||||
|
const createdShape = chart.getShapeById(entityId)
|
||||||
|
if (createdShape && typeof createdShape.bringToFront === 'function') {
|
||||||
|
createdShape.bringToFront()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TradingView Shapes] Error creating shape:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a shape in TradingView
|
||||||
|
*/
|
||||||
|
function updateTVShape(shape: Shape) {
|
||||||
|
if (!isChartReady) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chart = tvWidget.activeChart()
|
||||||
|
if (!chart) return
|
||||||
|
|
||||||
|
const tvShape = chart.getShapeById(shape.id)
|
||||||
|
|
||||||
|
if (!tvShape) {
|
||||||
|
console.warn('[TradingView Shapes] Shape not found in TV, creating instead:', shape.id)
|
||||||
|
createTVShape(shape)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update points
|
||||||
|
const tvPoints = shape.points.map(p => {
|
||||||
|
const point: { time: number; price: number; channel?: string } = {
|
||||||
|
time: p.time,
|
||||||
|
price: p.price
|
||||||
|
}
|
||||||
|
if (p.channel) {
|
||||||
|
point.channel = p.channel
|
||||||
|
}
|
||||||
|
return point
|
||||||
|
})
|
||||||
|
tvShape.setPoints(tvPoints)
|
||||||
|
|
||||||
|
// Update properties
|
||||||
|
tvShape.setProperties({
|
||||||
|
linecolor: shape.color || '#2962FF',
|
||||||
|
linewidth: shape.line_width || 1,
|
||||||
|
linestyle: LINE_STYLE_TO_TV[shape.line_style || 'solid'] || 0,
|
||||||
|
...shape.properties
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('[TradingView Shapes] Updated shape in TV:', shape.id)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TradingView Shapes] Error updating shape in TV:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a shape from TradingView
|
||||||
|
*/
|
||||||
|
function removeTVShape(shapeId: string) {
|
||||||
|
if (!isChartReady) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chart = tvWidget.activeChart()
|
||||||
|
if (!chart) return
|
||||||
|
|
||||||
|
const tvShape = chart.getShapeById(shapeId)
|
||||||
|
|
||||||
|
if (tvShape) {
|
||||||
|
chart.removeEntity(tvShape)
|
||||||
|
console.log('[TradingView Shapes] Removed shape from TV:', shapeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TradingView Shapes] Error removing shape from TV:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup selection tracking
|
||||||
|
*/
|
||||||
|
function setupSelectionTracking() {
|
||||||
|
if (!isChartReady) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chart = tvWidget.activeChart()
|
||||||
|
if (!chart) return
|
||||||
|
|
||||||
|
chart.onSelectionChanged().subscribe(null, () => {
|
||||||
|
const selectedObjects = chart.selection()
|
||||||
|
|
||||||
|
// Filter for shape/drawing objects only
|
||||||
|
const selectedShapeIds = selectedObjects
|
||||||
|
.filter((obj: any) => obj.type === 'study' || obj.name?.startsWith('LineTool'))
|
||||||
|
.map((obj: any) => obj.id)
|
||||||
|
|
||||||
|
console.log('[TradingView Shapes] Selection changed:', selectedShapeIds)
|
||||||
|
chartStore.chart_state.selected_shapes = selectedShapeIds
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[TradingView Shapes] Selection tracking not available:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
const cleanupShapeListeners = setupShapeListeners()
|
||||||
|
setupStoreWatchers()
|
||||||
|
setupSelectionTracking()
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
cleanupShapeListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ export interface ChartState {
|
|||||||
start_time: number | null
|
start_time: number | null
|
||||||
end_time: number | null
|
end_time: number | null
|
||||||
interval: string
|
interval: string
|
||||||
|
selected_shapes: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useChartStore = defineStore('ChartStore', () => {
|
export const useChartStore = defineStore('ChartStore', () => {
|
||||||
@@ -13,7 +14,8 @@ export const useChartStore = defineStore('ChartStore', () => {
|
|||||||
symbol: 'BINANCE:BTC/USDT',
|
symbol: 'BINANCE:BTC/USDT',
|
||||||
start_time: null,
|
start_time: null,
|
||||||
end_time: null,
|
end_time: null,
|
||||||
interval: '15'
|
interval: '15',
|
||||||
|
selected_shapes: []
|
||||||
})
|
})
|
||||||
|
|
||||||
return { chart_state }
|
return { chart_state }
|
||||||
|
|||||||
64
web/src/stores/shapes.ts
Normal file
64
web/src/stores/shapes.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export interface ControlPoint {
|
||||||
|
time: number
|
||||||
|
price: number
|
||||||
|
channel?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shape {
|
||||||
|
id: string
|
||||||
|
type: string
|
||||||
|
points: ControlPoint[]
|
||||||
|
color?: string
|
||||||
|
line_width?: number
|
||||||
|
line_style?: string
|
||||||
|
properties?: Record<string, any>
|
||||||
|
symbol?: string
|
||||||
|
created_at?: number
|
||||||
|
modified_at?: number
|
||||||
|
original_id?: string // Original ID from backend/agent before TradingView assigns its own ID
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useShapeStore = defineStore('ShapeStore', () => {
|
||||||
|
const shapes = ref<Record<string, Shape>>({})
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
const addShape = (shape: Shape) => {
|
||||||
|
shapes.value[shape.id] = shape
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateShape = (id: string, updates: Partial<Shape>) => {
|
||||||
|
if (shapes.value[id]) {
|
||||||
|
const updated = {
|
||||||
|
...shapes.value[id],
|
||||||
|
...updates,
|
||||||
|
modified_at: Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
// Replace the entire shape object to ensure arrays are replaced atomically
|
||||||
|
shapes.value[id] = updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeShape = (id: string) => {
|
||||||
|
delete shapes.value[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getShape = (id: string): Shape | undefined => {
|
||||||
|
return shapes.value[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllShapes = (): Shape[] => {
|
||||||
|
return Object.values(shapes.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shapes,
|
||||||
|
addShape,
|
||||||
|
updateShape,
|
||||||
|
removeShape,
|
||||||
|
getShape,
|
||||||
|
getAllShapes
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user