dmoera 0.2.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.
dmoera-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CacheCarti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
dmoera-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: dmoera
3
+ Version: 0.2.0
4
+ Summary: Local backtesting for dMoERA strategies: run every bot in the Crypto-Strategies corpus on your own machine with the exact validation engine, then grade and validate the result.
5
+ Author: CacheCarti
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/CacheCarti/dmoera-cli
8
+ Project-URL: Repository, https://github.com/CacheCarti/dmoera-cli
9
+ Project-URL: Documentation, https://github.com/CacheCarti/Crypto-Strategies
10
+ Keywords: algorithmic-trading,backtesting,crypto,binance,trading-strategies,quant,overfitting,deflated-sharpe
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Office/Business :: Financial :: Investment
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.24
21
+ Dynamic: license-file
22
+
23
+ # dmoera
24
+
25
+ This is the engine behind [Crypto-Strategies](https://github.com/CacheCarti/Crypto-Strategies),
26
+ packaged so you can run it yourself. Every bot in that corpus, the winners
27
+ and the 700-odd corpses, was tested with the same simulation code you get
28
+ when you install this. Same bar loop, same exit logic, same friction model,
29
+ same 1000-bar context cap.
30
+
31
+ ```
32
+ pip install dmoera
33
+ dmoera backtest strategies/weekend_season_eth_v6.py
34
+ ```
35
+
36
+ The first run downloads candles once from Binance's public data and caches
37
+ them in `~/.dmoera/cache`. After that everything runs offline and instant.
38
+ If Binance is unreachable from your network for any reason, point `--csv`
39
+ at any OHLCV file and you're still in business.
40
+
41
+ ## The commands
42
+
43
+ **`dmoera backtest bot.py`** runs a bot over real candles and tells you what
44
+ it did: trades, net return after friction, win rate, significance, drawdown.
45
+ Add `--export-trades trades.csv` and you get the full trade list as a file.
46
+
47
+ **`dmoera grade trades.csv`** is the lie detector. It takes any trade log,
48
+ yours or one of ours, and grades it S through F after realistic fees. It
49
+ splits your results by market regime, runs 500 random-entry placebo bots
50
+ against yours, and computes a deflated Sharpe so a lucky streak can't
51
+ pretend to be skill. It reads TradingView, Freqtrade and Binance exports
52
+ too, so you can bring results from other tools and still get a straight
53
+ answer.
54
+
55
+ **`dmoera validate bot.py`** runs the 7 open validation stages on a bot:
56
+ code safety, in-sample, out-of-sample, rolling walk-forward, randomized
57
+ starts, slippage stress, and a holdout slice nothing is allowed to touch.
58
+ These use public default thresholds. Passing here means your bot isn't
59
+ obviously broken. It is not the real exam.
60
+
61
+ **`dmoera certify bot.py`** is the real exam. It uploads your code to
62
+ dMoERA's servers, where the sealed version runs: the actual thresholds, the
63
+ sealed holdout data, the attempt ledger that catches brute-forcing. You
64
+ need an API key from dmoera.xyz Settings for this one, because it costs
65
+ real compute. The verdict that comes back is bound to your code's hash, so
66
+ you can put it in your README and anyone can verify it later.
67
+
68
+ The short version: `backtest` tells you what a bot did. `grade` tells you
69
+ whether to believe it. `validate` tells you whether it holds up across
70
+ windows it didn't pick. `certify` is the only result that counts.
71
+
72
+ ## Reproducing the corpus numbers
73
+
74
+ Every report card in the Crypto-Strategies repo came out of this engine.
75
+ To check a card's claim yourself:
76
+
77
+ ```
78
+ dmoera backtest strategies/<bot>.py --interval 5m --days 180
79
+ dmoera backtest strategies/<bot>.py --interval 5m --days 180 --export-trades t.csv
80
+ dmoera grade t.csv
81
+ ```
82
+
83
+ Cross-asset strategies that read `ctx.features` work locally too. Open
84
+ interest and positioning come from Binance's public metrics archive, and
85
+ reference-asset returns (the `btc_return_*` family) are computed from the
86
+ reference asset's own candles. If a feature can't be served locally, the
87
+ run tells you which keys fell back to defaults instead of failing silently.
88
+
89
+ ## Why the numbers here mean something
90
+
91
+ Most local backtesters drift from whatever runs in production. Different
92
+ fee handling, different exit logic, different context windows, and one day
93
+ you notice your local results and the platform's results disagree. This
94
+ package doesn't have that problem because the simulation engine was
95
+ extracted verbatim from the dMoERA validation harness, the same code that
96
+ produced every report card in the corpus. A number here is the same number
97
+ there.
98
+
99
+ What the package deliberately leaves out is the sealed exam. The real
100
+ thresholds, the holdout data, the seeds and the attempt ledger stay on
101
+ dMoERA's servers. That is what makes `certify` worth something. The public
102
+ stages tell you your bot isn't obviously broken. Only the sealed exam can
103
+ tell you it's real.
104
+
105
+ ## For AI agents
106
+
107
+ The [dmoera-mcp](https://github.com/CacheCarti/dmoera-mcp) server exposes
108
+ the platform to AI agents, and it uses the same API key this package uses
109
+ for `certify`. One key, both tools.
110
+
111
+ ## License
112
+
113
+ MIT
dmoera-0.2.0/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # dmoera
2
+
3
+ This is the engine behind [Crypto-Strategies](https://github.com/CacheCarti/Crypto-Strategies),
4
+ packaged so you can run it yourself. Every bot in that corpus, the winners
5
+ and the 700-odd corpses, was tested with the same simulation code you get
6
+ when you install this. Same bar loop, same exit logic, same friction model,
7
+ same 1000-bar context cap.
8
+
9
+ ```
10
+ pip install dmoera
11
+ dmoera backtest strategies/weekend_season_eth_v6.py
12
+ ```
13
+
14
+ The first run downloads candles once from Binance's public data and caches
15
+ them in `~/.dmoera/cache`. After that everything runs offline and instant.
16
+ If Binance is unreachable from your network for any reason, point `--csv`
17
+ at any OHLCV file and you're still in business.
18
+
19
+ ## The commands
20
+
21
+ **`dmoera backtest bot.py`** runs a bot over real candles and tells you what
22
+ it did: trades, net return after friction, win rate, significance, drawdown.
23
+ Add `--export-trades trades.csv` and you get the full trade list as a file.
24
+
25
+ **`dmoera grade trades.csv`** is the lie detector. It takes any trade log,
26
+ yours or one of ours, and grades it S through F after realistic fees. It
27
+ splits your results by market regime, runs 500 random-entry placebo bots
28
+ against yours, and computes a deflated Sharpe so a lucky streak can't
29
+ pretend to be skill. It reads TradingView, Freqtrade and Binance exports
30
+ too, so you can bring results from other tools and still get a straight
31
+ answer.
32
+
33
+ **`dmoera validate bot.py`** runs the 7 open validation stages on a bot:
34
+ code safety, in-sample, out-of-sample, rolling walk-forward, randomized
35
+ starts, slippage stress, and a holdout slice nothing is allowed to touch.
36
+ These use public default thresholds. Passing here means your bot isn't
37
+ obviously broken. It is not the real exam.
38
+
39
+ **`dmoera certify bot.py`** is the real exam. It uploads your code to
40
+ dMoERA's servers, where the sealed version runs: the actual thresholds, the
41
+ sealed holdout data, the attempt ledger that catches brute-forcing. You
42
+ need an API key from dmoera.xyz Settings for this one, because it costs
43
+ real compute. The verdict that comes back is bound to your code's hash, so
44
+ you can put it in your README and anyone can verify it later.
45
+
46
+ The short version: `backtest` tells you what a bot did. `grade` tells you
47
+ whether to believe it. `validate` tells you whether it holds up across
48
+ windows it didn't pick. `certify` is the only result that counts.
49
+
50
+ ## Reproducing the corpus numbers
51
+
52
+ Every report card in the Crypto-Strategies repo came out of this engine.
53
+ To check a card's claim yourself:
54
+
55
+ ```
56
+ dmoera backtest strategies/<bot>.py --interval 5m --days 180
57
+ dmoera backtest strategies/<bot>.py --interval 5m --days 180 --export-trades t.csv
58
+ dmoera grade t.csv
59
+ ```
60
+
61
+ Cross-asset strategies that read `ctx.features` work locally too. Open
62
+ interest and positioning come from Binance's public metrics archive, and
63
+ reference-asset returns (the `btc_return_*` family) are computed from the
64
+ reference asset's own candles. If a feature can't be served locally, the
65
+ run tells you which keys fell back to defaults instead of failing silently.
66
+
67
+ ## Why the numbers here mean something
68
+
69
+ Most local backtesters drift from whatever runs in production. Different
70
+ fee handling, different exit logic, different context windows, and one day
71
+ you notice your local results and the platform's results disagree. This
72
+ package doesn't have that problem because the simulation engine was
73
+ extracted verbatim from the dMoERA validation harness, the same code that
74
+ produced every report card in the corpus. A number here is the same number
75
+ there.
76
+
77
+ What the package deliberately leaves out is the sealed exam. The real
78
+ thresholds, the holdout data, the seeds and the attempt ledger stay on
79
+ dMoERA's servers. That is what makes `certify` worth something. The public
80
+ stages tell you your bot isn't obviously broken. Only the sealed exam can
81
+ tell you it's real.
82
+
83
+ ## For AI agents
84
+
85
+ The [dmoera-mcp](https://github.com/CacheCarti/dmoera-mcp) server exposes
86
+ the platform to AI agents, and it uses the same API key this package uses
87
+ for `certify`. One key, both tools.
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,13 @@
1
+ """dmoera - local backtesting, grading and validation for dMoERA strategies.
2
+
3
+ Runs the exact simulation engine the dMoERA validation harness uses, on your
4
+ own machine, against Binance data you cache locally (or any OHLCV CSV you
5
+ bring). Every bot in the Crypto-Strategies corpus runs unmodified.
6
+ """
7
+ __version__ = "0.2.0"
8
+
9
+ from dmoera.strategy_contract import ( # noqa: F401
10
+ Strategy, BarContext, Signal, SignalDirection, Bar,
11
+ validate_metadata, CONTRACT_VERSION,
12
+ )
13
+ from dmoera.engine import simulate_strategy, SimTrade, load_strategy # noqa: F401
@@ -0,0 +1,378 @@
1
+ """dmoera command line interface.
2
+
3
+ Commands:
4
+ dmoera backtest bot.py [--symbol ETHUSDT] [--interval 5m] [--days 180]
5
+ [--csv FILE] [--friction 6.0] [--export-trades F]
6
+ dmoera grade trades.csv [--fee 12] [--trials 20] [--json]
7
+ dmoera validate bot.py [--days 180] (7 open stages, public thresholds)
8
+ dmoera certify bot.py (official verdict; needs DMOERA_API_KEY)
9
+ """
10
+ import argparse
11
+ import sys
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+
17
+ def _load_bot_file(path: str):
18
+ from dmoera.engine import load_strategy
19
+ code = Path(path).read_text(encoding="utf-8")
20
+ strategy_class, error = load_strategy(code)
21
+ if error:
22
+ print(f"[dmoera] failed to load {path}: {error}")
23
+ sys.exit(1)
24
+ return strategy_class
25
+
26
+
27
+ def _write_trades_csv(path: str, trades, symbol: str, interval: str) -> None:
28
+ """Export SimTrades in the simple format dmoera grade parses:
29
+ timestamp,side,entry_price,exit_price,size[,exit_time,symbol]"""
30
+ import csv as _csv
31
+ from datetime import timedelta
32
+ step = timedelta(seconds={"1m": 60, "3m": 180, "5m": 300, "15m": 900, "30m": 1800,
33
+ "1h": 3600, "2h": 7200, "4h": 14400, "1d": 86400}.get(interval, 300))
34
+ with open(path, "w", newline="") as f:
35
+ w = _csv.writer(f)
36
+ w.writerow(["timestamp", "side", "entry_price", "exit_price", "size",
37
+ "exit_time", "symbol"])
38
+ for t in trades:
39
+ try:
40
+ entry_dt = datetime.fromisoformat(str(t.timestamp).replace("Z", ""))
41
+ except ValueError:
42
+ continue
43
+ exit_dt = entry_dt + step * max(1, t.hold_bars)
44
+ w.writerow([entry_dt.strftime("%Y-%m-%d %H:%M:%S"),
45
+ "long" if t.direction == "long" else "short",
46
+ t.entry_price, t.exit_price, 1,
47
+ exit_dt.strftime("%Y-%m-%d %H:%M:%S"), symbol])
48
+
49
+
50
+ def _resolve_symbol(args) -> str:
51
+ meta = getattr(getattr(args, "_strategy_class", None), "METADATA", {}) or {}
52
+ if args.symbol:
53
+ return args.symbol.upper()
54
+ parts = meta.get("domain", "eth_usdc").split("_")
55
+ quote = "USDC" if len(parts) > 1 and parts[1].lower() == "usdc" else "USDT"
56
+ return parts[0].upper() + quote
57
+
58
+
59
+ def _maybe_features(strategy_class, code: str, symbol: str, interval: str,
60
+ candles, days: int) -> Optional[list]:
61
+ """Build the local feature series if the bot reads ctx.features."""
62
+ from dmoera.features import build_feature_series, used_feature_keys
63
+ keys = used_feature_keys(code)
64
+ if keys is not None and not keys:
65
+ return None
66
+ print("[dmoera] bot reads ctx.features - building local feature series ...")
67
+ return build_feature_series(candles, code, symbol, interval)
68
+
69
+
70
+ def cmd_backtest(args) -> int:
71
+ from dmoera import data as d
72
+ from dmoera.engine import simulate_strategy
73
+
74
+ strategy_class = _load_bot_file(args.file)
75
+ meta = getattr(strategy_class, "METADATA", {}) or {}
76
+ if args.symbol:
77
+ symbol = args.symbol.upper()
78
+ else:
79
+ # domain like "eth_usdc" / "btc_usdt" -> ETHUSDC / BTCUSDT
80
+ parts = meta.get("domain", "eth_usdc").split("_")
81
+ quote = "USDC" if len(parts) > 1 and parts[1].lower() == "usdc" else "USDT"
82
+ symbol = parts[0].upper() + quote
83
+ warmup = int(meta.get("warmup_bars", 0) or 0)
84
+
85
+ print(f"[dmoera] loading candles: {symbol} {args.interval} last {args.days}d ...")
86
+ candles = d.load_candles(symbol, interval=args.interval, days=args.days, csv_path=args.csv)
87
+ if len(candles) < warmup + 10:
88
+ print(f"[dmoera] not enough candles ({len(candles)}) for warmup {warmup}. "
89
+ f"Use --days or --csv to provide more data.")
90
+ return 1
91
+ print(f"[dmoera] {len(candles)} candles "
92
+ f"({candles[0]['timestamp'].date()} -> {candles[-1]['timestamp'].date()})")
93
+
94
+ code = Path(args.file).read_text(encoding="utf-8")
95
+ feats = _maybe_features(strategy_class, code, symbol, args.interval, candles, args.days)
96
+
97
+ strategy = strategy_class()
98
+ trades, metrics = simulate_strategy(
99
+ strategy, candles, symbol=symbol,
100
+ friction_bps=args.friction, warmup_bars=warmup,
101
+ features_series=feats,
102
+ )
103
+ if "error" in metrics:
104
+ print(f"[dmoera] backtest error: {metrics['error']}")
105
+ return 1
106
+
107
+ if args.export_trades:
108
+ _write_trades_csv(args.export_trades, trades, symbol, args.interval)
109
+ print(f"[dmoera] {len(trades)} trades exported -> {args.export_trades} "
110
+ f"(grade them: dmoera grade {args.export_trades})")
111
+
112
+ print()
113
+ print(f" strategy: {meta.get('name', strategy_class.__name__)}")
114
+ print(f" domain: {meta.get('domain', '?')} ({symbol} {args.interval})")
115
+ print(f" trades: {metrics.get('total_trades', 0)}")
116
+ print(f" net return: {metrics.get('total_return_bps', 0.0):+.1f} bps "
117
+ f"(after ~{args.friction * 2:.0f} bps round-trip friction)")
118
+ print(f" win rate: {metrics.get('win_rate', 0.0) * 100:.1f}%")
119
+ print(f" significance: {metrics.get('sharpe', 0.0):.2f} "
120
+ f"(t-stat on {metrics.get('total_trades', 0)} trades)")
121
+ print(f" max drawdown: {metrics.get('max_drawdown_bps', 0.0):.1f} bps")
122
+ print(f" final equity: {metrics.get('final_equity', 100.0):.2f} "
123
+ f"(start {100.0})")
124
+ print()
125
+ print(" Same engine, same friction, same 1000-bar cap as the dMoERA")
126
+ print(" validation harness. A number here means the same thing it")
127
+ print(" means there. Run `dmoera validate` for the 7-stage battery,")
128
+ print(" or `dmoera certify` for the official sealed verdict.")
129
+ return 0
130
+
131
+
132
+ def cmd_grade(args) -> int:
133
+ from dmoera.grade import cli as grade_cli
134
+ argv = [args.file, "--fee", str(args.fee), "--trials", str(args.trials)]
135
+ if args.json:
136
+ argv.append("--json")
137
+ return grade_cli.main(argv)
138
+
139
+
140
+ # ── validate: the 7 open stages, public default thresholds ────────────────
141
+ # These thresholds are PUBLISHED approximations, not the sealed exam's bar.
142
+ # Passing here means "not obviously broken", nothing more.
143
+
144
+ STAGE_SPLIT = 0.6 # in-sample = first 60%, OOS = last 40%
145
+ HOLDOUT_FRAC = 0.15 # last 15% is the holdout slice
146
+ WF_WINDOWS = 5 # rolling walk-forward windows
147
+ RANDOM_STARTS = 5 # randomized-start runs
148
+ STRESS_MULT = 2.0 # slippage stress: friction x2
149
+
150
+
151
+ def _sim_slice(strategy_class, candles, symbol, warmup, lo, hi, friction, feats=None):
152
+ from dmoera.engine import simulate_strategy
153
+ start = max(0, lo - warmup)
154
+ s = strategy_class()
155
+ f = feats[start:hi] if feats is not None else None
156
+ return simulate_strategy(s, candles[start:hi], symbol=symbol,
157
+ friction_bps=friction, warmup_bars=warmup,
158
+ features_series=f)
159
+
160
+
161
+ def _fmt_bps(x):
162
+ return f"{x:+.1f} bps" if x else "0.0 bps"
163
+
164
+
165
+ def cmd_validate(args) -> int:
166
+ from dmoera import data as d
167
+ from dmoera.engine import load_strategy, simulate_strategy
168
+
169
+ strategy_class = _load_bot_file(args.file)
170
+ meta = getattr(strategy_class, "METADATA", {}) or {}
171
+ if args.symbol:
172
+ symbol = args.symbol.upper()
173
+ else:
174
+ parts = meta.get("domain", "eth_usdc").split("_")
175
+ quote = "USDC" if len(parts) > 1 and parts[1].lower() == "usdc" else "USDT"
176
+ symbol = parts[0].upper() + quote
177
+ warmup = int(meta.get("warmup_bars", 0) or 0)
178
+ friction = args.friction
179
+
180
+ print(f"[dmoera] loading candles: {symbol} {args.interval} last {args.days}d ...")
181
+ candles = d.load_candles(symbol, interval=args.interval, days=args.days, csv_path=args.csv)
182
+ n = len(candles)
183
+ if n < warmup + 200:
184
+ print(f"[dmoera] not enough candles ({n}) for the battery. Need ~200+ after warmup.")
185
+ return 1
186
+ print(f"[dmoera] {n} candles ({candles[0]['timestamp'].date()} -> {candles[-1]['timestamp'].date()})")
187
+ print(f"[dmoera] running the 7 open stages (public thresholds, friction {friction} bps/side) ...\n")
188
+
189
+ code = Path(args.file).read_text(encoding="utf-8")
190
+ feats = _maybe_features(strategy_class, code, symbol, args.interval, candles, args.days)
191
+
192
+ holdout_start = int(n * (1 - HOLDOUT_FRAC))
193
+ oos_end = holdout_start
194
+ is_end = int(oos_end * STAGE_SPLIT)
195
+ results = []
196
+
197
+ def stage(name, ok, detail):
198
+ results.append((name, ok, detail))
199
+ print(f" [{'PASS' if ok else 'FAIL'}] {name}: {detail}")
200
+
201
+ # 1. code safety - load_strategy's static checks already ran in _load_bot_file
202
+ stage("1 code safety", True, "static checks passed (no network, no clock, no file IO)")
203
+
204
+ # 2. in-sample
205
+ _, m_is = _sim_slice(strategy_class, candles, symbol, warmup, 0, is_end, friction, feats)
206
+ is_bps = m_is.get("total_return_bps", 0.0)
207
+ stage("2 in-sample", m_is.get("total_trades", 0) > 0,
208
+ f"{m_is.get('total_trades', 0)} trades, {_fmt_bps(is_bps)}")
209
+
210
+ # 3. out-of-sample
211
+ _, m_oos = _sim_slice(strategy_class, candles, symbol, warmup, is_end, oos_end, friction, feats)
212
+ oos_bps = m_oos.get("total_return_bps", 0.0)
213
+ stage("3 out-of-sample", oos_bps > 0,
214
+ f"{m_oos.get('total_trades', 0)} trades, {_fmt_bps(oos_bps)} (bar: positive after friction)")
215
+
216
+ # 4. rolling walk-forward: WF_WINDOWS consecutive slices over the IS+OOS span
217
+ wf_rets = []
218
+ span = oos_end
219
+ wsize = max(warmup + 50, int(span * 0.25))
220
+ for k in range(WF_WINDOWS):
221
+ lo = int(k * (span - wsize) / max(1, WF_WINDOWS - 1))
222
+ _, m_w = _sim_slice(strategy_class, candles, symbol, warmup, lo, lo + wsize, friction, feats)
223
+ wf_rets.append(m_w.get("total_return_bps", 0.0))
224
+ wf_pos = sum(1 for r in wf_rets if r > 0)
225
+ stage("4 walk-forward", wf_pos >= WF_WINDOWS // 2 + 1,
226
+ f"{wf_pos}/{WF_WINDOWS} windows positive (bar: majority)")
227
+
228
+ # 5. randomized starts: short runs from random offsets (seeded, deterministic)
229
+ import random as _random
230
+ rng = _random.Random(20261109)
231
+ rs_rets = []
232
+ rsize = max(warmup + 50, int(n * 0.2))
233
+ for _ in range(RANDOM_STARTS):
234
+ lo = rng.randint(0, max(0, n - rsize - 1))
235
+ _, m_r = _sim_slice(strategy_class, candles, symbol, warmup, lo, lo + rsize, friction, feats)
236
+ rs_rets.append(m_r.get("total_return_bps", 0.0))
237
+ rs_med = sorted(rs_rets)[len(rs_rets) // 2]
238
+ stage("5 randomized starts", rs_med > 0,
239
+ f"median of {RANDOM_STARTS} random windows: {_fmt_bps(rs_med)}")
240
+
241
+ # 6. slippage stress: OOS again at 2x friction
242
+ _, m_st = _sim_slice(strategy_class, candles, symbol, warmup, is_end, oos_end, friction * STRESS_MULT, feats)
243
+ st_bps = m_st.get("total_return_bps", 0.0)
244
+ stage("6 slippage stress", st_bps > 0,
245
+ f"OOS at {friction * STRESS_MULT:.0f} bps/side: {_fmt_bps(st_bps)}")
246
+
247
+ # 7. holdout - reported, never tuned against
248
+ _, m_h = _sim_slice(strategy_class, candles, symbol, warmup, holdout_start, n, friction, feats)
249
+ h_bps = m_h.get("total_return_bps", 0.0)
250
+ stage("7 holdout", h_bps > 0,
251
+ f"last {n - holdout_start} bars: {_fmt_bps(h_bps)} (never used above)")
252
+
253
+ passed = sum(1 for _, ok, _ in results if ok)
254
+ print()
255
+ print(f" VALIDATION: {passed}/7 stages passed (public thresholds)")
256
+ if passed == 7:
257
+ print(" Not obviously broken. The REAL exam is sealed and server-side:")
258
+ print(" `dmoera certify` for the official verdict.")
259
+ else:
260
+ fails = [name for name, ok, _ in results if not ok]
261
+ print(f" Failed: {', '.join(fails)}")
262
+ print(" Fix these locally for free - certify counts attempts, so")
263
+ print(" don't burn server attempts on a bot that fails here.")
264
+ return 0 if passed == 7 else 1
265
+
266
+
267
+ def cmd_certify(args) -> int:
268
+ import hashlib
269
+ import json
270
+ import os
271
+ import urllib.request
272
+
273
+ api_key = args.api_key or os.environ.get("DMOERA_API_KEY")
274
+ if not api_key:
275
+ print("[dmoera] certify needs an API key (it runs the sealed exam on our servers,")
276
+ print(" which costs compute). Get one free at https://dmoera.xyz ->")
277
+ print(" Settings -> API keys, then: set DMOERA_API_KEY=dmo_pat_...")
278
+ return 1
279
+ server = (args.server or os.environ.get("DMOERA_SERVER") or "https://dmoera.xyz").rstrip("/")
280
+
281
+ code = Path(args.file).read_text(encoding="utf-8")
282
+ code_hash = hashlib.sha256(code.encode("utf-8")).hexdigest()
283
+ print(f"[dmoera] code hash: {code_hash[:16]}...")
284
+ print(f"[dmoera] uploading to {server} (sealed exam: real thresholds, holdout, attempt ledger) ...")
285
+
286
+ body = json.dumps({"code": code, "code_hash": code_hash}).encode()
287
+ req = urllib.request.Request(
288
+ f"{server}/api/package/certify", data=body, method="POST",
289
+ headers={"Content-Type": "application/json",
290
+ "Authorization": f"Bearer {api_key}",
291
+ "User-Agent": "dmoera-cli/0.2"})
292
+ try:
293
+ with urllib.request.urlopen(req, timeout=600) as r:
294
+ res = json.load(r)
295
+ except urllib.error.HTTPError as e:
296
+ try:
297
+ err = json.load(e)
298
+ except Exception:
299
+ err = {}
300
+ if e.code == 401:
301
+ print("[dmoera] unauthorized - that API key is not valid. Check DMOERA_API_KEY.")
302
+ elif e.code == 402:
303
+ print(f"[dmoera] {err.get('error', 'not enough credits')}")
304
+ elif e.code == 429:
305
+ print(f"[dmoera] rate limited: {err.get('error', 'try again later')}")
306
+ else:
307
+ print(f"[dmoera] certify failed (HTTP {e.code}): {err.get('error', e.reason)}")
308
+ return 1
309
+ except Exception as e:
310
+ print(f"[dmoera] could not reach {server}: {e}")
311
+ return 1
312
+
313
+ if not res.get("success"):
314
+ print(f"[dmoera] certify failed: {res.get('error', 'unknown error')}")
315
+ return 1
316
+ print()
317
+ print("+" + "-" * 62 + "+")
318
+ print("|" + f" OFFICIAL VERDICT {res.get('verdict', '?')} attempt #{res.get('attempt', '?')}".ljust(62) + "|")
319
+ print("+" + "-" * 62 + "+")
320
+ for k, label in (("oos_return_bps", "OOS return"), ("sharpe", "Sharpe"),
321
+ ("max_drawdown_bps", "Max drawdown"), ("total_trades", "Trades")):
322
+ if k in res:
323
+ print(f" {label:16s} {res[k]}")
324
+ if res.get("cert_id"):
325
+ print(f" cert: {res['cert_id']}")
326
+ print(f" verify: {server}/api/package/certify/verify/{res.get('code_hash', '')}")
327
+ print(f" Share the cert line + hash in your README - anyone can verify it.")
328
+ return 0
329
+
330
+
331
+ def main(argv=None) -> int:
332
+ p = argparse.ArgumentParser(prog="dmoera", description=__doc__,
333
+ formatter_class=argparse.RawDescriptionHelpFormatter)
334
+ sub = p.add_subparsers(dest="cmd", required=True)
335
+
336
+ bt = sub.add_parser("backtest", help="backtest a strategy file locally")
337
+ bt.add_argument("file", help="path to the strategy .py file")
338
+ bt.add_argument("--symbol", default=None, help="e.g. ETHUSDT (default: from METADATA.domain)")
339
+ bt.add_argument("--interval", default="5m",
340
+ choices=["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "1d"])
341
+ bt.add_argument("--days", type=int, default=180, help="history length (default 180)")
342
+ bt.add_argument("--csv", default=None, help="use this OHLCV CSV instead of fetching")
343
+ bt.add_argument("--friction", type=float, default=6.0,
344
+ help="per-side friction in bps (default 6.0)")
345
+ bt.add_argument("--export-trades", default=None,
346
+ help="write the trade list to this CSV (then: dmoera grade FILE)")
347
+ bt.set_defaults(func=cmd_backtest)
348
+
349
+ gr = sub.add_parser("grade", help="grade a trade log S-F after fees, regimes and luck")
350
+ gr.add_argument("file", help="trade log: our CSV, TradingView, Freqtrade JSON, Binance CSV")
351
+ gr.add_argument("--fee", type=float, default=12.0, help="round-trip fee bps (default 12)")
352
+ gr.add_argument("--trials", type=int, default=20,
353
+ help="how many versions you tried before this one (default 20)")
354
+ gr.add_argument("--json", action="store_true", help="print full JSON result")
355
+ gr.set_defaults(func=cmd_grade)
356
+
357
+ va = sub.add_parser("validate", help="run the 7 open validation stages on a bot (public thresholds)")
358
+ va.add_argument("file", help="path to the strategy .py file")
359
+ va.add_argument("--symbol", default=None)
360
+ va.add_argument("--interval", default="5m",
361
+ choices=["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "1d"])
362
+ va.add_argument("--days", type=int, default=180)
363
+ va.add_argument("--csv", default=None, help="use this OHLCV CSV instead of fetching")
364
+ va.add_argument("--friction", type=float, default=6.0)
365
+ va.set_defaults(func=cmd_validate)
366
+
367
+ ce = sub.add_parser("certify", help="official verdict from the dMoERA sealed exam (needs DMOERA_API_KEY)")
368
+ ce.add_argument("file", help="path to the strategy .py file")
369
+ ce.add_argument("--api-key", default=None, help="or set DMOERA_API_KEY env var")
370
+ ce.add_argument("--server", default=None, help="dMoERA API base (default https://dmoera.xyz)")
371
+ ce.set_defaults(func=cmd_certify)
372
+
373
+ args = p.parse_args(argv)
374
+ return args.func(args)
375
+
376
+
377
+ if __name__ == "__main__":
378
+ sys.exit(main())