Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ dist/
.pnp.*

# Generated files
chart-data.json
out/chart-data.json
out/chart-config.json
out/e2e-*-output.json
Expand Down Expand Up @@ -230,3 +231,10 @@ testdata/*-output.json
# Contribot
contribot.*.json
transcripts/

.playwright-cli
# Editor and tool artifacts
*.backup

# Claude Code agent artifacts
.claude/scheduled_tasks.lock
129 changes: 129 additions & 0 deletions .interface-design/system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Design System

## File Structure

```
out/css/tokens.css — CSS custom properties (single source of truth for all tokens)
out/css/styles.css — component styles (@import ./tokens.css; zero hardcoded values)
out/index.html — <link rel="stylesheet" href="css/styles.css" />
```

Dependency graph: `index.html → styles.css → tokens.css`

## Tokens (`out/css/tokens.css`)

### Surfaces
| Token | Value | Usage |
|---|---|---|
| `--surface-base` | `#0f172a` | Deepest bg: body gradient start, table `<th>` |
| `--surface-card` | `#1a2035` | Section bg: `.trades-section`, `.info` |
| `--surface-raised` | `#1e293b` | Card bg: `#container`, `.chart-container` |
| `--surface-raised-alpha` | `rgba(30,41,59,0.85)` | Fullscreen toggle button bg |
| `--surface-hover` | `rgba(255,255,255,0.06)` | Row hover — visible on any dark surface |

### Borders
| Token | Value | Usage |
|---|---|---|
| `--border-subtle` | `#2d3748` | Inner dividers, row borders, section internals |
| `--border-default` | `#334155` | Outer borders: container, chart, buttons |

### Text
| Token | Value | Usage |
|---|---|---|
| `--text-primary` | `#cbd5e1` | Body text, card text |
| `--text-secondary` | `#94a3b8` | `<th>`, summary, button default label |
| `--text-muted` | `#6b7280` | Timestamp, `.no-trades` placeholder |

### Accent
| Token | Value | Usage |
|---|---|---|
| `--color-accent` | `#5eead4` | Section headings, hover state, focus ring |
| `--color-accent-alt` | `#2dd4bf` | h1 gradient end |
| `--color-accent-subtle` | `rgba(94,234,212,0.25)` | Pane resize handle hover fill |

### Actions
| Token | Value | Usage |
|---|---|---|
| `--color-action` | `#2563eb` | `.refresh-btn` bg |
| `--color-action-hover` | `#1d4ed8` | `.refresh-btn:hover` bg |

### Semantic
| Token | Value | Usage |
|---|---|---|
| `--color-long` | `#10b981` | Long direction, positive P/L |
| `--color-short` | `#ef4444` | Short direction, negative P/L |
| `--color-open` | `#3b82f6` | Open / in-progress trades |

### Trade Table
| Token | Value | Usage |
|---|---|---|
| `--stripe-tint` | `rgba(255,255,255,0.03)` | Alternating trade-pair row tint |

### Typography
| Token | Value | Usage |
|---|---|---|
| `--font-ui` | `'Segoe UI', system-ui, sans-serif` | All text |

### Border Radius
| Token | Value | Usage |
|---|---|---|
| `--radius-md` | `6px` | Most elements |
| `--radius-lg` | `8px` | Outer `#container` only |

### Shadows
| Token | Value | Usage |
|---|---|---|
| `--shadow-container` | `0 4px 6px -1px rgba(0,0,0,0.2)` | `#container` only |
| `--shadow-text` | `0 2px 4px rgba(0,0,0,0.3)` | `h1` text-shadow |

### Transitions
| Token | Value | Usage |
|---|---|---|
| `--transition-duration` | `0.2s` | All interactive elements |

## Spacing Scale
Rem-based: `0.5rem`, `0.75rem`, `1rem`, `1.25rem`, `1.5rem`, `2rem`, `2.5rem`
Pixel exceptions: `8px` (absolute inset positioning only)

## Font Scale
| Size | Usage |
|---|---|
| `2.5rem` | `h1` page title |
| `1.5rem` | `h2` section title |
| `1rem` | Body, action button |
| `0.875rem` | Table, timestamp, secondary labels, compact buttons |

Font weight `600`: profit values, table headers.

## Component Patterns

### Filled button (`.refresh-btn`)
- `background-color: var(--color-action)` → hover: `var(--color-action-hover)`
- `padding: 0.75rem 1.5rem`, `border-radius: var(--radius-md)`, `font-size: 1rem`
- `:focus-visible`: `outline: 2px solid var(--color-accent); outline-offset: 2px`
- `:active`: `transform: scale(0.97)`

### Ghost/outline button (`.sort-toggle-btn`, `.fullscreen-toggle-btn`)
- `background: transparent`, `border: 1px solid var(--border-default)`, `color: var(--text-secondary)`
- Hover: `border-color: var(--color-accent); color: var(--color-accent)`
- `:focus-visible`: same accent outline ring as filled button
- `:active`: `transform: scale(0.97)`

### Section card (`.trades-section`, `.info`)
- `background-color: var(--surface-card)`, `border: 1px solid var(--border-subtle)`, `border-radius: var(--radius-md)`, `padding: 1rem`

### Section header (`.trades-header`)
- `display: flex`, `justify-content: space-between`, `align-items: center`
- `margin-bottom: 1rem`, `padding-bottom: 0.5rem`, `border-bottom: 1px solid var(--border-subtle)`
- Title: `color: var(--color-accent)`

## Chart-specific (JS, not CSS)
Chart colors are LightweightCharts API options passed in `index.html` inline `<script>`.
They are intentionally separate from the CSS token system (different rendering context).

| Value | Usage |
|---|---|
| `#1e1e3c` | Chart layout background |
| `#DDD` | Chart text color |
| `#2B2B43` | Grid lines, scale borders |
| `#26a69a` / `#ef5350` | Candlestick up/down (TradingView convention) |
23 changes: 8 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Makefile for Runner - PineScript Go Port
# Centralized build automation following Go project conventions

.PHONY: help build test test-unit test-integration test-e2e test-golden test-golden-update test-parser test-codegen test-runtime test-series test-syminfo regression-syminfo bench bench-series coverage coverage-show check ci clean clean-all cross-compile fmt vet lint build-strategy
.PHONY: help build test test-unit test-integration test-e2e test-golden test-golden-update test-parser test-codegen test-runtime test-series test-syminfo regression-syminfo test-ui bench bench-series coverage coverage-show check ci clean clean-all cross-compile fmt vet lint build-strategy

# Project configuration
PROJECT_NAME := runner
Expand Down Expand Up @@ -161,6 +161,11 @@ test-syminfo: ## Run syminfo.tickerid integration tests only
test-syminfo-regression: ## Run syminfo.tickerid regression test suite
@./scripts/test-syminfo-regression.sh

test-ui: ## Run chart viewer JS unit tests (node:test, no install required)
@echo "Running chart viewer JS tests..."
@node --test out/tests/*.test.js
@echo "✓ Chart UI tests passed"

bench: ## Run benchmarks
@echo "Running benchmarks..."
@ $(GO) test $(BENCH_FLAGS) -bench=. ./...
Expand Down Expand Up @@ -356,21 +361,9 @@ all: ci ## Full validation (format, vet, lint, build, all tests)

install-hooks: ## Install git pre-commit hook
@echo "Installing pre-commit hook..."
@echo '#!/bin/sh' > .git/hooks/pre-commit
@echo '# Git pre-commit hook - full validation' >> .git/hooks/pre-commit
@echo 'set -e' >> .git/hooks/pre-commit
@echo 'export PATH="$$HOME/.local/go/bin:/usr/local/go/bin:$$PATH"' >> .git/hooks/pre-commit
@echo 'export GOPATH="$$HOME/go"' >> .git/hooks/pre-commit
@echo 'export PATH="$$PATH:$$GOPATH/bin"' >> .git/hooks/pre-commit
@echo 'if ! command -v go >/dev/null 2>&1; then' >> .git/hooks/pre-commit
@echo ' echo "✗ Go not found. Run: make install"' >> .git/hooks/pre-commit
@echo ' exit 1' >> .git/hooks/pre-commit
@echo 'fi' >> .git/hooks/pre-commit
@echo 'echo "🔍 Running pre-commit validation..."' >> .git/hooks/pre-commit
@echo 'make all' >> .git/hooks/pre-commit
@echo 'exit 0' >> .git/hooks/pre-commit
@cp scripts/pre-commit-hook.sh .git/hooks/pre-commit
@chmod +x .git/hooks/pre-commit
@echo "✓ Pre-commit hook installed (runs: make all)"
@echo "✓ Pre-commit hook installed"

install: ## Install Go to ~/.local (no sudo required)
@./scripts/install-deps.sh
Expand Down
12 changes: 12 additions & 0 deletions cmd/pine-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ func main() {
os.Exit(1)
}

reportFeatureGaps(strategyCode.FeatureGaps)

temporaryDirectory := os.TempDir()

/* Create unique temp file to avoid conflicts when running tests in parallel */
Expand All @@ -141,6 +143,16 @@ func main() {
fmt.Printf("Next: Compile with: go build -o %s %s\n", *outputFlag, temporaryGoFile)
}

