# 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))