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:
warren618 committed 2026-04-09 11:31:41 +08:00
1 parent d457cf4a5b
commit 211ddb80b7
7 files changed
+434 -17

No files matched your search

+11 -7
View File
@@ -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:
+19
View File
@@ -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
View File
@@ -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:
+230
View File
@@ -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 &rarr; New blank indicator &rarr; Paste code &rarr; 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>
);
+6
View File
@@ -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;