bruedemo 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
brue/__init__.py ADDED
@@ -0,0 +1,65 @@
1
+ """brue: pure-Python implementation of the Brue trading-strategy language.
2
+
3
+ Ported 1:1 from the Rust engine (src/lib.rs); the conformance harness in
4
+ conformance/ verifies byte-identical output against it.
5
+ """
6
+ from __future__ import annotations
7
+
8
+
9
+
10
+ def run_script(source, bars):
11
+ from .runtime import ExternalData
12
+ return run_script_with_data(source, bars, ExternalData())
13
+
14
+
15
+ def run_script_with_data(source, bars, external):
16
+ from .runtime import RunOptions
17
+ return run_script_full(source, bars, external, {}, RunOptions())
18
+
19
+
20
+ def run_script_full(source, bars, external, input_overrides, options):
21
+ """tokenize + parse + execute, errors shaped exactly like the reference."""
22
+ from . import parser, tokenizer
23
+ from .errors import BrueError
24
+
25
+ # Windows editors and Python text-mode writes produce CRLF; the
26
+ # tokenizer speaks LF. Normalize at every entry point.
27
+ source = source.replace("\r\n", "\n").replace("\r", "\n")
28
+ try:
29
+ tokens = tokenizer.tokenize(source)
30
+ except BrueError as e:
31
+ return _error_output(e)
32
+ try:
33
+ program = parser.parse(tokens)
34
+ except BrueError as e:
35
+ return _error_output(e)
36
+ from .runtime import execute_with_options
37
+ return execute_with_options(program, bars, input_overrides, external, options)
38
+
39
+
40
+ # parse_time_arg and parse_bars_csv live in data.py, ported with Rust-exact
41
+ # parsing (float()/int() alone are looser than f64::from_str: underscores,
42
+ # unicode digits); re-exported here for the lib.rs-shaped public API.
43
+ from .data import parse_bars_csv, parse_time_arg # noqa: E402,F401
44
+ from .api import BacktestResult, BrueScriptError, backtest # noqa: E402,F401
45
+
46
+
47
+ def _error_output(e):
48
+ from .output import ErrorInfo, ExecOutput
49
+ from .value import Value
50
+ return ExecOutput(
51
+ success=False,
52
+ commands=[],
53
+ errors=[ErrorInfo(message=e.message, line=e.loc.line, col=e.loc.col,
54
+ phase=e.phase, suggestion=e.suggestion)],
55
+ inputs=[],
56
+ live_orders=[],
57
+ live_closes=[],
58
+ decl_title=Value.Null,
59
+ decl_overlay=Value.Bool(True),
60
+ decl_symbol=None,
61
+ decl_timeframe=None,
62
+ strategy_result=None,
63
+ )
64
+
65
+
brue/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
brue/api.py ADDED
@@ -0,0 +1,182 @@
1
+ """The Python-native front door for the brue package.
2
+
3
+ The engine internals speak BarData lists and ExecOutput (ported 1:1 from
4
+ the reference engine; conformance-locked, do not touch). This module is the
5
+ convenience layer a Python user actually calls:
6
+
7
+ import brue
8
+
9
+ result = brue.backtest(script, df) # df: pandas or rows
10
+ result.stats["netProfit"], result.trades, result.equity_curve
11
+
12
+ Accepted data shapes: a pandas DataFrame (any case-insensitive subset of
13
+ time/open/high/low/close/volume columns, time in epoch s, epoch ms, or
14
+ datetimes), a list of dicts with those keys, a list of [t,o,h,l,c,v] rows,
15
+ or a path to a csv. pandas is NOT a dependency: it is duck-typed, so the
16
+ package installs clean without it.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+
22
+
23
+ class BrueScriptError(Exception):
24
+ """The script failed to tokenize/parse/run; message is user-facing."""
25
+
26
+ def __init__(self, errors):
27
+ self.errors = errors
28
+ first = errors[0] if errors else {}
29
+ super().__init__(first.get("message", "script error"))
30
+
31
+
32
+ class BacktestResult:
33
+ """Friendly view over the engine's canonical output."""
34
+
35
+ def __init__(self, raw: dict, bar_times=None):
36
+ self.raw = raw
37
+ sr = raw.get("strategyResult") or {}
38
+ self.stats = {k: v for k, v in sr.items()
39
+ if k not in ("trades", "equityCurve")}
40
+ self.trades = sr.get("trades", [])
41
+ # The engine emits one equity value per bar, no timestamps; the
42
+ # caller's bar times restore the x axis.
43
+ self.equity_curve = sr.get("equityCurve", [])
44
+ self.bar_times = bar_times or []
45
+
46
+ def trades_frame(self):
47
+ """The trade ledger as a pandas DataFrame (pandas required here)."""
48
+ import pandas as pd
49
+ return pd.DataFrame(self.trades)
50
+
51
+ def equity_frame(self):
52
+ import pandas as pd
53
+ df = pd.DataFrame({"equity": self.equity_curve})
54
+ if len(self.bar_times) == len(df):
55
+ df.insert(0, "time", pd.to_datetime(self.bar_times, unit="ms"))
56
+ return df
57
+
58
+ def __repr__(self):
59
+ s = self.stats
60
+ return (f"BacktestResult(trades={s.get('totalTrades')}, "
61
+ f"netProfit={s.get('netProfit')}, "
62
+ f"winRate={s.get('winRate')}, "
63
+ f"profitFactor={s.get('profitFactor')})")
64
+
65
+
66
+ def _to_ms(t) -> float:
67
+ """Epoch seconds, epoch ms, or datetime-like -> epoch ms."""
68
+ if hasattr(t, "timestamp"):
69
+ return float(t.timestamp() * 1000.0)
70
+ x = float(t)
71
+ # Heuristic shared with the userdata importer: values before the year
72
+ # 5138 in seconds are seconds; anything bigger is already ms.
73
+ return x * 1000.0 if x < 1e11 else x
74
+
75
+
76
+ def _bars_from_any(data):
77
+ from .runtime import BarData
78
+
79
+ if isinstance(data, str):
80
+ from . import parse_bars_csv
81
+ with open(data, encoding="utf-8") as f:
82
+ return parse_bars_csv(f.read())
83
+
84
+ # pandas DataFrame, duck-typed so pandas stays optional.
85
+ if hasattr(data, "columns") and hasattr(data, "itertuples"):
86
+ cols = {str(c).strip().lower(): c for c in data.columns}
87
+ tcol = next((cols[k] for k in ("time", "ts", "timestamp", "date",
88
+ "datetime") if k in cols), None)
89
+ need = [k for k in ("open", "high", "low", "close") if k not in cols]
90
+ if tcol is None or need:
91
+ missing = (["time"] if tcol is None else []) + need
92
+ raise ValueError(f"data is missing columns: {', '.join(missing)}")
93
+ # Follow the index when there is no time column? No: explicit only,
94
+ # a wrong guess silently shifts every fill.
95
+ out = []
96
+ vol = cols.get("volume")
97
+ for row in data.itertuples(index=False):
98
+ d = dict(zip(data.columns, row))
99
+ out.append(BarData(
100
+ time=_to_ms(d[tcol]),
101
+ open=float(d[cols["open"]]), high=float(d[cols["high"]]),
102
+ low=float(d[cols["low"]]), close=float(d[cols["close"]]),
103
+ volume=float(d[vol]) if vol is not None else 0.0))
104
+ return out
105
+
106
+ if isinstance(data, (list, tuple)) and data:
107
+ first = data[0]
108
+ if isinstance(first, dict):
109
+ return [BarData(time=_to_ms(r.get("time", r.get("ts", r.get("timestamp")))),
110
+ open=float(r["open"]), high=float(r["high"]),
111
+ low=float(r["low"]), close=float(r["close"]),
112
+ volume=float(r.get("volume", 0.0)))
113
+ for r in data]
114
+ if isinstance(first, (list, tuple)) and len(first) >= 5:
115
+ return [BarData(time=_to_ms(r[0]), open=float(r[1]), high=float(r[2]),
116
+ low=float(r[3]), close=float(r[4]),
117
+ volume=float(r[5]) if len(r) > 5 else 0.0)
118
+ for r in data]
119
+ raise ValueError("unsupported data shape: pass a DataFrame, list of "
120
+ "dicts, list of [t,o,h,l,c,v] rows, or a csv path")
121
+
122
+
123
+ def indicator_series(script: str, data, inputs: dict | None = None) -> dict:
124
+ """
125
+ Run a Brue script as a chart INDICATOR: every plot() call becomes a
126
+ named series aligned to the bars. This is how hosts (the LSE Terminal,
127
+ notebooks) draw Brue-written indicators on live charts.
128
+
129
+ Returns {"plots": {name: [float per bar]}, "meta": {name: kwargs},
130
+ "overlay": bool, "inputs": [declared input() specs],
131
+ "times": [epoch ms per bar]}.
132
+ Raises BrueScriptError on script errors.
133
+ """
134
+ from . import run_script_full
135
+ from .runtime import ExternalData, RunOptions
136
+ from .value import Value
137
+
138
+ bars = _bars_from_any(data)
139
+ options = RunOptions()
140
+ options.collect_plots = True
141
+ overrides = {}
142
+ for k, v in (inputs or {}).items():
143
+ overrides[str(k)] = (Value.Bool(v) if isinstance(v, bool)
144
+ else Value.Num(v) if isinstance(v, (int, float))
145
+ else Value.Str(str(v)))
146
+ out = run_script_full(script, bars, ExternalData(), overrides, options)
147
+ raw = json.loads(out.to_json())
148
+ if not raw.get("success"):
149
+ raise BrueScriptError(raw.get("errors", []))
150
+ n = len(bars)
151
+ plots = {}
152
+ for name, points in getattr(out, "plots", {}).items():
153
+ col = [float("nan")] * n
154
+ for bar_i, v in points:
155
+ if 0 <= bar_i < n:
156
+ col[bar_i] = v
157
+ plots[name] = col
158
+ return {"plots": plots,
159
+ "meta": getattr(out, "plot_meta", {}),
160
+ "overlay": bool((raw.get("declaration") or {}).get("overlay", True)),
161
+ "inputs": raw.get("inputs", []),
162
+ "times": [b.time for b in bars]}
163
+
164
+
165
+ def backtest(script: str, data, extended_stats: bool = False,
166
+ trade_from=None, trade_to=None) -> BacktestResult:
167
+ """Run a Brue strategy over the given bars, entirely in-process."""
168
+ from . import run_script_full
169
+ from .runtime import ExternalData, RunOptions
170
+
171
+ bars = _bars_from_any(data)
172
+ options = RunOptions()
173
+ options.extended_stats = bool(extended_stats)
174
+ if trade_from is not None:
175
+ options.trade_from = _to_ms(trade_from)
176
+ if trade_to is not None:
177
+ options.trade_to = _to_ms(trade_to)
178
+ out = run_script_full(script, bars, ExternalData(), {}, options)
179
+ raw = json.loads(out.to_json())
180
+ if not raw.get("success"):
181
+ raise BrueScriptError(raw.get("errors", []))
182
+ return BacktestResult(raw, bar_times=[b.time for b in bars])
brue/ast.py ADDED
@@ -0,0 +1,319 @@
1
+ # AST node types, mirroring src/brue/ast.ts one-for-one (ported from
2
+ # brue-rust src/ast.rs). The kind_str names must match the TS `kind` strings
3
+ # exactly: the runtime's expression fingerprint falls back to
4
+ # "{kind}_{line}_{col}" and those keys decide series-cache identity.
5
+ from dataclasses import dataclass
6
+ from typing import List, Optional, Tuple
7
+
8
+
9
+ @dataclass
10
+ class Loc:
11
+ line: int
12
+ col: int
13
+
14
+
15
+ class Expr:
16
+ """Base class for all expression nodes (the Rust Expr enum)."""
17
+
18
+ # The TS `kind` string (used by the fingerprint fallback). Each subclass
19
+ # sets _KIND; kind_str() keeps the Rust method name.
20
+ _KIND = ""
21
+
22
+ def kind_str(self) -> str:
23
+ return self._KIND
24
+
25
+
26
+ @dataclass
27
+ class KeywordArg:
28
+ name: str
29
+ value: Expr
30
+
31
+
32
+ @dataclass
33
+ class NumberLiteral(Expr):
34
+ value: float
35
+ loc: Loc
36
+ _KIND = "NumberLiteral"
37
+
38
+
39
+ @dataclass
40
+ class StringLiteral(Expr):
41
+ value: str
42
+ loc: Loc
43
+ _KIND = "StringLiteral"
44
+
45
+
46
+ @dataclass
47
+ class BoolLiteral(Expr):
48
+ value: bool
49
+ loc: Loc
50
+ _KIND = "BoolLiteral"
51
+
52
+
53
+ @dataclass
54
+ class NaLiteral(Expr):
55
+ loc: Loc
56
+ _KIND = "NaLiteral"
57
+
58
+
59
+ @dataclass
60
+ class ColorLiteral(Expr):
61
+ r: float
62
+ g: float
63
+ b: float
64
+ a: float
65
+ loc: Loc
66
+ _KIND = "ColorLiteral"
67
+
68
+
69
+ @dataclass
70
+ class Identifier(Expr):
71
+ name: str
72
+ loc: Loc
73
+ _KIND = "Identifier"
74
+
75
+
76
+ @dataclass
77
+ class Binary(Expr):
78
+ op: str
79
+ left: Expr
80
+ right: Expr
81
+ loc: Loc
82
+ _KIND = "BinaryExpr"
83
+
84
+
85
+ @dataclass
86
+ class Unary(Expr):
87
+ op: str
88
+ operand: Expr
89
+ loc: Loc
90
+ _KIND = "UnaryExpr"
91
+
92
+
93
+ @dataclass
94
+ class Call(Expr):
95
+ callee: str
96
+ args: List[Expr]
97
+ kwargs: List[KeywordArg]
98
+ loc: Loc
99
+ _KIND = "CallExpr"
100
+
101
+
102
+ @dataclass
103
+ class History(Expr):
104
+ source: Expr
105
+ offset: Expr
106
+ loc: Loc
107
+ _KIND = "HistoryExpr"
108
+
109
+
110
+ @dataclass
111
+ class Ternary(Expr):
112
+ condition: Expr
113
+ if_true: Expr
114
+ if_false: Expr
115
+ loc: Loc
116
+ _KIND = "TernaryExpr"
117
+
118
+
119
+ @dataclass
120
+ class ArrayLiteral(Expr):
121
+ elements: List[Expr]
122
+ loc: Loc
123
+ _KIND = "ArrayLiteral"
124
+
125
+
126
+ @dataclass
127
+ class MapLiteral(Expr):
128
+ entries: List[Tuple[Expr, Expr]]
129
+ loc: Loc
130
+ _KIND = "MapLiteral"
131
+
132
+
133
+ @dataclass
134
+ class Member(Expr):
135
+ object: Expr
136
+ member: str
137
+ loc: Loc
138
+ _KIND = "MemberExpr"
139
+
140
+
141
+ @dataclass
142
+ class MethodCall(Expr):
143
+ object: Expr
144
+ method: str
145
+ args: List[Expr]
146
+ loc: Loc
147
+ _KIND = "MethodCallExpr"
148
+
149
+
150
+ class FmtPart:
151
+ """The Rust FmtPart enum; variants are the nested classes
152
+ FmtPart.Text / FmtPart.Expr so call sites keep the Rust paths."""
153
+
154
+ @dataclass
155
+ class Text:
156
+ value: str
157
+
158
+ @dataclass
159
+ class Expr:
160
+ expr: "Expr"
161
+
162
+
163
+ @dataclass
164
+ class FormatString(Expr):
165
+ parts: List[object] # FmtPart.Text | FmtPart.Expr
166
+ specs: List[Optional[str]]
167
+ loc: Loc
168
+ _KIND = "FormatStringExpr"
169
+
170
+
171
+ @dataclass
172
+ class FunctionParam:
173
+ name: str
174
+ default_value: Optional[Expr]
175
+
176
+
177
+ @dataclass
178
+ class TypeField:
179
+ name: str
180
+ field_type: str
181
+ default_value: Optional[Expr]
182
+
183
+
184
+ @dataclass
185
+ class DeclarationStmt:
186
+ args: List[Expr]
187
+ kwargs: List[KeywordArg]
188
+ symbol: Optional[str]
189
+ timeframe: Optional[str]
190
+ execution_mode: str # "backtest" | "execute"
191
+ broker: Optional[str]
192
+ account: Optional[str]
193
+ password: Optional[str]
194
+ loc: Loc
195
+
196
+
197
+ @dataclass
198
+ class UseStmt:
199
+ canonical: str
200
+ timeframe: Optional[str]
201
+ alias: str
202
+ alias_explicit: bool
203
+ is_dataset: bool
204
+ loc: Loc
205
+
206
+
207
+ class Stmt:
208
+ """Base class for all statement nodes (the Rust Stmt enum)."""
209
+
210
+
211
+ @dataclass
212
+ class Assign(Stmt):
213
+ target: str
214
+ op: str
215
+ value: Expr
216
+ loc: Loc
217
+
218
+
219
+ @dataclass
220
+ class Persist(Stmt):
221
+ target: str
222
+ value: Expr
223
+ loc: Loc
224
+
225
+
226
+ @dataclass
227
+ class Const(Stmt):
228
+ target: str
229
+ value: Expr
230
+ loc: Loc
231
+
232
+
233
+ @dataclass
234
+ class If(Stmt):
235
+ condition: Expr
236
+ body: List[Stmt]
237
+ elifs: List[Tuple[Expr, List[Stmt]]]
238
+ else_body: List[Stmt]
239
+ loc: Loc
240
+
241
+
242
+ @dataclass
243
+ class For(Stmt):
244
+ variable: str
245
+ iterable: Expr
246
+ body: List[Stmt]
247
+ loc: Loc
248
+
249
+
250
+ @dataclass
251
+ class While(Stmt):
252
+ condition: Expr
253
+ body: List[Stmt]
254
+ loc: Loc
255
+
256
+
257
+ @dataclass
258
+ class Break(Stmt):
259
+ loc: Loc
260
+
261
+
262
+ @dataclass
263
+ class Continue(Stmt):
264
+ loc: Loc
265
+
266
+
267
+ @dataclass
268
+ class FunctionDef(Stmt):
269
+ name: str
270
+ params: List[FunctionParam]
271
+ body: List[Stmt]
272
+ loc: Loc
273
+
274
+
275
+ @dataclass
276
+ class Return(Stmt):
277
+ value: Optional[Expr]
278
+ loc: Loc
279
+
280
+
281
+ @dataclass
282
+ class ExprStmt(Stmt):
283
+ expr: Expr
284
+ loc: Loc
285
+
286
+
287
+ @dataclass
288
+ class MemberAssign(Stmt):
289
+ target: Expr
290
+ value: Expr
291
+ loc: Loc
292
+
293
+
294
+ @dataclass
295
+ class MultiAssign(Stmt):
296
+ targets: List[str]
297
+ value: Expr
298
+ loc: Loc
299
+
300
+
301
+ @dataclass
302
+ class TypeDef(Stmt):
303
+ name: str
304
+ fields: List[TypeField]
305
+ loc: Loc
306
+
307
+
308
+ @dataclass
309
+ class EnumDef(Stmt):
310
+ name: str
311
+ members: List[Tuple[str, Optional[str]]]
312
+ loc: Loc
313
+
314
+
315
+ @dataclass
316
+ class Program:
317
+ declaration: DeclarationStmt
318
+ uses: List[UseStmt]
319
+ body: List[Stmt]