69 lines
3.4 KiB
Python
# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
| |||
"""Property test of the sizing rules, on random symbols and setups (no terminal needed).
| |||
| |||
python3 tests/check_sizing.py [cases] default 200000, fixed seed
| |||
| |||
For every random case it checks: the money lost at the stop never exceeds the risk asked; it is within one lot step
| |||
of it (when a lot can be given); the lot is a whole number of lot steps, never below the minimum and never above the
| |||
maximum; a risk too small for the minimum lot gives 0 plus a note instead of a rounded-up lot."""
| |||
import random
| |||
import sys
| |||
from pathlib import Path
| |||
| |||
sys.path.insert(0, str(Path(__file__).parent))
| |||
from reference import Symbol, size, loss_per_lot, tick_price # noqa: E402
| |||
| |||
# (name, tick size, tick value per lot, price level, typical stop in ticks)
| |||
KINDS = [("fx5", 0.00001, 1.0, 1.1, 150), ("jpy3", 0.001, 0.65, 150.0, 150), ("gold2", 0.01, 1.0, 3000.0, 800),
| |||
("index1", 0.1, 0.1, 20000.0, 400), ("crypto2", 0.01, 0.01, 90000.0, 200000), ("fx3", 0.001, 1.0, 1.5, 100)]
| |||
STEPS = [(0.01, 0.01), (0.01, 0.1), (0.001, 0.001), (1.0, 1.0), (0.1, 0.1)] # (vol_step, vol_min)
| |||
| |||
| |||
def main(n):
| |||
rng = random.Random(20261021)
| |||
bad = []
| |||
stats = dict(sized=0, below_min=0, capped=0)
| |||
worst_gap = 0.0
| |||
for i in range(n):
| |||
name, ts, tv, lvl, stop_ticks = rng.choice(KINDS)
| |||
step, vmin = rng.choice(STEPS)
| |||
vmax = rng.choice([50.0, 100.0, 500.0])
| |||
sym = Symbol(ts, tv * rng.uniform(0.5, 1.5), step, vmin, vmax)
| |||
entry = lvl * rng.uniform(0.9, 1.1)
| |||
d = ts * max(2, rng.uniform(0.3, 3.0) * stop_ticks)
| |||
sl = entry - d if rng.random() < 0.5 else entry + d
| |||
risk = rng.choice([10, 25, 50, 100, 250, 1000, 5000]) * rng.uniform(0.2, 2.0)
| |||
lots, real, note = size(sym, entry, sl, risk)
| |||
lpl = loss_per_lot(sym, tick_price(sym, entry), tick_price(sym, sl))
| |||
if lots > 0:
| |||
stats["sized"] += 1
| |||
if lots * lpl > risk * (1 + 1e-9) and lots < vmax:
| |||
bad.append((i, "risk exceeded", name, lots, lots * lpl, risk))
| |||
if lots >= vmax - 1e-9:
| |||
stats["capped"] += 1
| |||
elif risk - lots * lpl >= step * lpl * (1 + 1e-9):
| |||
bad.append((i, "more than one lot step under the risk", name, lots, lots * lpl, risk))
| |||
else:
| |||
worst_gap = max(worst_gap, (risk - lots * lpl) / (step * lpl))
| |||
k = lots / step
| |||
if abs(k - round(k)) > 1e-6:
| |||
bad.append((i, "not a whole number of steps", name, lots, step))
| |||
if lots < vmin - 1e-12 or lots > vmax + 1e-12:
| |||
bad.append((i, "outside min/max", name, lots, vmin, vmax))
| |||
else:
| |||
stats["below_min"] += 1
| |||
if note != "min lot" or risk / lpl >= vmin + 1e-9:
| |||
bad.append((i, "zero lot without a good reason", name, lots, note, risk, lpl))
| |||
print(f"cases: {n}")
| |||
print(f"sized: {stats['sized']} (capped at the maximum lot: {stats['capped']})")
| |||
print(f"below the minimum lot (answered 0 + note): {stats['below_min']}")
| |||
print(f"largest shortfall under the risk, in lot steps: {worst_gap:.6f} (must stay under 1)")
| |||
print(f"violations: {len(bad)}")
| |||
for b in bad[:10]:
| |||
print(" ", b)
| |||
print("RESULT:", "PASS" if not bad else "FAIL")
| |||
return 0 if not bad else 1
| |||
| |||
| |||
if __name__ == "__main__":
| |||
sys.exit(main(int(sys.argv[1]) if len(sys.argv) > 1 else 200000))
|