bruedemo 0.1.0__tar.gz

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.
@@ -0,0 +1,2 @@
1
+ __pycache__/
2
+ conformance/out/
@@ -0,0 +1,83 @@
1
+ # Brue's job: the decision layer
2
+
3
+ One sentence: given bars, Brue decides what a strategy does, and it makes
4
+ those decisions identically everywhere. Everything else in a trading
5
+ pipeline belongs to something else.
6
+
7
+ ```
8
+ DATA -> BRUE -> EXECUTION -> RESULTS
9
+ pandas / feeds / the strategy backtest: simulated stats, trade
10
+ broker bars / csv brain fills inside Brue ledger, equity
11
+ live: order intents -> broker adapter
12
+ (user's account)
13
+ ```
14
+
15
+ ## What Brue OWNS
16
+
17
+ 1. **Indicators.** ema, rsi, atr and the rest of the 143 builtins, computed
18
+ with fixed, conformance-locked semantics. Same inputs, same bits, on
19
+ every implementation.
20
+ 2. **Strategy logic.** Entries, exits, conditions, long/short. The part a
21
+ trader actually thinks about, written in a language built for exactly
22
+ that.
23
+ 3. **Position rules.** Sizing (including risk-based sizing off stop
24
+ distance), stops, targets, trailing stops, commission.
25
+ 4. **In backtest mode: the simulator.** Next-bar-open fills (an order
26
+ placed on the signal bar fills at the NEXT bar's open, never the signal
27
+ bar), bracket handling, and the full stats block. This is where "same
28
+ script, same numbers" is enforced.
29
+ 5. **In live mode: signals only.** The same logic evaluates on each new
30
+ bar and emits order INTENTS ("enter long, 2% risk, stop here"). Brue
31
+ never sends the order itself.
32
+
33
+ ## What Brue deliberately does NOT do
34
+
35
+ - It never fetches data. Bars come in from the host (pandas frame, csv,
36
+ a feed, a broker terminal).
37
+ - It never holds broker credentials.
38
+ - It never talks to a broker. Executing an intent is the host's job,
39
+ through whatever adapter the user connects with their own account.
40
+ - It never touches the filesystem or network from inside a script. A
41
+ strategy is pure logic over bars, which is what makes it safe to run in
42
+ a sandbox, in a browser, or on someone else's machine.
43
+
44
+ That boundary is the product: the strategy is a portable artifact. The
45
+ same script runs in the site editor, the cloud notebook, the desktop
46
+ terminal, a Python session via `brue.backtest(script, df)`, and (once the
47
+ live driver ships) against any connected broker, and every one of those
48
+ produces bit-identical decisions because they are all the same
49
+ conformance-tested engine. The pipe around the strategy is swappable; the
50
+ strategy is yours.
51
+
52
+ ## How the pieces line up in Python
53
+
54
+ ```python
55
+ import pandas as pd # data: pandas' job
56
+ import brue # decisions: Brue's job
57
+
58
+ df = pd.read_csv("EURJPY_1h.csv")
59
+ result = brue.backtest(SCRIPT, df, extended_stats=True)
60
+ result.stats # dict of the canonical stats block
61
+ result.trades_frame() # trade ledger as a DataFrame
62
+ result.equity_frame() # per-bar equity with timestamps
63
+ ```
64
+
65
+ The engine core under this API is a 1:1 port verified byte-for-byte
66
+ against the reference implementation over the full conformance corpus
67
+ (see PORTING.md for the rules and the verification ladder).
68
+
69
+ ## What still needs to be built (in order)
70
+
71
+ 1. **Package release.** Publish to PyPI so `pip install` works outside
72
+ this machine. (Name decision pending.)
73
+ 2. **Live driver.** The streaming counterpart of `backtest()`: feed bars
74
+ one at a time, get order intents out through a callback, with
75
+ semantics provably identical to the backtest of the same window.
76
+ 3. **Broker adapters.** A thin adapter contract that turns intents into
77
+ orders on the user's own account, paper-trading adapter first, then
78
+ real ones. Credentials always stay on the user's machine.
79
+ 4. **Host integrations.** The desktop terminal and notebook surfaces move
80
+ onto this engine so every surface runs the same code.
81
+ 5. **Spec + public conformance suite.** The rulebook and test corpus
82
+ published so third-party tooling can target the language without
83
+ semantic drift.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: bruedemo
3
+ Version: 0.1.0
4
+ Summary: Internal test build of the Brue trading-strategy language engine. Will be republished under its final name; do not depend on this package.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # bruedemo
10
+
11
+ Internal TEST build of the Brue trading-strategy language engine
12
+ (pure Python). Published for real-world install testing by the
13
+ London Strategic Edge team only. The engine will be republished under
14
+ its final name; do not depend on this package.
15
+
16
+ ```python
17
+ import brue
18
+
19
+ result = brue.backtest(script, dataframe)
20
+ ```
@@ -0,0 +1,37 @@
1
+ # brue-py: 1:1 Python port of brue-rust (owner decision 2026-07-21: the
2
+ # shipping Brue engine is PYTHON; the Rust tree becomes the porting source).
3
+
4
+ RULES FOR EVERY PORTED MODULE
5
+ - Source of truth: /opt/lse-worker/brue-rust/src/<name>.rs. Port line by
6
+ line; keep the SAME module name (value.rs -> brue/value.py) and the SAME
7
+ function/struct names (Rust is snake_case already; structs become
8
+ dataclasses or plain classes with identical field names).
9
+ - Semantics are the product. Preserve every documented quirk (na == na is
10
+ true, and/or evaluate both sides, division by zero -> na, compound-assign
11
+ coercion order, EMA SMA-seeding, float accumulation ORDER in indicators,
12
+ reference equality for colors/arrays/maps, byte-identical error messages
13
+ including "did you mean" tie-breaks).
14
+ - Numbers: Python floats are IEEE754 doubles like Rust f64; keep operation
15
+ order identical. Never "simplify" running sums into vectorized forms.
16
+ - jsmath/jsnum are V8-exact; port the fdlibm bit-twiddling as-is using
17
+ struct.pack/unpack for bit casts.
18
+ - No third-party imports in the engine (stdlib only). numpy is BANNED in
19
+ engine modules (accumulation-order parity).
20
+ - Comments: keep the Rust file's explanatory comments where they explain
21
+ WHY. Never use em dashes or en dashes anywhere.
22
+ - Cross-module imports: from . import jsmath (etc.); keep function names so
23
+ parallel porting cannot desync interfaces.
24
+ - Every module ends with nothing else: no __main__ blocks except cli.py.
25
+
26
+ LAYOUT
27
+ brue/value.py errors.py jsnum.py jsmath.py tokenizer.py ast.py parser.py
28
+ indicators.py session.py data.py output.py builtins.py runtime.py
29
+ strategy.py quant.py cli.py __init__.py (__main__.py -> cli.main)
30
+
31
+ VERIFICATION LADDER
32
+ 1. Module-level checks where possible (jsmath vs node mathcheck samples,
33
+ tokenizer/parser over the conformance corpus).
34
+ 2. Full conformance: conformance/run_all_py.sh compares `python -m brue run`
35
+ output against the RUST binary (itself 138/138 vs the TS reference),
36
+ NaN/Inf-exact, over corpus x generated datasets. The port is DONE only
37
+ when every case matches byte-for-byte.
@@ -0,0 +1,12 @@
1
+ # bruedemo
2
+
3
+ Internal TEST build of the Brue trading-strategy language engine
4
+ (pure Python). Published for real-world install testing by the
5
+ London Strategic Edge team only. The engine will be republished under
6
+ its final name; do not depend on this package.
7
+
8
+ ```python
9
+ import brue
10
+
11
+ result = brue.backtest(script, dataframe)
12
+ ```
@@ -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
+
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -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])