func reportFeatureGaps(gaps []string) {
if len(gaps) == 0 {
return
}
fmt.Fprintf(os.Stderr, "WARNING: %d unimplemented Pine function(s) — binary runs with stubs:\n", len(gaps))
for _, name := range gaps {
fmt.Fprintf(os.Stderr, " - %s()\n", name)
}
}

func deriveStrategyNameFromSourceFile(inputPath string) string {
baseFilename := filepath.Base(inputPath)
extension := filepath.Ext(baseFilename)
Expand Down
15 changes: 9 additions & 6 deletions codegen/argument_expression_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,22 @@ func (g *ArgumentExpressionGenerator) ensureFloat64(expr ast.Expression, code st
}

func (g *ArgumentExpressionGenerator) generateIdentifier(id *ast.Identifier) (string, error) {
expectsSeries := false
if g.signatureRegistry != nil {
paramType, hasSignature := g.signatureRegistry.GetParameterType(g.functionName, g.parameterIndex)
expectsSeries = hasSignature && paramType == ParamTypeSeries
}

if constVal, isConstant := g.generator.constants[id.Name]; isConstant {
if constVal == "input.source" {
if expectsSeries {
return fmt.Sprintf("%sSeries", id.Name), nil
}
return fmt.Sprintf("%sSeries.GetCurrent()", id.Name), nil
}
return id.Name, nil
}

expectsSeries := false
if g.signatureRegistry != nil {
paramType, hasSignature := g.signatureRegistry.GetParameterType(g.functionName, g.parameterIndex)
expectsSeries = hasSignature && paramType == ParamTypeSeries
}

// Arrow resolver checked before builtins; bypassed when passing to a series-typed parameter
// so the identifier routes to *Series by name convention instead of scalar resolution.
if !expectsSeries && g.generator.arrowAccessResolver != nil {
Expand Down
4 changes: 1 addition & 3 deletions codegen/argument_extractor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,7 @@ func TestExtractNamedOrPositional_UseDefault(t *testing.T) {
}

func TestExtractWhenCondition_Found(t *testing.T) {
g := &generator{
builtinHandler: NewBuiltinIdentifierHandler(),
}
g := newTestGenerator()
extractor := &ArgumentExtractor{generator: g}

args := []ast.Expression{
Expand Down
15 changes: 14 additions & 1 deletion codegen/argument_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,27 @@ func (p *ArgumentParser) ParseInt(expr ast.Expression) ParsedArgument {

/*
ParseFloat extracts a float literal from an AST expression.
Handles both float64 and int AST literal types.
Handles float64, int, and UnaryExpression for negative numbers.

Returns:

ParsedArgument.IsValid = true if numeric literal found
ParsedArgument.Value = float64 value
*/
func (p *ArgumentParser) ParseFloat(expr ast.Expression) ParsedArgument {
if unary, ok := expr.(*ast.UnaryExpression); ok && unary.Operator == "-" {
inner := p.ParseFloat(unary.Argument)
if inner.IsValid {
return ParsedArgument{
IsValid: true,
IsLiteral: true,
Value: -inner.MustBeFloat(),
SourceExpr: expr,
}
}
return ParsedArgument{IsValid: false, SourceExpr: expr}
}

lit, ok := expr.(*ast.Literal)
if !ok {
return ParsedArgument{IsValid: false, SourceExpr: expr}
Expand Down
Loading
Loading