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 +21 -0
- dmoera-0.2.0/PKG-INFO +113 -0
- dmoera-0.2.0/README.md +91 -0
- dmoera-0.2.0/dmoera/__init__.py +13 -0
- dmoera-0.2.0/dmoera/cli.py +378 -0
- dmoera-0.2.0/dmoera/data.py +369 -0
- dmoera-0.2.0/dmoera/engine.py +1191 -0
- dmoera-0.2.0/dmoera/features.py +314 -0
- dmoera-0.2.0/dmoera/grade/__init__.py +15 -0
- dmoera-0.2.0/dmoera/grade/audit.py +390 -0
- dmoera-0.2.0/dmoera/grade/cli.py +117 -0
- dmoera-0.2.0/dmoera/grade/demo.py +62 -0
- dmoera-0.2.0/dmoera/grade/keycheck.py +83 -0
- dmoera-0.2.0/dmoera/grade/parsers.py +324 -0
- dmoera-0.2.0/dmoera/grade/scale.py +30 -0
- dmoera-0.2.0/dmoera/strategy_contract.py +241 -0
- dmoera-0.2.0/dmoera.egg-info/PKG-INFO +113 -0
- dmoera-0.2.0/dmoera.egg-info/SOURCES.txt +24 -0
- dmoera-0.2.0/dmoera.egg-info/dependency_links.txt +1 -0
- dmoera-0.2.0/dmoera.egg-info/entry_points.txt +2 -0
- dmoera-0.2.0/dmoera.egg-info/requires.txt +1 -0
- dmoera-0.2.0/dmoera.egg-info/top_level.txt +2 -0
- dmoera-0.2.0/domains/__init__.py +9 -0
- dmoera-0.2.0/domains/strategy_contract.py +4 -0
- dmoera-0.2.0/pyproject.toml +33 -0
- dmoera-0.2.0/setup.cfg +4 -0
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())
|