feat: Pine Script v6 export for TradingView integration
Add full-stack Pine Script export: backtest strategies can now be
converted to TradingView Pine Script v6 via natural language, CLI,
API, or the web UI.
- New `pine-script` skill with Python-to-Pine mapping table and v6 syntax guide
- CLI: `/pine <run_id>` slash command and `--pine` flag
- API: `GET /runs/{run_id}/pine` endpoint
- Frontend: Pine Script button on RunCompleteCard + modal code viewer with copy
- README: updated news, capabilities, skill count (65), CLI/API docs
This commit is contained in:
1 parent
d457cf4a5b
commit
211ddb80b7
7 files changed
+434
-17
No files matched your search
@@ -15,7 +15,7 @@
|
||||
<a href="https://pypi.org/project/vibe-trading-ai/"><img src="https://img.shields.io/pypi/v/vibe-trading-ai?style=flat&logo=pypi&logoColor=white" alt="PyPI"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow?style=flat" alt="License"></a>
|
||||
<br>
|
||||
<img src="https://img.shields.io/badge/Skills-64-orange" alt="Skills">
|
||||
<img src="https://img.shields.io/badge/Skills-65-orange" alt="Skills">
|
||||
<img src="https://img.shields.io/badge/Swarm_Presets-29-7C3AED" alt="Swarm">
|
||||
<img src="https://img.shields.io/badge/Tools-20-0F766E" alt="Tools">
|
||||
<img src="https://img.shields.io/badge/Markets-3-2563EB" alt="Markets">
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
## 📰 News
|
||||
|
||||
- **2026-04-09** 🔧 Multi-market backtest engines — per-market rules (A-share T+1, crypto funding/liquidation, US/HK fees) replace the single vectorised engine.
|
||||
- **2026-04-09** 🔧 Multi-market backtest engines with per-market rules; **Pine Script v6 export** — convert strategies to TradingView in one command (`/pine`, API, frontend viewer).
|
||||
- **2026-04-01** 🚀 Released **v0.1.0** — Initial release: ReAct agent, 64 skills, 29 swarm presets, cross-market backtest, CLI + Web UI + MCP server.
|
||||
|
||||
---
|
||||
@@ -57,6 +57,7 @@ Vibe-Trading is an AI-powered multi-agent finance workspace that turns natural l
|
||||
• **Strategy Generation** — Automatically writes trading code from your ideas<br>
|
||||
• **Smart Data Access** — Pulls market data from multiple sources seamlessly<br>
|
||||
• **Performance Testing** — Tests your strategies against historical market data<br>
|
||||
• **TradingView Export** — One-click convert strategies to Pine Script v6 for TradingView<br>
|
||||
• **Expert Teams** — Deploys specialized AI agents for complex research tasks<br>
|
||||
• **Live Updates** — Watch the entire analysis process in real-time
|
||||
|
||||
@@ -69,7 +70,7 @@ Vibe-Trading is an AI-powered multi-agent finance workspace that turns natural l
|
||||
<td align="center" width="25%" valign="top">
|
||||
<img src="assets/scene-research.png" height="150" alt="Research"/><br>
|
||||
<h3>🔍 DeepResearch for Trading</h3>
|
||||
<img src="https://img.shields.io/badge/64_Skills-FF6B6B?style=for-the-badge&logo=bookstack&logoColor=white" alt="Skills" /><br><br>
|
||||
<img src="https://img.shields.io/badge/65_Skills-FF6B6B?style=for-the-badge&logo=bookstack&logoColor=white" alt="Skills" /><br><br>
|
||||
<div align="left" style="font-size: 4px;">
|
||||
• Multi-domain analysis coverage across markets<br>
|
||||
• Auto strategy and signal generation<br>
|
||||
@@ -115,14 +116,14 @@ Vibe-Trading is an AI-powered multi-agent finance workspace that turns natural l
|
||||
|
||||
## 64 Skills across 8 Core Finance Domains
|
||||
|
||||
- 📊 64 specialized finance skills across 8 core domains
|
||||
- 📊 65 specialized finance skills across 8 core domains
|
||||
- 🌐 Complete coverage from traditional markets to crypto & DeFi
|
||||
- 🔬 Comprehensive capabilities spanning technical analysis to quantitative research
|
||||
|
||||
| Area | Examples |
|
||||
|------|----------|
|
||||
| Technical Analysis | `technical-basic`, `candlestick`, `ichimoku`, `harmonic`, `elliott-wave`, `smc` |
|
||||
| Quant Research | `strategy-generate`, `multi-factor`, `pair-trading`, `factor-research`, `ml-strategy` |
|
||||
| Quant Research | `strategy-generate`, `pine-script`, `multi-factor`, `pair-trading`, `factor-research`, `ml-strategy` |
|
||||
| Fundamentals | `valuation-model`, `financial-statement`, `earnings-forecast`, `credit-analysis` |
|
||||
| HK/US Equities | `edgar-sec-filings`, `earnings-revision`, `us-etf-flow`, `hk-connect-flow`, `adr-hshare` |
|
||||
| Crypto Desk | `perp-funding-basis`, `liquidation-heatmap`, `stablecoin-flow`, `token-unlock-treasury`, `defi-yield` |
|
||||
@@ -311,7 +312,7 @@ vibe-trading serve # API server
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/help` | Show all commands |
|
||||
| `/skills` | List all 64 finance skills |
|
||||
| `/skills` | List all 65 finance skills |
|
||||
| `/swarm` | List 29 swarm team presets |
|
||||
| `/swarm run <preset> [vars_json]` | Run a swarm team with live streaming |
|
||||
| `/swarm list` | Swarm run history |
|
||||
@@ -320,6 +321,7 @@ vibe-trading serve # API server
|
||||
| `/list` | Recent runs |
|
||||
| `/show <run_id>` | Run details + metrics |
|
||||
| `/code <run_id>` | Generated strategy code |
|
||||
| `/pine <run_id>` | Pine Script for TradingView |
|
||||
| `/trace <run_id>` | Full execution replay |
|
||||
| `/continue <run_id> <prompt>` | Continue a run with new instructions |
|
||||
| `/sessions` | List chat sessions |
|
||||
@@ -347,6 +349,7 @@ vibe-trading --swarm-run investment_committee '{"topic":"BTC outlook"}'
|
||||
vibe-trading --list
|
||||
vibe-trading --show <run_id>
|
||||
vibe-trading --code <run_id>
|
||||
vibe-trading --pine <run_id> # Pine Script for TradingView
|
||||
vibe-trading --trace <run_id>
|
||||
vibe-trading --continue <run_id> "refine the strategy"
|
||||
vibe-trading --upload report.pdf
|
||||
@@ -366,6 +369,7 @@ vibe-trading serve --port 8899
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/runs` | List runs |
|
||||
| `GET` | `/runs/{run_id}` | Run details |
|
||||
| `GET` | `/runs/{run_id}/pine` | Pine Script export |
|
||||
| `POST` | `/sessions` | Create session |
|
||||
| `POST` | `/sessions/{id}/messages` | Send message |
|
||||
| `GET` | `/sessions/{id}/events` | SSE event stream |
|
||||
@@ -442,7 +446,7 @@ Browse on ClawHub: [clawhub.ai/skills/vibe-trading](https://clawhub.ai/skills/vi
|
||||
<details>
|
||||
<summary><b>OpenSpace — self-evolving skills</b></summary>
|
||||
|
||||
All 64 finance skills are published on [open-space.cloud](https://open-space.cloud) and evolve autonomously through OpenSpace's self-evolution engine.
|
||||
All 65 finance skills are published on [open-space.cloud](https://open-space.cloud) and evolve autonomously through OpenSpace's self-evolution engine.
|
||||
|
||||
To use with OpenSpace, add both MCP servers to your agent config:
|
||||
|
||||
|
||||
@@ -395,6 +395,25 @@ async def get_run_code(run_id: str):
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/runs/{run_id}/pine")
|
||||
async def get_run_pine(run_id: str):
|
||||
"""Return Pine Script file for a run.
|
||||
|
||||
Args:
|
||||
run_id: Run identifier.
|
||||
|
||||
Returns:
|
||||
Object with pine script content and exists flag.
|
||||
"""
|
||||
pine_path = RUNS_DIR / run_id / "artifacts" / "strategy.pine"
|
||||
if not pine_path.exists():
|
||||
return {"exists": False, "content": None}
|
||||
return {
|
||||
"exists": True,
|
||||
"content": pine_path.read_text(encoding="utf-8"),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/runs/{run_id}", response_model=RunResponse)
|
||||
async def get_run_result(run_id: str):
|
||||
"""Fetch full details for a historical run by ``run_id``."""
|
||||
|
||||
+23
-1
@@ -358,7 +358,7 @@ def cmd_run(prompt: str, max_iter: int, *, json_mode: bool = False, no_rich: boo
|
||||
return _result_exit_code(result)
|
||||
_print_result(result, time.perf_counter() - start, no_rich=no_rich)
|
||||
if result.get("run_id"):
|
||||
tip = f"--show {result['run_id']} | --continue {result['run_id']} \"...\" | --code {result['run_id']}"
|
||||
tip = f"--show {result['run_id']} | --continue {result['run_id']} \"...\" | --code {result['run_id']} | --pine {result['run_id']}"
|
||||
if no_rich:
|
||||
print(tip)
|
||||
else:
|
||||
@@ -466,6 +466,7 @@ def _print_help() -> None:
|
||||
("/list", "List recent runs"),
|
||||
("/show <run_id>", "Show run details"),
|
||||
("/code <run_id>", "Show generated code"),
|
||||
("/pine <run_id>", "Show Pine Script for TradingView"),
|
||||
("/trace <run_id>", "Replay run trace"),
|
||||
("/continue <run_id> <prompt>", "Continue an existing run"),
|
||||
("/swarm", "List swarm team presets"),
|
||||
@@ -525,6 +526,11 @@ def _handle_slash_command(input_str: str, *, max_iter: int) -> None:
|
||||
cmd_code(arg)
|
||||
else:
|
||||
console.print("[red]Usage: /code <run_id>[/red]")
|
||||
elif cmd == "/pine":
|
||||
if arg:
|
||||
cmd_pine(arg)
|
||||
else:
|
||||
console.print("[red]Usage: /pine <run_id>[/red]")
|
||||
elif cmd == "/trace":
|
||||
if arg:
|
||||
cmd_trace(arg)
|
||||
@@ -980,6 +986,19 @@ def cmd_code(run_id: str) -> None:
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_pine(run_id: str) -> None:
|
||||
"""Show Pine Script for a run."""
|
||||
pine_path = RUNS_DIR / run_id / "artifacts" / "strategy.pine"
|
||||
if not pine_path.exists():
|
||||
console.print(f"[red]{run_id}/artifacts/strategy.pine not found[/red]")
|
||||
console.print("[dim]Ask the agent: \"export this strategy to Pine Script\"[/dim]")
|
||||
return
|
||||
code = pine_path.read_text(encoding="utf-8")
|
||||
console.print(Syntax(code, "javascript", theme="monokai", line_numbers=True), width=120)
|
||||
console.print()
|
||||
console.print("[dim]Copy and paste into TradingView Pine Editor → Add to Chart[/dim]")
|
||||
|
||||
|
||||
def cmd_skills() -> None:
|
||||
"""List available skills."""
|
||||
from src.agent.skills import SkillsLoader
|
||||
@@ -1316,6 +1335,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--list", action="store_true", help="List runs")
|
||||
parser.add_argument("--show", metavar="RUN_ID", help="Show run details")
|
||||
parser.add_argument("--code", metavar="RUN_ID", help="Show generated code")
|
||||
parser.add_argument("--pine", metavar="RUN_ID", help="Show Pine Script for TradingView")
|
||||
parser.add_argument("--trace", metavar="RUN_ID", help="Replay a run trace")
|
||||
parser.add_argument("--skills", action="store_true", help="List skills")
|
||||
parser.add_argument("--max-iter", type=int, default=50, help="Maximum agent iterations")
|
||||
@@ -1474,6 +1494,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return _coerce_exit_code(cmd_show(args.show))
|
||||
if args.code:
|
||||
return _coerce_exit_code(cmd_code(args.code))
|
||||
if args.pine:
|
||||
return _coerce_exit_code(cmd_pine(args.pine))
|
||||
if args.trace:
|
||||
return _coerce_exit_code(cmd_trace(args.trace))
|
||||
if args.skills:
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
---
|
||||
name: pine-script
|
||||
description: Convert Python backtest strategies to TradingView Pine Script v6, or generate Pine Script from natural language descriptions.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This skill enables two workflows:
|
||||
1. **Export**: convert an existing `signal_engine.py` (from a completed backtest run) into a TradingView Pine Script v6 strategy
|
||||
2. **Generate**: write Pine Script v6 directly from a natural-language strategy description
|
||||
|
||||
Output file: `artifacts/strategy.pine` (inside the run directory, or a new run directory).
|
||||
|
||||
## Workflow: Export from Backtest
|
||||
|
||||
1. `load_skill("pine-script")` — read this conversion guide
|
||||
2. `read_file("config.json")` — understand instruments, dates, parameters
|
||||
3. `read_file("code/signal_engine.py")` — understand the Python strategy logic
|
||||
4. **Translate** the strategy to Pine Script v6 using the mapping table below
|
||||
5. `write_file("artifacts/strategy.pine")` — save the output
|
||||
6. Return the Pine Script in a code block with usage instructions
|
||||
|
||||
## Workflow: Generate from Description
|
||||
|
||||
1. `load_skill("pine-script")` — read this guide
|
||||
2. Write Pine Script v6 directly based on the user's description
|
||||
3. `write_file("artifacts/strategy.pine")` — save the output
|
||||
4. Return the code with usage instructions
|
||||
|
||||
## Pine Script v6 Strategy Template
|
||||
|
||||
```pinescript
|
||||
// This strategy was generated by Vibe-Trading
|
||||
// Paste into TradingView Pine Editor → Add to Chart
|
||||
//@version=6
|
||||
strategy("Strategy Name", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1, initial_capital=1000000)
|
||||
|
||||
// ============================================================================
|
||||
// INPUTS
|
||||
// ============================================================================
|
||||
// [Group inputs logically with input.int(), input.float(), input.string()]
|
||||
|
||||
// ============================================================================
|
||||
// CALCULATIONS
|
||||
// ============================================================================
|
||||
// [Core indicator calculations]
|
||||
|
||||
// ============================================================================
|
||||
// CONDITIONS
|
||||
// ============================================================================
|
||||
longCondition = false
|
||||
shortCondition = false
|
||||
exitLongCondition = false
|
||||
exitShortCondition = false
|
||||
|
||||
// ============================================================================
|
||||
// STRATEGY EXECUTION
|
||||
// ============================================================================
|
||||
if longCondition
|
||||
strategy.entry("Long", strategy.long)
|
||||
if shortCondition
|
||||
strategy.entry("Short", strategy.short)
|
||||
if exitLongCondition
|
||||
strategy.close("Long")
|
||||
if exitShortCondition
|
||||
strategy.close("Short")
|
||||
|
||||
// ============================================================================
|
||||
// PLOTS
|
||||
// ============================================================================
|
||||
// [Visual overlays: moving averages, bands, signals]
|
||||
|
||||
// ============================================================================
|
||||
// ALERTS
|
||||
// ============================================================================
|
||||
alertcondition(longCondition, title="Long Signal", message="Long entry signal triggered")
|
||||
alertcondition(shortCondition, title="Short Signal", message="Short entry signal triggered")
|
||||
```
|
||||
|
||||
## Python → Pine Script Mapping Table
|
||||
|
||||
### Technical Indicators
|
||||
|
||||
| Python (pandas/numpy) | Pine Script v6 |
|
||||
|------------------------|----------------|
|
||||
| `df['close'].rolling(n).mean()` | `ta.sma(close, n)` |
|
||||
| `df['close'].ewm(span=n).mean()` | `ta.ema(close, n)` |
|
||||
| `ta.RSI(df['close'], n)` or manual RSI | `ta.rsi(close, n)` |
|
||||
| `ta.MACD(df['close'])` | `[macdLine, signalLine, hist] = ta.macd(close, 12, 26, 9)` |
|
||||
| `df['close'].rolling(n).std()` | `ta.stdev(close, n)` |
|
||||
| `df['high'].rolling(n).max()` | `ta.highest(high, n)` |
|
||||
| `df['low'].rolling(n).min()` | `ta.lowest(low, n)` |
|
||||
| `df['close'].pct_change()` | `(close - close[1]) / close[1]` |
|
||||
| `df['volume'].rolling(n).mean()` | `ta.sma(volume, n)` |
|
||||
| `df['close'] > df['close'].shift(1)` | `close > close[1]` |
|
||||
| Bollinger Bands (manual) | `[mid, upper, lower] = ta.bb(close, length, mult)` |
|
||||
| ATR (manual or ta-lib) | `ta.atr(length)` |
|
||||
| ADX | `ta.adx(high, low, close, length)` (returns adx value) |
|
||||
| Stochastic | `ta.stoch(close, high, low, length, smoothK, smoothD)` |
|
||||
| CCI | `ta.cci(close, length)` (Pine v6: `ta.cci(close, length)`) |
|
||||
| Williams %R | `ta.wpr(length)` |
|
||||
| MFI | `ta.mfi(hlc3, volume, length)` — Pine v6: `ta.mfi(close, length)` |
|
||||
| OBV | `ta.obv` |
|
||||
| VWAP | `ta.vwap` |
|
||||
|
||||
### Data References
|
||||
|
||||
| Python | Pine Script v6 |
|
||||
|--------|----------------|
|
||||
| `df['open']` | `open` |
|
||||
| `df['high']` | `high` |
|
||||
| `df['low']` | `low` |
|
||||
| `df['close']` | `close` |
|
||||
| `df['volume']` | `volume` |
|
||||
| `df.index` (datetime) | `time` |
|
||||
| `df['close'].shift(n)` | `close[n]` |
|
||||
|
||||
### Signal Logic
|
||||
|
||||
| Python Pattern | Pine Script v6 |
|
||||
|---------------|----------------|
|
||||
| `(fast > slow) & (fast.shift(1) <= slow.shift(1))` | `ta.crossover(fast, slow)` |
|
||||
| `(fast < slow) & (fast.shift(1) >= slow.shift(1))` | `ta.crossunder(fast, slow)` |
|
||||
| `signal.where(condition, 0)` | `condition ? value : 0` |
|
||||
| `np.where(cond, val_true, val_false)` | `cond ? val_true : val_false` |
|
||||
| `signal.clip(-1, 1)` | `math.max(-1, math.min(1, signal))` |
|
||||
| `signal.fillna(0)` | `nz(signal, 0)` |
|
||||
| `pd.isna(value)` | `na(value)` |
|
||||
|
||||
### Position Sizing
|
||||
|
||||
| Python Pattern | Pine Script v6 |
|
||||
|---------------|----------------|
|
||||
| Equal weight 1/N | `strategy.percent_of_equity` with `default_qty_value = 100/N` |
|
||||
| Full position on signal=1.0 | `default_qty_type=strategy.percent_of_equity, default_qty_value=100` |
|
||||
| Half position on signal=0.5 | Use `strategy.entry(..., qty=strategy.equity * 0.5 / close)` |
|
||||
| Stop-loss | `strategy.exit("Exit", stop=entryPrice * (1 - stopPct))` |
|
||||
| Take-profit | `strategy.exit("Exit", limit=entryPrice * (1 + tpPct))` |
|
||||
|
||||
## Pine Script v6 Syntax Rules
|
||||
|
||||
### Critical Rules (common errors)
|
||||
|
||||
1. **Version declaration must be first line**: `//@version=6`
|
||||
2. **Ternary operators MUST stay on one line** or use intermediate variables:
|
||||
```pinescript
|
||||
// WRONG — causes "end of line without line continuation"
|
||||
text = condition ? "value1" :
|
||||
"value2"
|
||||
|
||||
// CORRECT
|
||||
text = condition ? "value1" : "value2"
|
||||
```
|
||||
3. **Line continuation**: continuation lines must be indented MORE than the starting line
|
||||
4. **No plot() in local scope** (if/for/function):
|
||||
```pinescript
|
||||
// WRONG
|
||||
if condition
|
||||
plot(value)
|
||||
|
||||
// CORRECT
|
||||
plot(condition ? value : na)
|
||||
```
|
||||
5. **var vs regular variables**: use `var` for persistent state across bars, regular assignment recalculates each bar
|
||||
6. **varip**: persistent even on real-time bar updates (intrabar persistence)
|
||||
|
||||
### Script Types
|
||||
|
||||
| Type | Declaration | Use Case |
|
||||
|------|-------------|----------|
|
||||
| `strategy` | `strategy("Name", ...)` | Backtestable with entry/exit orders |
|
||||
| `indicator` | `indicator("Name", ...)` | Visual indicators, no backtesting |
|
||||
| `library` | `library("Name")` | Reusable functions |
|
||||
|
||||
**Default output: `strategy`** (matches Vibe-Trading's backtest-first philosophy).
|
||||
|
||||
### Avoid Repainting
|
||||
|
||||
- Use `barstate.isconfirmed` for signals on confirmed bars only
|
||||
- Use `request.security()` with `lookahead=barmerge.lookahead_off` (default in v6)
|
||||
- Do NOT use `security()` on lower timeframes without understanding implications
|
||||
|
||||
### Platform Limits
|
||||
|
||||
- Max 500 bars lookback for `[]` operator
|
||||
- Max 500 plot calls
|
||||
- Max 64 `strategy.entry/exit` calls per bar
|
||||
- Max 40 `request.security()` calls
|
||||
|
||||
## TradingView Symbol Format
|
||||
|
||||
When generating Pine Script, map Vibe-Trading codes to TradingView symbols:
|
||||
|
||||
| Vibe-Trading Format | TradingView Format | Exchange |
|
||||
|---------------------|-------------------|----------|
|
||||
| `000001.SZ` | `SZSE:000001` | Shenzhen |
|
||||
| `600519.SH` | `SSE:600519` | Shanghai |
|
||||
| `AAPL.US` | `NASDAQ:AAPL` | US (auto-detect) |
|
||||
| `700.HK` | `HKEX:0700` | Hong Kong |
|
||||
| `BTC-USDT` | `OKX:BTCUSDT.P` or `BINANCE:BTCUSDT` | Crypto |
|
||||
|
||||
**Note**: Pine Script strategies are chart-independent — the user applies them to whichever symbol they want on TradingView. Include a comment noting the original instrument.
|
||||
|
||||
## Usage Instructions (include in response)
|
||||
|
||||
After generating the Pine Script, always include:
|
||||
|
||||
```
|
||||
How to use in TradingView:
|
||||
1. Open TradingView → Pine Editor (bottom panel)
|
||||
2. Click "Open" → "New blank indicator"
|
||||
3. Delete all existing code
|
||||
4. Paste the Pine Script above
|
||||
5. Click "Add to Chart"
|
||||
6. The strategy will appear on your chart with entry/exit markers
|
||||
7. Open "Strategy Tester" tab to see backtest results
|
||||
```
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before outputting the Pine Script:
|
||||
- [ ] `//@version=6` is the first line
|
||||
- [ ] `strategy()` declaration includes commission and initial_capital matching config.json
|
||||
- [ ] All indicator calculations use Pine built-in `ta.*` functions (not manual reimplementation)
|
||||
- [ ] Entry/exit conditions match the Python signal logic semantically
|
||||
- [ ] No `plot()` inside local scopes (if/for/function)
|
||||
- [ ] Ternary operators are on single lines
|
||||
- [ ] Inputs use `input.int()` / `input.float()` so users can tune parameters in TradingView UI
|
||||
- [ ] Alert conditions are included for key signals
|
||||
- [ ] Comment header notes the original Vibe-Trading run_id and instrument
|
||||
@@ -0,0 +1,84 @@
|
||||
import { memo, useState, useCallback } from "react";
|
||||
import { X, Copy, Check, ExternalLink } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const PineScriptViewer = memo(function PineScriptViewer({ code, onClose }: Props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = code;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" onClick={onClose}>
|
||||
<div
|
||||
className="relative w-full max-w-3xl max-h-[80vh] mx-4 rounded-xl border bg-background shadow-2xl flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold">Pine Script v6</span>
|
||||
<span className="text-xs text-muted-foreground">strategy.pine</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
|
||||
>
|
||||
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
<a
|
||||
href="https://www.tradingview.com/pine-script-docs/welcome/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 px-2 py-1.5 rounded-lg text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Docs
|
||||
</a>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code */}
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<pre className="text-xs leading-relaxed font-mono whitespace-pre-wrap break-words text-foreground/90">
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-4 py-2.5 border-t bg-muted/30">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
TradingView Pine Editor → New blank indicator → Paste code → Add to Chart
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,11 +1,12 @@
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { memo, useEffect, useState, useCallback } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { BarChart3 } from "lucide-react";
|
||||
import { BarChart3, Code2, Loader2 } from "lucide-react";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { api } from "@/lib/api";
|
||||
import { AgentAvatar } from "./AgentAvatar";
|
||||
import { MetricsCard } from "./MetricsCard";
|
||||
import { MiniEquityChart } from "@/components/charts/MiniEquityChart";
|
||||
import { PineScriptViewer } from "./PineScriptViewer";
|
||||
import type { AgentMessage } from "@/types/agent";
|
||||
|
||||
interface Props {
|
||||
@@ -15,6 +16,11 @@ interface Props {
|
||||
export const RunCompleteCard = memo(function RunCompleteCard({ msg }: Props) {
|
||||
const { t } = useI18n();
|
||||
const [curve, setCurve] = useState(msg.equityCurve);
|
||||
const [pineCode, setPineCode] = useState<string | null>(null);
|
||||
const [pineLoading, setPineLoading] = useState(false);
|
||||
const [showPine, setShowPine] = useState(false);
|
||||
const [pineChecked, setPineChecked] = useState(false);
|
||||
const [pineExists, setPineExists] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!curve && msg.runId) {
|
||||
@@ -24,6 +30,37 @@ export const RunCompleteCard = memo(function RunCompleteCard({ msg }: Props) {
|
||||
}
|
||||
}, [msg.runId, curve]);
|
||||
|
||||
// Check if Pine Script exists for this run
|
||||
useEffect(() => {
|
||||
if (msg.runId && !pineChecked) {
|
||||
api.getRunPine(msg.runId).then(r => {
|
||||
setPineChecked(true);
|
||||
if (r.exists && r.content) {
|
||||
setPineExists(true);
|
||||
setPineCode(r.content);
|
||||
}
|
||||
}).catch(() => { setPineChecked(true); });
|
||||
}
|
||||
}, [msg.runId, pineChecked]);
|
||||
|
||||
const handlePineClick = useCallback(async () => {
|
||||
if (pineCode) {
|
||||
setShowPine(true);
|
||||
return;
|
||||
}
|
||||
if (!msg.runId) return;
|
||||
setPineLoading(true);
|
||||
try {
|
||||
const r = await api.getRunPine(msg.runId);
|
||||
if (r.exists && r.content) {
|
||||
setPineCode(r.content);
|
||||
setPineExists(true);
|
||||
setShowPine(true);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { setPineLoading(false); }
|
||||
}, [pineCode, msg.runId]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<AgentAvatar />
|
||||
@@ -34,13 +71,28 @@ export const RunCompleteCard = memo(function RunCompleteCard({ msg }: Props) {
|
||||
{curve && curve.length > 1 && (
|
||||
<MiniEquityChart data={curve} height={80} />
|
||||
)}
|
||||
<Link
|
||||
to={`/runs/${msg.runId}`}
|
||||
className="text-sm text-primary hover:underline inline-flex items-center gap-1.5 font-medium"
|
||||
>
|
||||
<BarChart3 className="h-3.5 w-3.5" />
|
||||
{t.fullReport}
|
||||
</Link>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Link
|
||||
to={`/runs/${msg.runId}`}
|
||||
className="text-sm text-primary hover:underline inline-flex items-center gap-1.5 font-medium"
|
||||
>
|
||||
<BarChart3 className="h-3.5 w-3.5" />
|
||||
{t.fullReport}
|
||||
</Link>
|
||||
{pineExists && (
|
||||
<button
|
||||
onClick={handlePineClick}
|
||||
disabled={pineLoading}
|
||||
className="text-sm text-emerald-600 dark:text-emerald-400 hover:underline inline-flex items-center gap-1.5 font-medium disabled:opacity-50"
|
||||
>
|
||||
{pineLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Code2 className="h-3.5 w-3.5" />}
|
||||
Pine Script
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showPine && pineCode && (
|
||||
<PineScriptViewer code={pineCode} onClose={() => setShowPine(false)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -43,6 +43,7 @@ export const api = {
|
||||
listRuns: () => request<RunListItem[]>("/runs"),
|
||||
getRun: (id: string) => request<RunData>(`/runs/${id}`),
|
||||
getRunCode: (id: string) => request<Record<string, string>>(`/runs/${id}/code`),
|
||||
getRunPine: (id: string) => request<PineScriptResult>(`/runs/${id}/pine`),
|
||||
listSessions: () => request<SessionItem[]>("/sessions"),
|
||||
createSession: (title?: string) => request<SessionItem>("/sessions", { method: "POST", body: JSON.stringify({ title: title || "" }) }),
|
||||
deleteSession: (sid: string) => request<{ status: string }>(`/sessions/${sid}`, { method: "DELETE" }),
|
||||
@@ -171,6 +172,11 @@ export interface ArtifactInfo {
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface PineScriptResult {
|
||||
exists: boolean;
|
||||
content: string | null;
|
||||
}
|
||||
|
||||
export interface SessionItem {
|
||||
session_id: string;
|
||||
title?: string;
|
||||
|
||||
Reference in new issue
Block a user