risk-position-tool/tests/check_sizing.py
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))