dmoera 0.2.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.
- dmoera/__init__.py +13 -0
- dmoera/cli.py +378 -0
- dmoera/data.py +369 -0
- dmoera/engine.py +1191 -0
- dmoera/features.py +314 -0
- dmoera/grade/__init__.py +15 -0
- dmoera/grade/audit.py +390 -0
- dmoera/grade/cli.py +117 -0
- dmoera/grade/demo.py +62 -0
- dmoera/grade/keycheck.py +83 -0
- dmoera/grade/parsers.py +324 -0
- dmoera/grade/scale.py +30 -0
- dmoera/strategy_contract.py +241 -0
- dmoera-0.2.0.dist-info/METADATA +113 -0
- dmoera-0.2.0.dist-info/RECORD +21 -0
- dmoera-0.2.0.dist-info/WHEEL +5 -0
- dmoera-0.2.0.dist-info/entry_points.txt +2 -0
- dmoera-0.2.0.dist-info/licenses/LICENSE +21 -0
- dmoera-0.2.0.dist-info/top_level.txt +2 -0
- domains/__init__.py +9 -0
- domains/strategy_contract.py +4 -0
dmoera/__init__.py
ADDED
|
@@ -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
|
dmoera/cli.py
ADDED
|
@@ -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())
|