Futures Position Sizer Overview Shapiro pipeline step 4: convert a direction, entry price, and stop-loss into a contract count, given an account risk budget and a verified contract spec (multiplier, tick size, tick value). This is a NEW, separate skill from -- futures contracts are leveraged, multiplier-based instruments with wildly different dollar-per-point values (a $0.25 move is $12.50 on ES but $5.00 on NQ and $31.25 on ZB); reusing the equity share-count sizer for futures would silently produce wrong position sizes. Two ways to size a trade: - Mode A (explicit) : supply directly. - Mode…

:>9} {'CCY':>4} {'EXCH':>6}\"\n print(header)\n print(\"-\" * len(header))\n for symbol in sorted(fs.CONTRACT_SPECS):\n row = fs.CONTRACT_SPECS[symbol]\n print(\n f\"{symbol:\u003c6} {str(row['exchange_product'])[:28]:\u003c28} {row['multiplier']:>10} \"\n f\"{row['tick_size']:>12} {row['tick_value']:>9} {row['currency']:>4} \"\n f\"{str(row['exchange']):>6}\"\n )\n\n\n# --- Main ----------------------------------------------------------------------\n\n\ndef main() -> int:\n args, parser = parse_arguments()\n\n if args.list_specs:\n print_spec_table()\n return 0\n\n if args.entry is None:\n parser.error(\"--entry is required\")\n if args.account_size is None:\n parser.error(\"--account-size is required\")\n\n mode_b = args.gate_json is not None\n if mode_b:\n if args.direction is not None or args.stop is not None:\n parser.error(\n \"--direction/--stop cannot be combined with --gate-json -- the gate \"\n \"report is authoritative for direction and stop when provided\"\n )\n else:\n if args.symbol is None:\n parser.error(\"--symbol is required in explicit mode (pass --gate-json for mode B)\")\n if args.direction is None or args.stop is None:\n parser.error(\"explicit mode requires both --direction and --stop\")\n\n as_of = args.as_of or date.today().isoformat()\n output_dir = Path(args.output_dir)\n\n if mode_b:\n raw_data, load_error = load_json_file(args.gate_json)\n gate = fs.normalize_gate_report(raw_data, load_error, symbol=args.symbol)\n if not gate.usable:\n resolved_symbol = (args.symbol or gate.symbol or \"UNKNOWN\").strip().upper()\n result = fs.build_gate_failure_result(\n symbol=resolved_symbol,\n entry=args.entry,\n reason=gate.reason,\n as_of=as_of,\n report_path=args.gate_json,\n setup_status=gate.setup_status,\n gate_confidence=gate.gate_confidence,\n warnings=gate.warnings,\n )\n _write_and_print(result, args, output_dir, as_of)\n return 0\n symbol = gate.symbol\n direction = gate.direction\n stop = gate.invalidation_level\n stop_source = \"gate\"\n gate_block: dict[str, Any] | None = {\n \"report_path\": args.gate_json,\n \"setup_status\": gate.setup_status,\n \"gate_confidence\": gate.gate_confidence,\n \"warnings\": list(gate.warnings),\n }\n else:\n symbol = args.symbol.strip().upper()\n direction = args.direction\n stop = args.stop\n stop_source = \"operator\"\n gate_block = None\n\n try:\n spec = fs.resolve_spec(\n symbol,\n multiplier=args.multiplier,\n tick_size=args.tick_size,\n contract_currency=args.contract_currency,\n )\n except fs.ConfigError as exc:\n print(f\"Error: {exc}\", file=sys.stderr)\n return 2\n\n if args.fx_rate is None:\n if fs.requires_fx_rate(spec[\"currency\"]):\n print(\n f\"Error: {symbol} is quoted in {spec['currency']}; --fx-rate is \"\n \"required to convert risk to USD\",\n file=sys.stderr,\n )\n return 2\n fx_rate = 1.0\n else:\n fx_rate = args.fx_rate\n\n max_contracts = args.max_contracts if args.max_contracts else None\n\n try:\n result = fs.size_futures_position(\n symbol=symbol,\n direction=direction,\n entry=args.entry,\n stop=stop,\n stop_source=stop_source,\n spec=spec,\n account_size=args.account_size,\n risk_pct=args.risk_pct,\n max_contracts=max_contracts,\n fx_rate=fx_rate,\n as_of=as_of,\n gate_block=gate_block,\n )\n except fs.ConfigError as exc:\n print(f\"Error: {exc}\", file=sys.stderr)\n return 2\n\n _write_and_print(result, args, output_dir, as_of)\n return 0\n\n\ndef _write_and_print(\n result: dict[str, Any], args: argparse.Namespace, output_dir: Path, as_of: str\n) -> None:\n symbol = result[\"symbol\"]\n if args.format in (\"json\", \"both\"):\n output_dir.mkdir(parents=True, exist_ok=True)\n json_path = output_dir / f\"futures_position_size_{symbol}_{as_of}.json\"\n generate_json_report(result, json_path)\n print(f\"JSON report: {json_path}\")\n if args.format in (\"text\", \"both\"):\n print(generate_text_report(result))\n else:\n status_line = f\"Status: {result['sizing_status']}\"\n if result.get(\"no_trade_reason\"):\n status_line += f\" ({result['no_trade_reason']})\"\n print(status_line)\n\n\nif __name__ == \"__main__\":\n try:\n sys.exit(main())\n except Exception as exc: # noqa: BLE001 - CLI should return a clear message, not a traceback.\n print(f\"Error: {exc}\", file=sys.stderr)\n sys.exit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":17504,"content_sha256":"c250b1523012049c72d3b32fc425b53b54ed8a274d09b0146b11b087374925ea"},{"filename":"scripts/futures_sizing.py","content":"#!/usr/bin/env python3\n\"\"\"\nFutures Position Sizer -- pure sizing logic.\n\nShapiro pipeline step 4: converts a direction + entry + stop into a contract\ncount, given an account risk budget. PURE: no file I/O, no network, no\nenvironment reads -- the CLI (`futures_position_sizer.py`) owns argument\nparsing and gate-report file loading, and calls into this module with\nalready-parsed primitives.\n\nThis module NEVER sizes a position without an explicit stop, and NEVER\nrounds a contract count up beyond the exact risk-budget/risk-per-contract\nquotient (floor only). Two distinct failure classes exist, matched to who\nsupplied the offending value (the same convention contrarian-setup-gate\nuses for its own untrusted-file handling):\n\n - A `ConfigError` (caught by the CLI, exit 2, no report written) is\n raised when the OPERATOR directly supplied a bad value: an invalid\n symbol override, a direction/stop geometry violation on the operator's\n own explicit --stop, a stop closer than one tick, or an off-tick-grid\n price on a bond-family contract (entry is ALWAYS operator-supplied,\n in both modes).\n - A `NO_TRADE` result dict (sizing_status=\"NO_TRADE\", exit 0, a report\n IS still written) is returned when the same class of problem is\n detected on a value that came from the untrusted gate-report file\n (mode B's stop = the gate's `invalidation_level`) -- blaming the CLI\n invocation would be misleading, and it would break the fail-closed\n \"always writes a report\" contract every other skill in this pipeline\n follows.\n\nContract-spec table: see CONTRACT_SPECS below and\n`references/futures-contract-specs.md` for the WebSearch-verified sourcing\nof every row (official exchange contractSpecs pages only).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport math\nimport re\nfrom dataclasses import dataclass\nfrom fractions import Fraction\nfrom typing import Any\n\nSCHEMA_VERSION = \"1.0\"\nSKILL_NAME = \"futures-position-sizer\"\n\n# Bond/note family: the ONLY fractional-notation (32nds/64ths) contracts\n# among the 23-symbol core table. A non-grid-aligned price on one of these\n# is almost certainly a notation mistype (typing \"110.16\" when the\n# intended price was \"110'16\" = 110 + 16/32 = 110.50) -- sizing it would\n# silently produce wrong money math, so it is a hard, fail-closed rejection\n# rather than a soft warning (unlike every other symbol, where an\n# off-grid/mid-quote price is legitimate and only warns).\nBOND_FAMILY = frozenset({\"ZT\", \"ZF\", \"ZN\", \"ZB\"})\n\n# The contract-count floor uses EXACT rational arithmetic (see\n# compute_contracts() below) -- NO epsilon of any kind. Two earlier\n# float-heuristic designs were both bugs, caught across two rounds of\n# independent user review:\n# (1) A RELATIVE epsilon (`floor(q * (1 + 1e-9))`, plan v3, review\n# round-2 P2): its absolute size grows with q, and at large q\n# (~1e8) it was large enough to swallow a GENUINE fractional\n# shortfall and round the contract count UP past the risk budget\n# (round-3 P1-1).\n# (2) An ABSOLUTE epsilon + a hard post-condition loop\n# (`while contracts * rpc > budget: contracts -= 1`, the round-3\n# P1-1 fix): at large `contracts`, float64 can no longer represent\n# the difference between `contracts` and `contracts - 1` when\n# multiplied by `risk_per_contract_usd` -- `(contracts - 1) * rpc`\n# equals `contracts * rpc` bit-for-bit -- so the loop's exit\n# condition never becomes true and it decrements toward zero one\n# at a time, which is not actually \"forever\" but is computationally\n# indistinguishable from a hang at the reported repro's scale\n# (~2.4e285 iterations) (round-3 follow-up, the user's own re-review\n# of the P1-1 fix).\n# `Fraction(finite_float)` converts a float64 to the EXACT rational number\n# it represents (every float64 is a dyadic rational -- an exact fraction\n# with a power-of-2 denominator -- so this conversion loses nothing); the\n# division and floor that follow are then exact rational operations with\n# no iteration and no representable-difference failure mode. This\n# terminates in O(1) regardless of scale, and\n# `contracts * risk_per_contract_usd \u003c= risk_budget_usd` holds BY\n# CONSTRUCTION (exact arithmetic), not by trusting any epsilon's sizing or\n# any loop's ability to actually converge.\n\n# Same relative-epsilon construction applied to the tick-grid / minimum-\n# stop-distance guards below, for the identical reason: tolerate float\n# representation noise around an exact grid point or an exact one-tick\n# distance, without ever masking a real off-grid price or a real\n# sub-one-tick stop.\nGRID_REL_EPSILON = 1e-9\n\nRISK_PCT_WARNING_THRESHOLD = 2.0\n\n# Sanity cap on the exact-floor contract count. The exact-rational floor\n# in compute_contracts() can no longer HANG (unlike the float-heuristic\n# designs it replaced) even on economically-absurd inputs (e.g. a\n# denormal-scale --multiplier override combined with a large\n# --account-size), but it can still return an economically meaningless\n# answer, like ~2.4e285 \"contracts\" -- there is no real futures account\n# that could ever hold anywhere near this many. Rejecting anything above\n# this bound turns nonsense input into a clean, immediate ConfigError\n# (exit 2) instead of a technically-well-formed but absurd SIZED report.\n# 1e12 has ~4 orders of magnitude of headroom over the largest legitimate\n# case this module's own test suite exercises (~1e8 contracts, a $1\n# trillion account against a cheap contract), while still being nowhere\n# near any real position size.\nCONTRACTS_SANITY_MAX = 1_000_000_000_000 # 1e12\n\n# A \"symbol\" -- whether from an untrusted gate-report file or an operator\n# CLI flag -- is allowed to become PART OF AN OUTPUT FILENAME\n# (futures_position_size_\u003cSYMBOL>_\u003cas-of>.json) and to be echoed verbatim\n# into JSON/text output. Every core-table and realistic override symbol is\n# 1-6 plain alphanumeric characters (ES, ZB, E6, ZZZZ, ...); this allowlist\n# is deliberately generous (up to 12 chars) while still rejecting the two\n# classes user review round 3 (P1-3) found reaching production code\n# unvalidated: whitespace-only (\" \", which `.strip()`s to an unusable\n# empty resolved symbol) and path-hostile characters (e.g. \"A/B\", which\n# would otherwise flow straight into a filename path component and either\n# crash with FileNotFoundError -- exit 1 -- or, worse, write outside the\n# intended output directory).\nSYMBOL_PATTERN = re.compile(r\"^[A-Za-z0-9]{1,12}$\")\n\n\ndef is_valid_symbol(value: str) -> bool:\n \"\"\"True if `value` is a safe, filename-safe, plain-alphanumeric\n symbol (1-12 characters). Callers must `.strip()` untrusted input\n BEFORE calling this -- whitespace is never itself part of a valid\n symbol, but stripping first lets \" B6 \" normalize to the valid \"B6\"\n rather than being rejected outright.\"\"\"\n return bool(SYMBOL_PATTERN.match(value))\n\n\n# WEBSEARCH-VERIFIED against official exchange sources (CME Group rulebook\n# chapters / contractSpecs pages, ICE, Cboe) -- see\n# references/futures-contract-specs.md for the full per-row sourcing notes,\n# including the two rows (ZT tick, QR identity) that needed the most\n# scrutiny and the three rows (A6/D6/S6) whose minimum price increment was\n# reduced by CME between 2016-2022 and is still misreported as the stale,\n# pre-reduction value ($10.00/$10.00/$12.50) by many third-party aggregators.\nVERIFIED_DATE = \"2026-07-17\"\n\n\ndef _spec(\n exchange_product: str,\n multiplier: float,\n tick_size: float,\n currency: str,\n exchange: str,\n source_url: str,\n) -> dict[str, Any]:\n return {\n \"exchange_product\": exchange_product,\n \"multiplier\": multiplier,\n \"tick_size\": tick_size,\n \"tick_value\": multiplier * tick_size,\n \"currency\": currency,\n \"exchange\": exchange,\n \"source_url\": source_url,\n \"verified_date\": VERIFIED_DATE,\n }\n\n\nCONTRACT_SPECS: dict[str, dict[str, Any]] = {\n \"ES\": _spec(\n \"E-mini S&P 500 Index futures\",\n 50,\n 0.25,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/rulebook/CME/IV/350/358/358.pdf\",\n ),\n \"NQ\": _spec(\n \"E-mini Nasdaq-100 Index futures\",\n 20,\n 0.25,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/rulebook/CME/IV/350/359/359.pdf\",\n ),\n \"YM\": _spec(\n \"E-mini Dow Jones Industrial Average ($5) Index futures\",\n 5,\n 1.0,\n \"USD\",\n \"CBOT\",\n \"https://www.cmegroup.com/rulebook/CBOT/III/27.pdf\",\n ),\n # QR = CME's E-mini Russell 2000 Index futures (current Globex ticker\n # RTY) -- NOT the discontinued ICE \"Russell 2000 Mini\" (legacy ticker\n # TF, $100 multiplier, delisted ~2017 when the Russell license moved\n # back to CME; using that multiplier would double the correct risk\n # figure). Confirmed live via FMP's COT data for symbol QR: every\n # recent weekly report's raw CFTC fields show\n # marketAndExchangeNames=\"RUSSELL E-MINI - CHICAGO MERCANTILE\n # EXCHANGE\", cftcContractMarketCode=\"239742\", contractUnits=\"(RUSSELL\n # 2000 INDEX X $50)\" -- an actively-traded ~400K-OI contract, unlike\n # the zero-OI legacy ICE product.\n \"QR\": _spec(\n \"E-mini Russell 2000 Index futures (Globex ticker RTY)\",\n 50,\n 0.10,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/rulebook/CME/III/300/393.pdf\",\n ),\n # VX outright/single-leg tick is 0.05 index points ($50.00/contract).\n # A finer 0.01-point ($10.00) tick applies ONLY to the individual legs\n # and net price of SPREAD trades, and a still-finer 0.005-point tick\n # applies only to TAS Block/ECRP order types -- neither is the right\n # tick for sizing a single-leg stop-loss, which is what this table is\n # for.\n \"VX\": _spec(\n \"Cboe Volatility Index (VIX) futures\",\n 1000,\n 0.05,\n \"USD\",\n \"CFE\",\n \"https://www.cboe.com/tradable-products/vix/vix-futures/specifications/\",\n ),\n # ZT's tick is disputed among third-party aggregators; resolved here\n # ONLY against CME Group's own Rulebook Chapter 21, which states\n # verbatim: \"The minimum price fluctuation shall be one-eighth of one\n # thirty-second of one point (equal to $7.8125 per contract).\" That is\n # 1/8 x 1/32 = 1/256 = 0.00390625 points -- NOT 1/4-of-1/32 (that\n # fraction is ZF's). ZT's $200,000 face value (2x ZF's $100,000) is\n # what lets its coarser fraction land on the same $7.8125 dollar tick\n # as ZF's finer one -- likely the source of aggregator confusion.\n \"ZT\": _spec(\n \"2-Year U.S. Treasury Note futures\",\n 2000,\n 0.00390625,\n \"USD\",\n \"CBOT\",\n \"https://www.cmegroup.com/rulebook/CBOT/II/21.pdf\",\n ),\n # ZF: 1/4-of-1/32 = 0.0078125 points, a fraction-of-a-fraction trap.\n \"ZF\": _spec(\n \"5-Year U.S. Treasury Note futures\",\n 1000,\n 0.0078125,\n \"USD\",\n \"CBOT\",\n \"https://www.cmegroup.com/rulebook/CBOT/II/20.pdf\",\n ),\n # ZN: 1/2-of-1/32 (= 1/64) = 0.015625 points.\n \"ZN\": _spec(\n \"10-Year U.S. Treasury Note futures\",\n 1000,\n 0.015625,\n \"USD\",\n \"CBOT\",\n \"https://www.cmegroup.com/rulebook/CBOT/II/19.pdf\",\n ),\n # ZB: 1/32 = 0.03125 points.\n \"ZB\": _spec(\n \"30-Year U.S. Treasury Bond futures\",\n 1000,\n 0.03125,\n \"USD\",\n \"CBOT\",\n \"https://www.cmegroup.com/rulebook/CBOT/V/18/18.pdf\",\n ),\n \"DX\": _spec(\n \"ICE U.S. Dollar Index futures\",\n 1000,\n 0.005,\n \"USD\",\n \"ICE\",\n \"https://www.ice.com/products/194/specs\",\n ),\n \"E6\": _spec(\n \"Euro FX futures (CME ticker 6E)\",\n 125_000,\n 0.00005,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/markets/fx/g10/euro-fx.contractSpecs.html\",\n ),\n \"J6\": _spec(\n \"Japanese Yen futures (CME ticker 6J)\",\n 12_500_000,\n 0.0000005,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/markets/fx/g10/japanese-yen.contractSpecs.html\",\n ),\n \"B6\": _spec(\n \"British Pound futures (CME ticker 6B)\",\n 62_500,\n 0.0001,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/markets/fx/g10/british-pound.contractSpecs.html\",\n ),\n # A6/D6/S6 (v3 correction): CME reduced the outright minimum price\n # increment on these three from 0.0001 to 0.00005 between 2016 and\n # 2022 -- many generic aggregator sites and default web-search\n # snippets still quote the stale, pre-reduction tick values\n # ($10.00/$10.00/$12.50). The CME 2023 FX Product Guide's own\n # per-symbol footnotes and cmegroup.com/trading/fx/mpi.html (the MPI\n # change-history page) confirm the CURRENT values used here.\n \"A6\": _spec(\n \"Australian Dollar futures (CME ticker 6A)\",\n 100_000,\n 0.00005,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/markets/fx/g10/australian-dollar.contractSpecs.html\",\n ),\n # D6 = CME 6C (Canadian Dollar) -- confirmed via CME's 2023 FX Product\n # Guide (CAD/USD futures page: Product Code 6C, contract size 100,000\n # CAD, quoted USD per CAD) and the \"D6\" vendor-symbol convention (D\n # for \"Dollar\"/CAD) used by data vendors that avoid a leading digit.\n \"D6\": _spec(\n \"Canadian Dollar futures (CME ticker 6C)\",\n 100_000,\n 0.00005,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/markets/fx/g10/canadian-dollar.contractSpecs.html\",\n ),\n \"S6\": _spec(\n \"Swiss Franc futures (CME ticker 6S)\",\n 125_000,\n 0.00005,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/markets/fx/g10/swiss-franc.contractSpecs.html\",\n ),\n \"GC\": _spec(\n \"Gold futures\",\n 100,\n 0.10,\n \"USD\",\n \"COMEX\",\n \"https://www.cmegroup.com/markets/metals/precious/gold.contractSpecs.html\",\n ),\n # SI outright tick is $0.005/oz ($25.00/contract). A finer\n # $0.001/oz ($5.00) tick applies only to spread/straddle trades and\n # settlement -- not the right tick for sizing a single-leg stop.\n \"SI\": _spec(\n \"Silver futures\",\n 5_000,\n 0.005,\n \"USD\",\n \"COMEX\",\n \"https://www.cmegroup.com/markets/metals/precious/silver.contractSpecs.html\",\n ),\n \"HG\": _spec(\n \"Copper futures\",\n 25_000,\n 0.0005,\n \"USD\",\n \"COMEX\",\n \"https://www.cmegroup.com/markets/metals/base/copper.contractSpecs.html\",\n ),\n \"PL\": _spec(\n \"Platinum futures\",\n 50,\n 0.10,\n \"USD\",\n \"NYMEX\",\n \"https://www.cmegroup.com/markets/metals/precious/platinum.contractSpecs.html\",\n ),\n \"CL\": _spec(\n \"WTI Crude Oil futures\",\n 1_000,\n 0.01,\n \"USD\",\n \"NYMEX\",\n \"https://www.cmegroup.com/markets/energy/crude-oil/light-sweet-crude.contractSpecs.html\",\n ),\n # NG outright tick is $0.001/MMBtu ($10.00/contract). A finer\n # $0.00025/MMBtu ($2.50) tick applies only to Globex inter-commodity\n # spreads -- not the right tick for a single-leg stop.\n \"NG\": _spec(\n \"Henry Hub Natural Gas futures\",\n 10_000,\n 0.001,\n \"USD\",\n \"NYMEX\",\n \"https://www.cmegroup.com/markets/energy/natural-gas/natural-gas.contractSpecs.html\",\n ),\n # BT: CME's own ticker for this contract is \"BTC\" (5 BTC/contract),\n # distinct from CME's Micro Bitcoin contract \"MBT\" (0.1 BTC/contract,\n # 50x smaller notional). \"BT\" is this project's COT/vendor-feed alias\n # for CME's BTC -- keyed as \"BT\" here to match\n # cot-contrarian-detector's CORE_SYMBOLS convention.\n \"BT\": _spec(\n \"Bitcoin futures (CME ticker BTC)\",\n 5,\n 5.00,\n \"USD\",\n \"CME\",\n \"https://www.cmegroup.com/rulebook/CME/IV/350/350.pdf\",\n ),\n}\n\nMARGIN_NOTE = (\n \"Exchange margin requirements are broker/time-dependent and NOT \"\n \"computed here; verify initial/maintenance margin with your broker.\"\n)\n\n\nclass ConfigError(Exception):\n \"\"\"An operator-caused configuration error: exit 2, no report written.\n\n Raised only for problems traceable to a value the OPERATOR supplied\n directly (CLI flags), never for a problem inside an untrusted gate\n report file -- those become a NO_TRADE result dict instead. See the\n module docstring's two-class convention.\n \"\"\"\n\n def __init__(self, message: str, *, reason: str):\n super().__init__(message)\n self.reason = reason\n\n\n# --- Numeric string validators (argparse-facing) ----------------------------\n\n\ndef strict_positive_float(value: str, *, max_value: float | None = None) -> float:\n \"\"\"Parse `value` as a finite, strictly-positive float.\n\n Rejects \"inf\"/\"-inf\"/\"nan\" (Python's `float()` accepts these as valid\n strings) and any literal that overflows to +/-inf on parse (e.g.\n \"1e309\" -- a syntactically ordinary-looking number). This is the\n strict validator every numeric CLI flag in this skill uses from day\n one -- position-sizer's plain `type=float` accepts \"inf\" and would\n silently produce an Infinity-valued JSON field (lesson from PR #249's\n ledger 10/10a/10b, explicitly called out as a trap to avoid here).\n Raises `ValueError` with a human-readable message on any rejection;\n the CLI wraps this in `argparse.ArgumentTypeError` so a bad flag is a\n parser-level usage error (exit 2), not a crash.\n \"\"\"\n try:\n parsed = float(value)\n except (TypeError, ValueError) as exc:\n raise ValueError(f\"{value!r} is not a valid number\") from exc\n if not math.isfinite(parsed):\n raise ValueError(f\"{value!r} must be finite (not inf/-inf/nan)\")\n if parsed \u003c= 0:\n raise ValueError(f\"{value!r} must be greater than 0\")\n if max_value is not None and parsed > max_value:\n raise ValueError(f\"{value!r} must be \u003c= {max_value}\")\n return parsed\n\n\ndef strict_nonneg_int(value: str) -> int:\n \"\"\"Parse `value` as an EXACT non-negative integer (used for\n --max-contracts, where 0 means \"no cap\" -- a HARD cap that must never\n be exceedable).\n\n Parses directly via `int()`, NEVER via `float()` (user review round 3,\n P2-4): float64 can only represent integers exactly up to 2**53\n (9,007,199,254,740,992) -- a value like 9007199254740995 would\n silently round-trip through `float()` to 9007199254740996, a\n different integer, making the \"hard cap\" off by one by construction.\n Python's `int()` has no such limit; it parses an integer-shaped string\n exactly regardless of magnitude. This also rejects \"inf\"/\"nan\" and any\n non-integral string (\"1.5\") -- `int()` itself raises `ValueError` on\n all of these (a decimal point, or \"inf\"/\"nan\", is never valid `int()`\n syntax), which is caught and re-raised with a clearer message.\"\"\"\n try:\n parsed_int = int(value)\n except ValueError as exc:\n raise ValueError(f\"{value!r} must be a whole number\") from exc\n if parsed_int \u003c 0:\n raise ValueError(f\"{value!r} must be >= 0\")\n return parsed_int\n\n\n# --- Whole-structure non-finite scan (defense-in-depth for the JSON writer)\n\n\ndef contains_non_finite(value: Any) -> bool:\n \"\"\"Iteratively check whether a structure contains any non-finite float\n (`inf`/`-inf`/`nan`) ANYWHERE, at any depth. Same iterative (not\n recursive) construction as contrarian-setup-gate's\n `_contains_non_finite` -- a legitimate structure can be nested deeply\n without limit and a recursive walker would raise `RecursionError`\n well before that on an entirely ordinary input. Used both by the CLI's\n hardened gate-json loader (copied verbatim from contrarian-setup-gate)\n and as a second, independent defense layer immediately before this\n module hands a result dict to the JSON writer's `allow_nan=False`.\n \"\"\"\n stack: list[Any] = [value]\n while stack:\n current = stack.pop()\n if isinstance(current, float):\n if not math.isfinite(current):\n return True\n elif isinstance(current, dict):\n stack.extend(current.values())\n elif isinstance(current, list):\n stack.extend(current)\n return False\n\n\n# --- Risk math ----------------------------------------------------------------\n\n\ndef geometry_ok(direction: str, entry: float, stop: float) -> bool:\n \"\"\"LONG requires stop \u003c entry; SHORT requires stop > entry. Equality\n or inversion is a violation in both directions -- there is no such\n thing as a zero-distance or wrong-side stop.\"\"\"\n if direction == \"LONG\":\n return stop \u003c entry\n if direction == \"SHORT\":\n return stop > entry\n raise ValueError(f\"unknown direction {direction!r}\") # pragma: no cover - CLI enforces the enum\n\n\ndef _tick_ratio_is_finite(price: float, tick_size: float) -> bool:\n \"\"\"True unless `price / tick_size` itself overflows to a non-finite\n value. Used by the bond-family off-grid check to distinguish a\n genuinely off-grid price from a price so extreme (an uncapped\n --entry/--stop) that the ratio computation overflowed -- the two need\n different ConfigError messages (see the bond-family off-grid block in\n `size_futures_position`).\"\"\"\n return math.isfinite(price / tick_size)\n\n\ndef _tick_ratio_and_nearest(distance: float, tick_size: float) -> tuple[float, int | None]:\n \"\"\"`nearest` is `None` when `ratio` itself overflowed to a non-finite\n value (an extreme, uncapped `--entry`/`--stop` divided by a small\n `tick_size` can do this even though both inputs are individually\n finite) -- `round()` on a non-finite float raises `OverflowError`, so\n callers must check for `None` instead of calling `round()` themselves.\"\"\"\n ratio = distance / tick_size\n if not math.isfinite(ratio):\n return ratio, None\n return ratio, round(ratio)\n\n\ndef is_on_tick_grid(price: float, tick_size: float, rel_eps: float = GRID_REL_EPSILON) -> bool:\n \"\"\"True if `price` lands on the tick grid (an integer number of ticks\n from 0), within a relative-epsilon tolerance that absorbs float64\n representation noise but not a genuine off-grid price. Used both for\n the bond-family hard off-grid rejection and the soft `off_tick_grid`\n warning on every other symbol.\n\n Never raises: a `price`/`tick_size` combination whose ratio overflows\n to a non-finite value is treated as maximally off-grid (`False`) rather\n than crashing -- the caller's own fail-closed handling (a bond-family\n ConfigError, or a warning for every other symbol) takes it from there;\n this function's only job is never to be the uncaught-exception source.\"\"\"\n ratio, nearest = _tick_ratio_and_nearest(price, tick_size)\n if nearest is None:\n return False\n if nearest == 0:\n # A price within one tick of zero is not a realistic futures\n # price for any symbol in this table, but guard the divide\n # against a zero denominator in the relative check below anyway.\n return math.isclose(ratio, 0.0, abs_tol=1e-6)\n return abs(ratio - nearest) \u003c= rel_eps * abs(nearest)\n\n\ndef stop_distance_ticks(entry: float, stop: float, tick_size: float) -> float:\n \"\"\"Exact (unrounded) tick count between entry and stop. Callers round\n this for display; the unrounded value is what the minimum-distance\n guard evaluates against.\"\"\"\n return abs(entry - stop) / tick_size\n\n\ndef meets_min_stop_distance(\n entry: float, stop: float, tick_size: float, rel_eps: float = GRID_REL_EPSILON\n) -> bool:\n \"\"\"True if the entry/stop distance is at least one tick. Relative-\n epsilon nudge (same construction as the floor algorithm below): a\n distance that is a hair under exactly one tick due to float noise\n still passes; a genuinely sub-one-tick distance still fails.\"\"\"\n ticks = stop_distance_ticks(entry, stop, tick_size)\n return ticks * (1 + rel_eps) >= 1.0\n\n\ndef _format_ticks(ticks: float) -> int | float:\n \"\"\"Render an exact whole-tick count as an int (matches the output\n contract's worked example: `stop_distance_ticks: 81`, not `81.0`);\n otherwise round to 2 decimals (off-grid symbols only warn, they are\n never forced onto the grid, so a fractional tick count is legitimate\n and reported at 2dp per plan).\n\n Never raises: `round(ticks)` (no ndigits -> converts to int) raises\n `OverflowError` on a non-finite `ticks` -- an extreme, uncapped\n `--entry`/`--stop` divided by a small `tick_size` can produce one even\n though both inputs are individually finite. `round(ticks, 2)` (WITH\n ndigits -> stays a float) never raises for a non-finite input, so the\n non-finite branch uses that form. The resulting non-finite value is\n always transient here: `size_futures_position`'s own\n `math.isfinite(risk_per_contract)` guard raises `ConfigError` before\n this result dict is ever written out, since the same extreme inputs\n that overflow the tick ratio also overflow the risk-per-contract\n product.\"\"\"\n if not math.isfinite(ticks):\n return round(ticks, 2)\n nearest = round(ticks)\n if abs(ticks - nearest) \u003c 1e-6:\n return int(nearest)\n return round(ticks, 2)\n\n\ndef compute_contracts(\n risk_budget_usd: float, risk_per_contract_usd: float, max_contracts: int | None = None\n) -> int:\n \"\"\"`floor(Fraction(risk_budget_usd) / Fraction(risk_per_contract_usd))`,\n then capped by `max_contracts` (None = uncapped) and rejected outright\n if economically implausible.\n\n `Fraction(finite_float)` converts a float64 to the EXACT rational\n number it represents (every float64 is a dyadic rational, so this is\n lossless); the division and floor that follow are then exact rational\n operations. This is the load-bearing correctness guarantee --\n `contracts * risk_per_contract_usd \u003c= risk_budget_usd` holds BY\n CONSTRUCTION -- replacing two earlier float-heuristic designs that\n were both bugs (see the module-level comment above this function):\n a relative epsilon that rounded UP past the budget at large scale, and\n an absolute epsilon + hard post-condition loop that could not\n terminate in practice at large scale (round-trip user review, round 3).\n\n NEVER rounds up beyond the exact quotient -- a true 2.5 stays 2. A\n \"true\" 3 * risk_per_contract that, in float64, evaluates to slightly\n MORE than a separately-specified budget (e.g. rpc = 0.1 * 3 =\n 0.30000000000000004, so 3 * rpc = 0.9000000000000001 > a 0.9 budget)\n money-safely floors to 2, not 3 -- the exact-rational quotient is what\n the money actually supports, not what an intuitive \"3 * rpc ~= budget\"\n read might suggest.\n\n Raises `ConfigError`:\n - `risk_per_contract_non_positive` if `risk_per_contract_usd` is\n exactly zero (division by zero) -- `size_futures_position` already\n guards this before calling here, but this function never crashes\n regardless of caller discipline.\n - `contracts_quotient_overflow` if either input is not finite --\n `Fraction()` itself raises `ValueError` (nan) or `OverflowError`\n (inf) constructing from a non-finite float; also already guarded\n upstream, same defense-in-depth rationale.\n - `contracts_implausibly_large` if the resulting contract count\n exceeds `CONTRACTS_SANITY_MAX` -- the exact-rational floor can no\n longer HANG on an economically-absurd input (e.g. a denormal-scale\n --multiplier override), but it can still return a technically\n correct, meaningless answer like ~2.4e285 \"contracts\"; this turns\n that into a clean, immediate error instead of a nonsensical SIZED\n report. Checked BEFORE `max_contracts` is applied, so a coincidental\n --max-contracts cap can never mask an absurd underlying input.\"\"\"\n try:\n q = Fraction(risk_budget_usd) / Fraction(risk_per_contract_usd)\n except ZeroDivisionError as exc:\n raise ConfigError(\n f\"risk_per_contract_usd is zero (risk_budget_usd={risk_budget_usd}) -- \"\n \"cannot compute a contract count\",\n reason=\"risk_per_contract_non_positive\",\n ) from exc\n except (ValueError, OverflowError) as exc:\n raise ConfigError(\n f\"risk_budget_usd or risk_per_contract_usd is not finite \"\n f\"(risk_budget_usd={risk_budget_usd}, \"\n f\"risk_per_contract_usd={risk_per_contract_usd})\",\n reason=\"contracts_quotient_overflow\",\n ) from exc\n contracts = math.floor(q)\n if contracts > CONTRACTS_SANITY_MAX:\n raise ConfigError(\n f\"computed contract count ({contracts}) is economically implausible \"\n f\"(risk_budget_usd={risk_budget_usd}, \"\n f\"risk_per_contract_usd={risk_per_contract_usd}) -- check \"\n \"--multiplier/--tick-size (or an unknown-symbol override) and the \"\n \"entry/stop distance for an unreasonably small risk_per_contract\",\n reason=\"contracts_implausibly_large\",\n )\n if max_contracts is not None and max_contracts > 0:\n contracts = min(contracts, max_contracts)\n return contracts\n\n\ndef requires_fx_rate(currency: str) -> bool:\n \"\"\"True when `currency` (a contract's quote currency) is not USD, and\n an explicit --fx-rate is therefore mandatory rather than defaulted.\"\"\"\n return currency.strip().upper() != \"USD\"\n\n\n# --- Contract-spec resolution ------------------------------------------------\n\n\ndef resolve_spec(\n symbol: str,\n *,\n specs: dict[str, dict[str, Any]] = CONTRACT_SPECS,\n multiplier: float | None = None,\n tick_size: float | None = None,\n contract_currency: str | None = None,\n) -> dict[str, Any]:\n \"\"\"Resolve `symbol` to a contract-spec dict, either from the verified\n `specs` table or from a fully-specified operator override for a\n symbol NOT in the table.\n\n Two fail-closed rules, both deliberately strict (documented judgment\n call, flagged for PR review like the margin-note interpretation):\n\n 1. A KNOWN symbol may never be silently overridden or silently left\n as-is when override flags are also given -- an override alongside\n a known symbol is treated as a config error rather than either\n guessing which one the operator meant or silently ignoring flags\n they explicitly typed.\n 2. An UNKNOWN symbol requires ALL THREE override flags together\n (--multiplier, --tick-size, --contract-currency) -- no partial\n override with an implicit default, since a silently-defaulted\n currency or multiplier on an unlisted contract is exactly the kind\n of silent-wrong-money-math this skill exists to prevent.\n \"\"\"\n symbol = symbol.strip().upper()\n override_given = any(v is not None for v in (multiplier, tick_size, contract_currency))\n\n if symbol in specs:\n if override_given:\n raise ConfigError(\n f\"{symbol} already has a verified contract spec in the table; \"\n \"remove --multiplier/--tick-size/--contract-currency, or use \"\n \"--list-specs to inspect the existing row\",\n reason=\"known_symbol_override_conflict\",\n )\n spec = dict(specs[symbol])\n spec[\"cot_symbol\"] = symbol\n return spec\n\n missing = [\n name\n for name, val in (\n (\"--multiplier\", multiplier),\n (\"--tick-size\", tick_size),\n (\"--contract-currency\", contract_currency),\n )\n if val is None\n ]\n if missing:\n raise ConfigError(\n f\"{symbol} is not in the verified spec table; provide all of \"\n f\"--multiplier, --tick-size, and --contract-currency together \"\n f\"(missing: {', '.join(missing)})\",\n reason=\"unknown_symbol_incomplete_override\",\n )\n\n currency = contract_currency.strip().upper()\n tick_value = multiplier * tick_size\n return {\n \"cot_symbol\": symbol,\n \"exchange_product\": None,\n \"multiplier\": multiplier,\n \"tick_size\": tick_size,\n \"tick_value\": tick_value,\n \"currency\": currency,\n \"exchange\": None,\n \"source_url\": None,\n \"verified_date\": None,\n }\n\n\ndef _spec_audit_dict(spec: dict[str, Any]) -> dict[str, Any]:\n exchange = spec.get(\"exchange\")\n return {\n \"multiplier\": spec[\"multiplier\"],\n \"tick_size\": spec[\"tick_size\"],\n \"tick_value\": spec[\"tick_value\"],\n \"currency\": spec[\"currency\"],\n \"source\": exchange.lower() if isinstance(exchange, str) else None,\n \"verified\": spec.get(\"verified_date\"),\n }\n\n\ndef _bond_off_grid_message(symbol: str, field_name: str, price: float, tick_size: float) -> str:\n return (\n f\"{symbol} {field_name} {price} is not on the {tick_size}-point tick grid. \"\n f\"Bond/note futures quote in 32nds/64ths-of-a-point notation shown with an \"\n f\"apostrophe -- e.g. 110'16 means 110 + 16/32 = 110.50 -- enter DECIMAL \"\n f\"points, not the raw digits after the apostrophe (110.16 is NOT 110'16).\"\n )\n\n\n# --- Top-level sizing orchestration ------------------------------------------\n\n\ndef _base_result(\n *,\n symbol: str,\n direction: str,\n entry: float,\n stop: float,\n spec: dict[str, Any],\n account_size: float,\n fx_rate: float,\n as_of: str,\n gate_block: dict[str, Any] | None,\n) -> dict[str, Any]:\n result: dict[str, Any] = {\n \"schema_version\": SCHEMA_VERSION,\n \"symbol\": symbol,\n \"direction\": direction,\n \"sizing_status\": None,\n \"no_trade_reason\": None,\n \"entry\": entry,\n \"stop\": stop,\n \"stop_distance_points\": None,\n \"stop_distance_ticks\": None,\n \"contract_spec\": _spec_audit_dict(spec),\n \"risk_per_contract_usd\": None,\n \"risk_budget_usd\": None,\n \"contracts\": 0,\n \"total_risk_usd\": None,\n \"risk_pct_of_account\": None,\n \"max_contracts_cap_applied\": False,\n \"fx_rate_used\": fx_rate,\n \"margin_note\": MARGIN_NOTE,\n \"warnings\": [],\n \"run_context\": {\n \"symbol\": symbol,\n \"as_of\": as_of,\n \"schema_version\": SCHEMA_VERSION,\n \"skill\": SKILL_NAME,\n },\n }\n if gate_block is not None:\n result[\"gate\"] = gate_block\n return result\n\n\ndef size_futures_position(\n *,\n symbol: str,\n direction: str,\n entry: float,\n stop: float,\n stop_source: str,\n spec: dict[str, Any],\n account_size: float,\n risk_pct: float,\n max_contracts: int | None,\n fx_rate: float,\n as_of: str,\n gate_block: dict[str, Any] | None = None,\n) -> dict[str, Any]:\n \"\"\"Compose the full sizing result (plan §2 output contract) from\n already-validated, already-normalized primitives.\n\n `stop_source` is `\"operator\"` (mode A: --stop is a direct CLI flag) or\n `\"gate\"` (mode B: --stop came from the gate report's\n `invalidation_level`) -- it selects which of the two fail-closed\n classes (ConfigError vs NO_TRADE) a geometry/off-grid/too-close\n violation resolves to. See module docstring.\n\n Raises `ConfigError` for every operator-caused violation. Returns a\n result dict (`sizing_status` \"SIZED\" or \"NO_TRADE\") otherwise --\n NEVER raises for a gate-caused violation or for the ordinary\n risk_below_one_contract outcome, both of which are legitimate,\n fail-closed answers, not errors.\n \"\"\"\n result = _base_result(\n symbol=symbol,\n direction=direction,\n entry=entry,\n stop=stop,\n spec=spec,\n account_size=account_size,\n fx_rate=fx_rate,\n as_of=as_of,\n gate_block=gate_block,\n )\n warnings: list[str] = result[\"warnings\"]\n\n def _no_trade(reason: str) -> dict[str, Any]:\n result[\"sizing_status\"] = \"NO_TRADE\"\n result[\"no_trade_reason\"] = reason\n return result\n\n # --- Geometry ---\n if not geometry_ok(direction, entry, stop):\n if stop_source == \"operator\":\n comparator = \"\u003c\" if direction == \"LONG\" else \">\"\n raise ConfigError(\n f\"{direction} requires stop {comparator} entry (entry={entry}, stop={stop})\",\n reason=\"direction_stop_mismatch\",\n )\n return _no_trade(\"entry_on_wrong_side_of_stop\")\n\n tick_size = spec[\"tick_size\"]\n is_bond = symbol in BOND_FAMILY\n\n # --- Off-grid handling: bond family is a hard, mode-aware rejection;\n # every other symbol only warns. Entry is ALWAYS operator-supplied in\n # both modes, so an off-grid entry is always a ConfigError.\n #\n # `_tick_ratio_is_finite` gates each bond off-grid check: an extreme\n # (>~1e300) --entry/--stop makes the price/tick_size ratio itself\n # overflow to a non-finite value, which `is_on_tick_grid` treats as\n # \"not on grid\" (False) -- correct on its own, but raising the 32nds-\n # notation ConfigError here would misattribute an overflow as a\n # notation mistake (code review round 3, P3). When the ratio has\n # overflowed, this deliberately skips straight past the bond-specific\n # check instead -- every bond-family multiplier (1000-2000) is large\n # enough relative to a tick_size in the 0.001-0.03 range that any\n # --entry/--stop extreme enough to overflow THIS ratio always also\n # overflows `risk_per_contract` a few lines below, where the correctly-\n # attributed overflow ConfigError is raised instead.\n if is_bond:\n if _tick_ratio_is_finite(entry, tick_size) and not is_on_tick_grid(entry, tick_size):\n raise ConfigError(\n _bond_off_grid_message(symbol, \"entry\", entry, tick_size),\n reason=\"entry_off_tick_grid\",\n )\n if _tick_ratio_is_finite(stop, tick_size) and not is_on_tick_grid(stop, tick_size):\n if stop_source == \"operator\":\n raise ConfigError(\n _bond_off_grid_message(symbol, \"stop\", stop, tick_size),\n reason=\"stop_off_tick_grid\",\n )\n return _no_trade(\"gate_stop_off_tick_grid\")\n else:\n if not is_on_tick_grid(entry, tick_size):\n warnings.append(\"off_tick_grid_entry\")\n if not is_on_tick_grid(stop, tick_size):\n warnings.append(\"off_tick_grid_stop\")\n\n # --- Minimum stop distance: independent of the (now epsilon-free,\n # exact-rational) contract-count floor below -- this guards against a\n # near-zero risk_per_contract implying an absurdly large contract\n # count from a genuinely too-close stop, which CONTRACTS_SANITY_MAX\n # would also catch downstream, but rejecting it here gives a clearer,\n # more specific reason (`stop_too_close` / `gate_stop_too_close`).\n if not meets_min_stop_distance(entry, stop, tick_size):\n if stop_source == \"operator\":\n raise ConfigError(\n f\"stop distance ({round(abs(entry - stop), 8)}) is less than one \"\n f\"tick ({tick_size}) for {symbol}\",\n reason=\"stop_too_close\",\n )\n return _no_trade(\"gate_stop_too_close\")\n\n # --- Risk math (price-space, exact; only rounded for display) ---\n distance = abs(entry - stop)\n ticks_exact = stop_distance_ticks(entry, stop, tick_size)\n result[\"stop_distance_points\"] = round(distance, 8)\n result[\"stop_distance_ticks\"] = _format_ticks(ticks_exact)\n\n # Defense-in-depth against float64 overflow AND underflow: every\n # operand here (entry, stop, multiplier, fx_rate, account_size,\n # risk_pct) already passed its own argparse-level max_value cap, but\n # the PRODUCT of several individually-valid values can still misbehave\n # two different ways:\n # - OVERFLOW: an extreme --account-size times a double-digit\n # --risk-pct (or similar) overflows to inf. An uncaught inf would\n # otherwise reach math.floor() inside compute_contracts() (raises\n # OverflowError) or the JSON writer's allow_nan=False (raises\n # ValueError).\n # - UNDERFLOW (user review round 3, P1-2): a DENORMAL-range\n # combination (e.g. entry/stop/multiplier all ~1e-308, individually\n # finite and positive, still pass the strict-positive validators)\n # multiplies down to exactly 0.0 rather than overflowing --\n # math.isfinite(0.0) is True, so the overflow check alone does NOT\n # catch this. An uncaught 0.0 divisor would otherwise reach\n # `risk_budget / risk_per_contract_usd` inside compute_contracts()\n # and raise ZeroDivisionError.\n # Both are uncaught crashes, exit 1, violating the two-class exit\n # contract (2 config / 0 fail-closed report -- never 1). Both\n # risk_per_contract and risk_budget are entirely operator-supplied in\n # both modes (never gate-sourced), so a ConfigError (exit 2, no\n # report) is the correct class here, same as resolve_spec's and\n # requires_fx_rate's own operator-side errors.\n risk_per_contract = distance * spec[\"multiplier\"] * fx_rate\n if not math.isfinite(risk_per_contract):\n raise ConfigError(\n f\"computed risk_per_contract is not finite (entry={entry}, stop={stop}, \"\n f\"stop_distance={distance}, multiplier={spec['multiplier']}, \"\n f\"fx_rate={fx_rate}) -- --entry/--stop (the distance between them), or \"\n \"--multiplier/--tick-size/--fx-rate (or an unknown-symbol override), \"\n \"is unreasonably large\",\n reason=\"risk_per_contract_overflow\",\n )\n if risk_per_contract \u003c= 0.0:\n raise ConfigError(\n f\"computed risk_per_contract is not positive (entry={entry}, stop={stop}, \"\n f\"stop_distance={distance}, multiplier={spec['multiplier']}, \"\n f\"fx_rate={fx_rate}) -- one or more of --entry/--stop (the distance \"\n \"between them), --multiplier/--tick-size/--fx-rate (or an \"\n \"unknown-symbol override) is too small, causing the product to \"\n \"underflow to zero\",\n reason=\"risk_per_contract_non_positive\",\n )\n result[\"risk_per_contract_usd\"] = round(risk_per_contract, 2)\n\n risk_budget = account_size * risk_pct / 100.0\n if not math.isfinite(risk_budget):\n raise ConfigError(\n f\"computed risk_budget is not finite (account_size={account_size}, \"\n f\"risk_pct={risk_pct}) -- --account-size is unreasonably large\",\n reason=\"risk_budget_overflow\",\n )\n result[\"risk_budget_usd\"] = round(risk_budget, 2)\n\n if risk_pct > RISK_PCT_WARNING_THRESHOLD:\n warnings.append(\"risk_pct_above_2\")\n\n cap = max_contracts if max_contracts and max_contracts > 0 else None\n pre_cap_contracts = compute_contracts(risk_budget, risk_per_contract, None)\n contracts = min(pre_cap_contracts, cap) if cap is not None else pre_cap_contracts\n result[\"contracts\"] = contracts\n result[\"max_contracts_cap_applied\"] = cap is not None and contracts \u003c pre_cap_contracts\n\n if contracts \u003c= 0:\n return _no_trade(\"risk_below_one_contract\")\n\n total_risk = contracts * risk_per_contract\n result[\"total_risk_usd\"] = round(total_risk, 2)\n result[\"risk_pct_of_account\"] = round(total_risk / account_size * 100.0, 2)\n result[\"sizing_status\"] = \"SIZED\"\n return result\n\n\ndef build_gate_failure_result(\n *,\n symbol: str,\n entry: float,\n reason: str,\n as_of: str,\n report_path: str,\n setup_status: str | None = None,\n gate_confidence: str | None = None,\n warnings: tuple[str, ...] = (),\n) -> dict[str, Any]:\n \"\"\"A minimal NO_TRADE result for a gate report that never yielded a\n usable stop -- unreadable/malformed/not-ready/symbol-mismatched. No\n risk math has run (there is no stop to run it against), so every\n downstream-computed field stays null, matching the output contract's\n \"only entry is known\" case.\"\"\"\n return {\n \"schema_version\": SCHEMA_VERSION,\n \"symbol\": symbol,\n \"direction\": None,\n \"sizing_status\": \"NO_TRADE\",\n \"no_trade_reason\": reason,\n \"entry\": entry,\n \"stop\": None,\n \"stop_distance_points\": None,\n \"stop_distance_ticks\": None,\n \"contract_spec\": None,\n \"risk_per_contract_usd\": None,\n \"risk_budget_usd\": None,\n \"contracts\": 0,\n \"total_risk_usd\": None,\n \"risk_pct_of_account\": None,\n \"max_contracts_cap_applied\": False,\n \"fx_rate_used\": None,\n \"margin_note\": MARGIN_NOTE,\n \"gate\": {\n \"report_path\": report_path,\n \"setup_status\": setup_status,\n \"gate_confidence\": gate_confidence,\n \"warnings\": list(warnings),\n },\n \"warnings\": [],\n \"run_context\": {\n \"symbol\": symbol,\n \"as_of\": as_of,\n \"schema_version\": SCHEMA_VERSION,\n \"skill\": SKILL_NAME,\n },\n }\n\n\n# --- Gate report normalization -----------------------------------------------\n\n\ndef _is_supported_schema(value: Any) -> bool:\n \"\"\"Accept schema major version \"1\" (e.g. \"1.0\", \"1.4\"); reject\n anything else, including missing/non-string/other-major values (fail\n closed). Same idiom as contrarian-setup-gate's own `_is_supported_schema`\n (gate_logic.py) -- reimplemented here, not imported, since skills in\n this repo are self-contained (no cross-skill Python dependency).\"\"\"\n if not isinstance(value, str) or not value:\n return False\n return value.split(\".\", 1)[0] == \"1\"\n\n\ndef _valid_finite_positive(value: Any) -> float | None:\n \"\"\"A usable `invalidation_level`: a finite, positive, non-bool number.\n `isinstance(True, int)` is `True` in Python, so bool must be excluded\n explicitly or a boolean would silently pass as 0.0/1.0. Same guard\n contrarian-setup-gate's `_valid_stop_reference` applies to the\n upstream price-action report's own stop_reference field.\"\"\"\n if isinstance(value, bool) or not isinstance(value, (int, float)):\n return None\n if not math.isfinite(value) or value \u003c= 0:\n return None\n return float(value)\n\n\n@dataclass(frozen=True)\nclass GateNormalized:\n \"\"\"One gate-report's normalized, fail-closed usability.\n\n `usable=True` only for a schema-supported, symbol-matched\n `READY_FOR_PLAN` report with a valid `direction` and\n `invalidation_level` -- exactly the shape `size_futures_position`\n needs for mode B. Every other case is `usable=False` with a named\n `reason`, always resolving to a NO_TRADE result (never a ConfigError\n -- the gate file is an untrusted input in every case here, by\n definition of mode B).\"\"\"\n\n usable: bool\n reason: str | None\n symbol: str | None = None\n direction: str | None = None\n invalidation_level: float | None = None\n setup_status: str | None = None\n gate_confidence: str | None = None\n warnings: tuple[str, ...] = ()\n\n\ndef _extract_confidence_and_warnings(\n raw_data: dict[str, Any],\n) -> tuple[str | None, tuple[str, ...]]:\n gate_confidence = raw_data.get(\"gate_confidence\")\n if not isinstance(gate_confidence, str):\n gate_confidence = None\n raw_warnings = raw_data.get(\"warnings\")\n warnings = (\n tuple(w for w in raw_warnings if isinstance(w, str))\n if isinstance(raw_warnings, list)\n else ()\n )\n return gate_confidence, warnings\n\n\ndef normalize_gate_report(\n raw_data: Any, load_error: str | None, *, symbol: str | None\n) -> GateNormalized:\n \"\"\"Normalize a contrarian-setup-gate JSON report for use as this\n skill's mode-B stop/direction source.\n\n `load_error` is one of the CLI's three hardened loader tags\n (\"unreadable\" / \"parse_error\" / \"non_finite\", copied verbatim from\n contrarian-setup-gate's `load_json_file`) or `None` when the file\n parsed cleanly. Everything below `load_error is None` is the NEW\n gate-shape (\"malformed\") checker this skill adds, in the same idiom\n as contrarian-setup-gate's own `normalize_*` functions -- detecting a\n wrong-shaped GATE-REPORT schema (setup_status/direction/\n invalidation_level types), not a wrong-shaped upstream input.\n\n `symbol`, when given, must match the report's own `symbol` field\n (case-insensitively); when omitted, the report's symbol is used as-is\n (plan §2: \"if --symbol omitted, taken from the gate report\").\n \"\"\"\n if load_error == \"unreadable\":\n return GateNormalized(False, \"gate_json_unreadable\")\n if load_error == \"parse_error\":\n return GateNormalized(False, \"gate_json_parse_error\")\n if load_error == \"non_finite\":\n return GateNormalized(False, \"gate_json_non_finite\")\n if not isinstance(raw_data, dict):\n return GateNormalized(False, \"gate_json_malformed\")\n\n if not _is_supported_schema(raw_data.get(\"schema_version\")):\n return GateNormalized(False, \"gate_json_schema_unsupported\")\n\n report_symbol = raw_data.get(\"symbol\")\n if not isinstance(report_symbol, str):\n return GateNormalized(False, \"gate_json_malformed\")\n # Strip BEFORE validating: a whitespace-only value (\" \") must not\n # slip past `not report_symbol` (a non-empty string of spaces is\n # truthy in Python) only to resolve to an unusable empty symbol\n # downstream -- and a path-hostile value (\"A/B\") must never reach a\n # GateNormalized.symbol that later flows into an output FILENAME\n # (user review round 3, P1-3: both were reachable before this check).\n report_symbol = report_symbol.strip()\n if not is_valid_symbol(report_symbol):\n return GateNormalized(False, \"gate_json_malformed\")\n report_symbol = report_symbol.upper()\n if symbol is not None and report_symbol != symbol.strip().upper():\n return GateNormalized(False, \"gate_symbol_mismatch\", symbol=report_symbol)\n\n setup_status = raw_data.get(\"setup_status\")\n if not isinstance(setup_status, str) or not setup_status:\n return GateNormalized(False, \"gate_json_malformed\", symbol=report_symbol)\n\n if setup_status != \"READY_FOR_PLAN\":\n gate_confidence, warnings = _extract_confidence_and_warnings(raw_data)\n return GateNormalized(\n False,\n \"gate_not_ready\",\n symbol=report_symbol,\n setup_status=setup_status,\n gate_confidence=gate_confidence,\n warnings=warnings,\n )\n\n direction = raw_data.get(\"direction\")\n if direction not in (\"LONG\", \"SHORT\"):\n return GateNormalized(\n False, \"gate_json_invalid_direction\", symbol=report_symbol, setup_status=setup_status\n )\n\n invalidation_level = _valid_finite_positive(raw_data.get(\"invalidation_level\"))\n if invalidation_level is None:\n return GateNormalized(\n False,\n \"gate_json_invalid_invalidation_level\",\n symbol=report_symbol,\n direction=direction,\n setup_status=setup_status,\n )\n\n gate_confidence, warnings = _extract_confidence_and_warnings(raw_data)\n return GateNormalized(\n True,\n None,\n symbol=report_symbol,\n direction=direction,\n invalidation_level=invalidation_level,\n setup_status=setup_status,\n gate_confidence=gate_confidence,\n warnings=warnings,\n )\n","content_type":"text/x-python; charset=utf-8","language":"python","size":51220,"content_sha256":"b62c92fc541f2c13929010e6cd66e802e179c6bd36a0be651773aaa3532a56d6"},{"filename":"scripts/tests/conftest.py","content":"\"\"\"Pytest configuration for futures-position-sizer tests.\"\"\"\n\nimport sys\nfrom pathlib import Path\n\nscripts_dir = Path(__file__).resolve().parents[1]\nif str(scripts_dir) not in sys.path:\n sys.path.insert(0, str(scripts_dir))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":227,"content_sha256":"c894252f919424e12d4f92dd016545edba9d6128a224e5f6c24912e2a9cd8132"},{"filename":"scripts/tests/test_futures_position_sizer.py","content":"\"\"\"Tests for futures_position_sizer.py -- the CLI wrapper.\n\nCovers: the hardened gate-json loader (unreadable / parse_error incl.\nRecursionError / non_finite, mirroring contrarian-setup-gate's own\nload_json_file), full mode-A and mode-B end-to-end runs, argparse-level\nnumeric validator rejections (inf/nan/1e309/zero/negative), exit-code\nasymmetry (ConfigError -> 2, gate-caused NO_TRADE -> 0), and JSON/text\nreport generation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nimport futures_position_sizer as cli\nimport pytest\n\nSCRIPT_PATH = Path(__file__).resolve().parents[1] / \"futures_position_sizer.py\"\n\n\ndef _run_cli(args: list[str], timeout: float | None = None) -> subprocess.CompletedProcess:\n return subprocess.run(\n [sys.executable, str(SCRIPT_PATH), *args],\n capture_output=True,\n text=True,\n timeout=timeout,\n )\n\n\ndef _ready_gate_fixture(**overrides) -> dict[str, Any]:\n fixture = {\n \"schema_version\": \"1.0\",\n \"symbol\": \"B6\",\n \"setup_status\": \"READY_FOR_PLAN\",\n \"direction\": \"SHORT\",\n \"gate_confidence\": \"HIGH\",\n \"entry_trigger\": \"price-action confirmation: key_reversal at week_of=2026-07-06\",\n \"invalidation_level\": 1.3450,\n \"missing_confirmations\": [],\n \"warnings\": [],\n \"inputs\": {},\n \"run_context\": {\"symbol\": \"B6\", \"as_of\": \"2026-07-15\", \"schema_version\": \"1.0\"},\n }\n fixture.update(overrides)\n return fixture\n\n\n# --- Section 1: hardened gate-json loader -----------------------------------\n\n\ndef test_load_json_file_missing_file_is_unreadable(tmp_path: Path) -> None:\n data, reason = cli.load_json_file(str(tmp_path / \"nope.json\"))\n assert data is None\n assert reason == \"unreadable\"\n\n\ndef test_load_json_file_directory_is_unreadable(tmp_path: Path) -> None:\n data, reason = cli.load_json_file(str(tmp_path))\n assert data is None\n assert reason == \"unreadable\"\n\n\ndef test_load_json_file_non_utf8_binary_is_unreadable(tmp_path: Path) -> None:\n path = tmp_path / \"binary.json\"\n path.write_bytes(b\"\\xff\\xfe\\x00bad\")\n data, reason = cli.load_json_file(str(path))\n assert data is None\n assert reason == \"unreadable\"\n\n\ndef test_load_json_file_invalid_json_is_parse_error(tmp_path: Path) -> None:\n path = tmp_path / \"bad.json\"\n path.write_text(\"{not valid json\", encoding=\"utf-8\")\n data, reason = cli.load_json_file(str(path))\n assert data is None\n assert reason == \"parse_error\"\n\n\ndef test_load_json_file_extreme_nesting_is_parse_error(tmp_path: Path) -> None:\n depth = 250_000\n raw_text = \"[\" * depth + \"1\" + \"]\" * depth\n path = tmp_path / \"extreme.json\"\n path.write_text(raw_text, encoding=\"utf-8\")\n data, reason = cli.load_json_file(str(path))\n assert data is None\n assert reason == \"parse_error\"\n\n\ndef test_load_json_file_overflow_number_is_non_finite(tmp_path: Path) -> None:\n path = tmp_path / \"overflow.json\"\n path.write_text('{\"invalidation_level\": 1e309}', encoding=\"utf-8\")\n data, reason = cli.load_json_file(str(path))\n assert data is None\n assert reason == \"non_finite\"\n\n\ndef test_load_json_file_literal_infinity_is_non_finite(tmp_path: Path) -> None:\n path = tmp_path / \"infinity.json\"\n path.write_text('{\"invalidation_level\": Infinity}', encoding=\"utf-8\")\n data, reason = cli.load_json_file(str(path))\n assert data is None\n assert reason == \"non_finite\"\n\n\ndef test_load_json_file_nan_nested_deep_is_non_finite(tmp_path: Path) -> None:\n path = tmp_path / \"deep_nan.json\"\n path.write_text('{\"a\": {\"b\": {\"c\": [1, 2, NaN]}}}', encoding=\"utf-8\")\n data, reason = cli.load_json_file(str(path))\n assert data is None\n assert reason == \"non_finite\"\n\n\ndef test_load_json_file_deep_but_finite_field_loads_normally(tmp_path: Path) -> None:\n fixture = _ready_gate_fixture()\n deep = 1.0\n for _ in range(500):\n deep = [deep]\n fixture[\"_deep_unused\"] = deep\n path = tmp_path / \"deep_finite.json\"\n path.write_text(json.dumps(fixture), encoding=\"utf-8\")\n data, reason = cli.load_json_file(str(path))\n assert reason is None\n assert data[\"symbol\"] == \"B6\"\n\n\ndef test_load_json_file_valid_json_succeeds(tmp_path: Path) -> None:\n path = tmp_path / \"ok.json\"\n path.write_text(json.dumps(_ready_gate_fixture()), encoding=\"utf-8\")\n data, reason = cli.load_json_file(str(path))\n assert reason is None\n assert data[\"setup_status\"] == \"READY_FOR_PLAN\"\n\n\n# --- Section 2: numeric argparse validators (subprocess, exit code 2) ------\n\n\nclass TestNumericValidatorsRejectDegenerateValues:\n BASE_ARGS = [\n \"--symbol\",\n \"ES\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"5000.25\",\n \"--stop\",\n \"4980.00\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--output-dir\",\n \"/tmp/does-not-matter\",\n ]\n\n def _with_override(self, flag: str, value: str) -> list[str]:\n args = list(self.BASE_ARGS)\n idx = args.index(flag)\n args[idx + 1] = value\n return args\n\n @pytest.mark.parametrize(\"bad_value\", [\"inf\", \"-inf\", \"nan\", \"1e309\", \"0\", \"-5\"])\n def test_entry_rejects_degenerate_values(self, bad_value):\n result = _run_cli(self._with_override(\"--entry\", bad_value))\n assert result.returncode == 2\n\n @pytest.mark.parametrize(\"bad_value\", [\"inf\", \"nan\", \"1e309\", \"0\", \"-5\"])\n def test_stop_rejects_degenerate_values(self, bad_value):\n result = _run_cli(self._with_override(\"--stop\", bad_value))\n assert result.returncode == 2\n\n @pytest.mark.parametrize(\"bad_value\", [\"inf\", \"nan\", \"1e309\", \"0\", \"-100000\"])\n def test_account_size_rejects_degenerate_values(self, bad_value):\n result = _run_cli(self._with_override(\"--account-size\", bad_value))\n assert result.returncode == 2\n\n @pytest.mark.parametrize(\"bad_value\", [\"inf\", \"nan\", \"0\", \"-1\", \"10.01\"])\n def test_risk_pct_rejects_degenerate_or_out_of_range_values(self, bad_value):\n result = _run_cli(self._with_override(\"--risk-pct\", bad_value))\n assert result.returncode == 2\n\n def test_risk_pct_accepts_boundary_10(self):\n result = _run_cli(self._with_override(\"--risk-pct\", \"10.0\"))\n assert result.returncode == 0\n\n @pytest.mark.parametrize(\"bad_value\", [\"-1\", \"1.5\", \"inf\", \"nan\"])\n def test_max_contracts_rejects_degenerate_values(self, bad_value):\n args = [*self.BASE_ARGS, \"--max-contracts\", bad_value]\n result = _run_cli(args)\n assert result.returncode == 2\n\n def test_max_contracts_zero_means_no_cap(self):\n args = [*self.BASE_ARGS, \"--max-contracts\", \"0\"]\n result = _run_cli(args)\n assert result.returncode == 0\n\n def test_max_contracts_beyond_2_pow_53_parses_exactly(self, tmp_path):\n # Code review round 3, P2-4: a --max-contracts value beyond\n # float64's exact-integer range must parse without precision\n # loss -- this is far above any real cap, so it's never actually\n # binding here, but it must not be silently coerced to a\n # different integer or crash.\n out_dir = tmp_path / \"reports\"\n args = [\n *self.BASE_ARGS,\n \"--max-contracts\",\n \"9007199254740995\",\n \"--output-dir\",\n str(out_dir),\n ]\n result = _run_cli(args)\n assert result.returncode == 0\n\n @pytest.mark.parametrize(\"bad_value\", [\"1.0\", \"1e2\", \"1,000\"])\n def test_max_contracts_rejects_non_integer_syntax(self, bad_value):\n # int()-based parsing is intentionally stricter than the old\n # float()-based one: a decimal point or exponent notation is never\n # valid --max-contracts syntax now, even when mathematically a\n # whole number.\n args = [*self.BASE_ARGS, \"--max-contracts\", bad_value]\n result = _run_cli(args)\n assert result.returncode == 2\n\n @pytest.mark.parametrize(\"bad_value\", [\"inf\", \"nan\", \"1e309\", \"0\", \"-1.2\"])\n def test_fx_rate_rejects_degenerate_values(self, bad_value):\n args = [*self.BASE_ARGS, \"--fx-rate\", bad_value]\n result = _run_cli(args)\n assert result.returncode == 2\n\n\n# --- Section 2b: float64 overflow guards (code review round 1, P2) ---------\n#\n# --account-size/--multiplier/--tick-size/--fx-rate lacked a max_value cap\n# (unlike --risk-pct, which already had one) -- an individually \"valid\"\n# (finite, positive) but extreme value on one of these, or an extreme\n# --entry/--stop (deliberately left uncapped -- real prices have no reason\n# to be bounded), could make a COMPUTED intermediate (risk_budget,\n# risk_per_contract, or the tick-grid ratio) overflow to a non-finite\n# value, which used to escape as an uncaught OverflowError/ValueError --\n# exit 1, violating the two-class exit contract (2 config / 0 fail-closed\n# report -- never 1).\n\n\nclass TestOverflowGuards:\n BASE_ARGS = [\n \"--symbol\", \"ES\",\n \"--direction\", \"LONG\",\n \"--entry\", \"5000.25\",\n \"--stop\", \"4980.00\",\n \"--account-size\", \"100000\",\n \"--risk-pct\", \"1.0\",\n \"--as-of\", \"2026-07-17\",\n ] # fmt: skip\n\n UNKNOWN_SYMBOL_ARGS = [\n \"--symbol\", \"ZZZZ\",\n \"--direction\", \"LONG\",\n \"--entry\", \"100.0\",\n \"--stop\", \"90.0\",\n \"--contract-currency\", \"USD\",\n \"--account-size\", \"100000\",\n \"--risk-pct\", \"1.0\",\n \"--as-of\", \"2026-07-17\",\n ] # fmt: skip\n\n def _with_override(self, base: list[str], flag: str, value: str) -> list[str]:\n args = list(base)\n idx = args.index(flag)\n args[idx + 1] = value\n return args\n\n def test_repro_a_extreme_account_size_exits_2_not_1(self, tmp_path):\n # Exact code-review repro A: --account-size 1.5e308 --risk-pct 10.0\n # used to overflow risk_budget to inf, then crash with\n # OverflowError inside math.floor() -- exit 1. Now caught by the\n # --account-size argparse cap itself, before any risk math runs.\n out_dir = tmp_path / \"reports\"\n args = self._with_override(self.BASE_ARGS, \"--account-size\", \"1.5e308\")\n args = self._with_override(args, \"--risk-pct\", \"10.0\")\n args = [*args, \"--output-dir\", str(out_dir)]\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n assert not out_dir.exists() or not any(out_dir.iterdir())\n\n def test_repro_b_extreme_multiplier_override_exits_2_not_1(self, tmp_path):\n # Exact code-review repro B: an unknown-symbol override with\n # --multiplier 1e308 --tick-size 0.01 used to overflow\n # risk_per_contract to inf, then crash writing the JSON report\n # (allow_nan=False -> ValueError) -- exit 1. Now caught by the\n # --multiplier argparse cap itself.\n out_dir = tmp_path / \"reports\"\n args = [\n *self.UNKNOWN_SYMBOL_ARGS,\n \"--multiplier\", \"1e308\",\n \"--tick-size\", \"0.01\",\n \"--output-dir\", str(out_dir),\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n assert not out_dir.exists() or not any(out_dir.iterdir())\n\n def test_account_size_boundary_at_cap_succeeds(self, tmp_path):\n out_dir = tmp_path / \"reports\"\n args = self._with_override(self.BASE_ARGS, \"--account-size\", \"1e12\")\n args = [*args, \"--output-dir\", str(out_dir), \"--format\", \"json\"]\n result = _run_cli(args)\n assert result.returncode == 0\n\n def test_account_size_boundary_above_cap_rejected(self):\n args = self._with_override(self.BASE_ARGS, \"--account-size\", \"1.1e12\")\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n\n def test_multiplier_boundary_at_cap_succeeds(self, tmp_path):\n out_dir = tmp_path / \"reports\"\n args = [\n *self.UNKNOWN_SYMBOL_ARGS,\n \"--multiplier\", \"1e9\",\n \"--tick-size\", \"0.01\",\n \"--output-dir\", str(out_dir),\n \"--format\", \"json\",\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 0\n\n def test_multiplier_boundary_above_cap_rejected(self):\n args = [\n *self.UNKNOWN_SYMBOL_ARGS,\n \"--multiplier\", \"1.1e9\",\n \"--tick-size\", \"0.01\",\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n\n def test_tick_size_boundary_at_cap_succeeds(self, tmp_path):\n # Entry/stop must be at least one 1e6-sized tick apart, or this\n # would (correctly) hit stop_too_close instead of exercising the\n # tick-size cap boundary itself.\n out_dir = tmp_path / \"reports\"\n args = [\n \"--symbol\", \"ZZZZ\",\n \"--direction\", \"LONG\",\n \"--entry\", \"3000000.0\",\n \"--stop\", \"1000000.0\",\n \"--multiplier\", \"1\",\n \"--tick-size\", \"1e6\",\n \"--contract-currency\", \"USD\",\n \"--account-size\", \"100000\",\n \"--risk-pct\", \"1.0\",\n \"--as-of\", \"2026-07-17\",\n \"--output-dir\", str(out_dir),\n \"--format\", \"json\",\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 0\n\n def test_tick_size_boundary_above_cap_rejected(self):\n args = [\n *self.UNKNOWN_SYMBOL_ARGS,\n \"--multiplier\", \"1\",\n \"--tick-size\", \"1.1e6\",\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n\n def test_fx_rate_boundary_at_cap_succeeds(self, tmp_path):\n out_dir = tmp_path / \"reports\"\n args = [\n *self.BASE_ARGS,\n \"--fx-rate\",\n \"1e6\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ]\n result = _run_cli(args)\n assert result.returncode == 0\n\n def test_fx_rate_boundary_above_cap_rejected(self):\n args = [*self.BASE_ARGS, \"--fx-rate\", \"1.1e6\"]\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n\n def test_one_trillion_account_size_control_case_still_sizes(self, tmp_path):\n # The cap's own boundary value must still produce an ordinary,\n # correct SIZED result -- the fix must never false-positive on a\n # merely large (but finite) account size.\n out_dir = tmp_path / \"reports\"\n args = self._with_override(self.BASE_ARGS, \"--account-size\", \"1e12\")\n args = [*args, \"--output-dir\", str(out_dir), \"--format\", \"json\"]\n result = _run_cli(args)\n assert result.returncode == 0\n payload = json.loads((out_dir / \"futures_position_size_ES_2026-07-17.json\").read_text())\n assert payload[\"sizing_status\"] == \"SIZED\"\n assert payload[\"risk_budget_usd\"] == pytest.approx(1e12 * 1.0 / 100.0)\n\n def test_extreme_entry_uncapped_flag_still_exits_2_not_1(self, tmp_path):\n # --entry deliberately has NO max_value cap (real prices are never\n # artificially bounded) -- this is the residual overflow route the\n # argparse-level caps on multiplier/tick-size/fx-rate alone cannot\n # prevent. Only the isfinite() guard on the computed\n # risk_per_contract catches it. Must exit 2 cleanly, never crash.\n out_dir = tmp_path / \"reports\"\n args = [\n \"--symbol\", \"ES\",\n \"--direction\", \"LONG\",\n \"--entry\", \"1e308\",\n \"--stop\", \"1.0\",\n \"--account-size\", \"100000\",\n \"--risk-pct\", \"1.0\",\n \"--output-dir\", str(out_dir),\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n assert not out_dir.exists() or not any(out_dir.iterdir())\n\n def test_extreme_bond_entry_gets_overflow_message_not_32nds(self, tmp_path):\n # Code review round 3, P3: an extreme --entry (>1e300) on a BOND\n # symbol used to trip the 32nds-notation ConfigError, misattributing\n # a numeric overflow as a notation mistake. Must exit 2 with an\n # overflow/magnitude message instead, and never mention \"32nds\".\n out_dir = tmp_path / \"reports\"\n args = [\n \"--symbol\", \"ZB\",\n \"--direction\", \"LONG\",\n \"--entry\", \"1e308\",\n \"--stop\", \"1.0\",\n \"--account-size\", \"100000\",\n \"--risk-pct\", \"1.0\",\n \"--output-dir\", str(out_dir),\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n assert \"32nds\" not in result.stderr\n assert \"not finite\" in result.stderr\n assert \"--entry\" in result.stderr\n assert not out_dir.exists() or not any(out_dir.iterdir())\n\n def test_normal_off_grid_bond_entry_still_gets_32nds_message(self, tmp_path):\n # Regression guard for the same fix: an ordinary, finite off-grid\n # bond entry (the classic 110.16-vs-110'16 mistype) must still be\n # rejected with the 32nds-notation message, not misrouted into the\n # overflow path.\n out_dir = tmp_path / \"reports\"\n args = [\n \"--symbol\", \"ZB\",\n \"--direction\", \"LONG\",\n \"--entry\", \"110.16\",\n \"--stop\", \"108.00\",\n \"--account-size\", \"100000\",\n \"--risk-pct\", \"1.0\",\n \"--output-dir\", str(out_dir),\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n assert \"32nds\" in result.stderr\n assert not out_dir.exists() or not any(out_dir.iterdir())\n\n\n# --- Section 2c: code review round 3 (user re-review) -----------------------\n# P1-1: floor epsilon rounded contracts UP past the risk budget at large\n# scale. P1-2: denormal underflow (risk_per_contract == 0.0) crashed with\n# ZeroDivisionError, exit 1.\n\n\nclass TestBudgetInvariantAndUnderflowGuards:\n def test_p1_1_exact_repro_never_exceeds_budget_end_to_end(self, tmp_path):\n # budget = account_size * risk_pct / 100 = 999999999500 * 10 / 100\n # = 99,999,999,950.0; risk_per_contract = distance(10) *\n # multiplier(100) = 1000.0 -- reproduces the reviewer's exact\n # q=99999999.95 case end-to-end through the real CLI.\n out_dir = tmp_path / \"reports\"\n args = [\n \"--symbol\", \"ZZZZ\",\n \"--direction\", \"LONG\",\n \"--entry\", \"100.0\",\n \"--stop\", \"90.0\",\n \"--multiplier\", \"100\",\n \"--tick-size\", \"0.01\",\n \"--contract-currency\", \"USD\",\n \"--account-size\", \"999999999500\",\n \"--risk-pct\", \"10.0\",\n \"--as-of\", \"2026-07-17\",\n \"--output-dir\", str(out_dir),\n \"--format\", \"json\",\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 0\n payload = json.loads((out_dir / \"futures_position_size_ZZZZ_2026-07-17.json\").read_text())\n assert payload[\"sizing_status\"] == \"SIZED\"\n assert payload[\"contracts\"] == 99_999_999\n assert payload[\"contracts\"] * payload[\"risk_per_contract_usd\"] \u003c= payload[\"risk_budget_usd\"]\n\n def test_p1_2_denormal_underflow_exits_2_not_1(self, tmp_path):\n # Exact reviewer repro end-to-end through the real CLI: all four\n # values individually pass the finite/positive validators, but the\n # product underflows to exactly 0.0.\n out_dir = tmp_path / \"reports\"\n args = [\n \"--symbol\", \"ZZZZ\",\n \"--direction\", \"LONG\",\n \"--entry\", \"2e-308\",\n \"--stop\", \"1e-308\",\n \"--multiplier\", \"1e-308\",\n \"--tick-size\", \"1e-308\",\n \"--contract-currency\", \"USD\",\n \"--account-size\", \"100000\",\n \"--risk-pct\", \"1.0\",\n \"--output-dir\", str(out_dir),\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n assert not out_dir.exists() or not any(out_dir.iterdir())\n\n def test_p1_1_followup_hang_repro_terminates_and_exits_2(self, tmp_path):\n # Second re-review of the P1-1 fix: the absolute-epsilon + hard\n # post-condition loop that \"fixed\" P1-1 could not terminate in\n # practice at this scale (float64 can no longer represent\n # `(contracts - 1) * rpc != contracts * rpc` once contracts is\n # ~2.4e285, so the loop's decrement never reaches its exit\n # condition). A `timeout=` on the subprocess call turns a\n # regression back into that failure mode into a clean pytest\n # failure (TimeoutExpired) instead of an actually-hanging test\n # suite. The exact-rational floor must terminate instantly and\n # reject the absurd result outright.\n out_dir = tmp_path / \"reports\"\n args = [\n \"--symbol\", \"ZZZZ\",\n \"--direction\", \"LONG\",\n \"--entry\", \"2\",\n \"--stop\", \"1\",\n \"--multiplier\", \"1.4296227991821346e-275\",\n \"--tick-size\", \"1\",\n \"--contract-currency\", \"USD\",\n \"--account-size\", \"341482236954.82006\",\n \"--risk-pct\", \"10\",\n \"--output-dir\", str(out_dir),\n ] # fmt: skip\n result = _run_cli(args, timeout=10.0)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n assert \"implausible\" in result.stderr\n assert not out_dir.exists() or not any(out_dir.iterdir())\n\n\n# --- Section 3: mode A / mode B end-to-end (in-process via cli.main) -------\n\n\ndef _argv(args, monkeypatch):\n monkeypatch.setattr(sys, \"argv\", [\"futures_position_sizer.py\", *args])\n\n\nclass TestModeAEndToEnd:\n def test_es_long_hand_checked_sized(self, tmp_path, monkeypatch, capsys):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ES\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"5000.25\",\n \"--stop\",\n \"4980.00\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"2.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n json_path = out_dir / \"futures_position_size_ES_2026-07-17.json\"\n assert json_path.exists()\n payload = json.loads(json_path.read_text())\n assert payload[\"sizing_status\"] == \"SIZED\"\n assert payload[\"contracts\"] == 1\n assert payload[\"risk_per_contract_usd\"] == pytest.approx(1012.50)\n\n def test_geometry_violation_exits_2_no_report(self, tmp_path, monkeypatch, capsys):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ES\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"5000.00\",\n \"--stop\",\n \"5010.00\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--output-dir\",\n str(out_dir),\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 2\n assert not out_dir.exists() or not any(out_dir.iterdir())\n\n def test_bond_off_grid_entry_exits_2(self, tmp_path, monkeypatch):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ZB\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"110.16\",\n \"--stop\",\n \"108.00\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--output-dir\",\n str(out_dir),\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 2\n\n def test_zero_contracts_is_no_trade_exit_0(self, tmp_path, monkeypatch):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ES\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"5000.25\",\n \"--stop\",\n \"4980.00\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n payload = json.loads((out_dir / \"futures_position_size_ES_2026-07-17.json\").read_text())\n assert payload[\"sizing_status\"] == \"NO_TRADE\"\n assert payload[\"no_trade_reason\"] == \"risk_below_one_contract\"\n\n def test_unknown_symbol_without_overrides_exits_2(self, tmp_path, monkeypatch):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ZZZZ\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"100.0\",\n \"--stop\",\n \"90.0\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--output-dir\",\n str(out_dir),\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 2\n\n def test_unknown_symbol_with_full_overrides_sizes(self, tmp_path, monkeypatch):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ZZZZ\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"100.0\",\n \"--stop\",\n \"90.0\",\n \"--multiplier\",\n \"10\",\n \"--tick-size\",\n \"0.5\",\n \"--contract-currency\",\n \"USD\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"5.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n payload = json.loads((out_dir / \"futures_position_size_ZZZZ_2026-07-17.json\").read_text())\n assert payload[\"sizing_status\"] == \"SIZED\"\n\n def test_direction_and_gate_json_conflict_exits_2(self, tmp_path):\n # parser.error() raises SystemExit -- exercised via subprocess like\n # every other argparse-level (as opposed to fs.ConfigError-level)\n # usage error, matching position-sizer's own convention.\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_text(json.dumps(_ready_gate_fixture()), encoding=\"utf-8\")\n result = _run_cli(\n [\n \"--gate-json\",\n str(gate_path),\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"1.35\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n ]\n )\n assert result.returncode == 2\n\n\nclass TestModeBEndToEnd:\n def test_ready_gate_report_sizes_via_gate_stop(self, tmp_path, monkeypatch):\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_text(json.dumps(_ready_gate_fixture()), encoding=\"utf-8\")\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--gate-json\",\n str(gate_path),\n \"--entry\",\n \"1.3400\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"5.0\",\n \"--fx-rate\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n payload = json.loads((out_dir / \"futures_position_size_B6_2026-07-17.json\").read_text())\n assert payload[\"direction\"] == \"SHORT\"\n assert payload[\"stop\"] == pytest.approx(1.3450)\n assert payload[\"gate\"][\"setup_status\"] == \"READY_FOR_PLAN\"\n\n def test_non_ready_gate_report_is_no_trade_exit_0(self, tmp_path, monkeypatch):\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_text(\n json.dumps(\n _ready_gate_fixture(setup_status=\"CROWDED\", direction=None, invalidation_level=None)\n ),\n encoding=\"utf-8\",\n )\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--gate-json\",\n str(gate_path),\n \"--entry\",\n \"1.34\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n payload = json.loads((out_dir / \"futures_position_size_B6_2026-07-17.json\").read_text())\n assert payload[\"sizing_status\"] == \"NO_TRADE\"\n assert payload[\"no_trade_reason\"] == \"gate_not_ready\"\n\n def test_binary_gate_file_is_no_trade_exit_0(self, tmp_path, monkeypatch):\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_bytes(b\"\\xff\\xfe\\x00bad\")\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--gate-json\",\n str(gate_path),\n \"--symbol\",\n \"B6\",\n \"--entry\",\n \"1.34\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n payload = json.loads((out_dir / \"futures_position_size_B6_2026-07-17.json\").read_text())\n assert payload[\"sizing_status\"] == \"NO_TRADE\"\n assert payload[\"no_trade_reason\"] == \"gate_json_unreadable\"\n\n def test_symbol_mismatch_is_no_trade_exit_0(self, tmp_path, monkeypatch):\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_text(json.dumps(_ready_gate_fixture()), encoding=\"utf-8\")\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--gate-json\",\n str(gate_path),\n \"--symbol\",\n \"ES\",\n \"--entry\",\n \"5000\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n payload = json.loads((out_dir / \"futures_position_size_ES_2026-07-17.json\").read_text())\n assert payload[\"no_trade_reason\"] == \"gate_symbol_mismatch\"\n\n # --- Code review round 3 (user re-review), P1-3 ------------------------\n\n def test_whitespace_only_gate_symbol_is_no_trade_exit_0(self, tmp_path, monkeypatch):\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_text(json.dumps(_ready_gate_fixture(symbol=\" \")), encoding=\"utf-8\")\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--gate-json\",\n str(gate_path),\n \"--entry\",\n \"1.34\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ], # fmt: skip\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n payload = json.loads(\n (out_dir / \"futures_position_size_UNKNOWN_2026-07-17.json\").read_text()\n )\n assert payload[\"sizing_status\"] == \"NO_TRADE\"\n assert payload[\"no_trade_reason\"] == \"gate_json_malformed\"\n\n def test_path_hostile_gate_symbol_is_no_trade_with_safe_filename(self, tmp_path, monkeypatch):\n # \"A/B\" must never reach the output filename -- with no --symbol\n # given to fall back on, the CLI must resolve to the safe literal\n # \"UNKNOWN\", not crash with FileNotFoundError writing into a\n # nonexistent \"futures_position_size_A\" subdirectory.\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_text(json.dumps(_ready_gate_fixture(symbol=\"A/B\")), encoding=\"utf-8\")\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--gate-json\",\n str(gate_path),\n \"--entry\",\n \"1.34\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ], # fmt: skip\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n written_files = list(out_dir.iterdir())\n assert len(written_files) == 1\n assert \"/\" not in written_files[0].name\n payload = json.loads(written_files[0].read_text())\n assert payload[\"sizing_status\"] == \"NO_TRADE\"\n assert payload[\"no_trade_reason\"] == \"gate_json_malformed\"\n\n def test_path_hostile_explicit_symbol_exits_2(self, tmp_path):\n # The same allowlist applies to an operator-typed --symbol in\n # explicit mode -- a usage error, exit 2, symmetric with the\n # gate-file case above being a fail-closed NO_TRADE (exit 0).\n out_dir = tmp_path / \"reports\"\n args = [\n \"--symbol\", \"A/B\",\n \"--direction\", \"LONG\",\n \"--entry\", \"100.0\",\n \"--stop\", \"90.0\",\n \"--account-size\", \"100000\",\n \"--risk-pct\", \"1.0\",\n \"--output-dir\", str(out_dir),\n ] # fmt: skip\n result = _run_cli(args)\n assert result.returncode == 2\n assert \"Traceback\" not in result.stderr\n assert not out_dir.exists() or not any(out_dir.iterdir())\n\n def test_gate_stop_off_tick_grid_bond_is_no_trade_exit_0(self, tmp_path, monkeypatch):\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_text(\n json.dumps(\n _ready_gate_fixture(symbol=\"ZB\", direction=\"LONG\", invalidation_level=108.16)\n ),\n encoding=\"utf-8\",\n )\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--gate-json\",\n str(gate_path),\n \"--entry\",\n \"110.50\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n payload = json.loads((out_dir / \"futures_position_size_ZB_2026-07-17.json\").read_text())\n assert payload[\"no_trade_reason\"] == \"gate_stop_off_tick_grid\"\n\n def test_gate_stop_too_close_is_no_trade_exit_0(self, tmp_path, monkeypatch):\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_text(\n json.dumps(_ready_gate_fixture(direction=\"LONG\", invalidation_level=1.34995)),\n encoding=\"utf-8\",\n )\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--gate-json\",\n str(gate_path),\n \"--entry\",\n \"1.35000\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n\n def test_entry_always_required_missing_exits_2(self, tmp_path):\n # parser.error() raises SystemExit -- see subprocess note above.\n gate_path = tmp_path / \"gate.json\"\n gate_path.write_text(json.dumps(_ready_gate_fixture()), encoding=\"utf-8\")\n result = _run_cli(\n [\n \"--gate-json\",\n str(gate_path),\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n ]\n )\n assert result.returncode == 2\n\n\n# --- Section 4: --list-specs -------------------------------------------------\n\n\ndef test_list_specs_prints_table_and_exits_0(capsys, monkeypatch):\n _argv([\"--list-specs\"], monkeypatch)\n exit_code = cli.main()\n assert exit_code == 0\n captured = capsys.readouterr()\n assert \"ES\" in captured.out\n assert \"GC\" in captured.out\n\n\ndef test_list_specs_ignores_other_missing_required_args(monkeypatch):\n _argv([\"--list-specs\"], monkeypatch)\n exit_code = cli.main()\n assert exit_code == 0\n\n\n# --- Section 5: currency / fx-rate guard ------------------------------------\n\n\ndef test_non_usd_symbol_without_fx_rate_exits_2(tmp_path, monkeypatch):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ZZZZ\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"100.0\",\n \"--stop\",\n \"90.0\",\n \"--multiplier\",\n \"10\",\n \"--tick-size\",\n \"0.5\",\n \"--contract-currency\",\n \"GBP\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--output-dir\",\n str(out_dir),\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 2\n\n\ndef test_non_usd_symbol_with_fx_rate_sizes(tmp_path, monkeypatch):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ZZZZ\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"100.0\",\n \"--stop\",\n \"90.0\",\n \"--multiplier\",\n \"10\",\n \"--tick-size\",\n \"0.5\",\n \"--contract-currency\",\n \"GBP\",\n \"--fx-rate\",\n \"1.25\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"5.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n payload = json.loads((out_dir / \"futures_position_size_ZZZZ_2026-07-17.json\").read_text())\n assert payload[\"fx_rate_used\"] == 1.25\n\n\n# --- Section 6: text format renders for SIZED and NO_TRADE -----------------\n\n\ndef test_text_format_renders_sized(tmp_path, monkeypatch, capsys):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ES\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"5000.25\",\n \"--stop\",\n \"4980.00\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"2.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"text\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n captured = capsys.readouterr()\n assert \"SIZED\" in captured.out\n assert \"1\" in captured.out\n\n\ndef test_text_format_renders_no_trade(tmp_path, monkeypatch, capsys):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ES\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"5000.25\",\n \"--stop\",\n \"4980.00\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"1.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"text\",\n ],\n monkeypatch,\n )\n exit_code = cli.main()\n assert exit_code == 0\n captured = capsys.readouterr()\n assert \"NO_TRADE\" in captured.out\n assert \"risk_below_one_contract\" in captured.out\n\n\n# --- Section 7: JSON writer never emits non-standard tokens -----------------\n\n\ndef test_json_report_has_no_nan_or_infinity_tokens(tmp_path, monkeypatch):\n out_dir = tmp_path / \"reports\"\n _argv(\n [\n \"--symbol\",\n \"ES\",\n \"--direction\",\n \"LONG\",\n \"--entry\",\n \"5000.25\",\n \"--stop\",\n \"4980.00\",\n \"--account-size\",\n \"100000\",\n \"--risk-pct\",\n \"2.0\",\n \"--as-of\",\n \"2026-07-17\",\n \"--output-dir\",\n str(out_dir),\n \"--format\",\n \"json\",\n ],\n monkeypatch,\n )\n cli.main()\n raw_text = (out_dir / \"futures_position_size_ES_2026-07-17.json\").read_text()\n assert \"NaN\" not in raw_text\n assert \"Infinity\" not in raw_text\n json.loads(raw_text) # must be strictly valid JSON\n","content_type":"text/x-python; charset=utf-8","language":"python","size":42847,"content_sha256":"ebc16b3508f59fc3dcfae3cf303504a4ae37060e3857d5ab7ea837e75e70022f"},{"filename":"scripts/tests/test_futures_sizing.py","content":"\"\"\"Tests for the pure futures_sizing module: validators, risk math, floor\nalgorithm, tick-grid guards, and gate-report normalization.\n\nContract-spec-table invariant/spot-check tests live in this same file (added\nonce the spec table is populated) but are import-order independent of the\ntests below -- everything here exercises the core sizing math against\nsynthetic, hand-built spec dicts so it never depends on the verified table\nbeing complete.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport math\n\nimport futures_sizing as fs\nimport pytest\n\n# A synthetic ES-like spec used across many tests so the math tests don't\n# depend on the real (WebSearch-verified) CONTRACT_SPECS table.\nES_SPEC = {\n \"cot_symbol\": \"ES\",\n \"exchange_product\": \"E-mini S&P 500\",\n \"multiplier\": 50,\n \"tick_size\": 0.25,\n \"tick_value\": 12.5,\n \"currency\": \"USD\",\n \"exchange\": \"CME\",\n \"source_url\": \"https://example.invalid/es\",\n \"verified_date\": \"2026-07-17\",\n}\n\n# A synthetic bond-family spec (mirrors ZB: 1/32 = 0.03125 point tick).\nZB_SPEC = {\n \"cot_symbol\": \"ZB\",\n \"exchange_product\": \"30-Year T-Bond\",\n \"multiplier\": 1000,\n \"tick_size\": 0.03125,\n \"tick_value\": 31.25,\n \"currency\": \"USD\",\n \"exchange\": \"CBOT\",\n \"source_url\": \"https://example.invalid/zb\",\n \"verified_date\": \"2026-07-17\",\n}\n\n\n# --- Numeric string validator (argparse-facing, pure logic) ----------------\n\n\nclass TestStrictPositiveFloat:\n def test_accepts_plain_positive(self):\n assert fs.strict_positive_float(\"5000.25\") == 5000.25\n\n def test_rejects_inf_string(self):\n with pytest.raises(ValueError):\n fs.strict_positive_float(\"inf\")\n\n def test_rejects_neg_inf_string(self):\n with pytest.raises(ValueError):\n fs.strict_positive_float(\"-inf\")\n\n def test_rejects_nan_string(self):\n with pytest.raises(ValueError):\n fs.strict_positive_float(\"nan\")\n\n def test_rejects_overflow_literal(self):\n # 1e309 overflows float64 to inf on parse -- must still be rejected.\n with pytest.raises(ValueError):\n fs.strict_positive_float(\"1e309\")\n\n def test_rejects_zero(self):\n with pytest.raises(ValueError):\n fs.strict_positive_float(\"0\")\n\n def test_rejects_negative(self):\n with pytest.raises(ValueError):\n fs.strict_positive_float(\"-5\")\n\n def test_rejects_non_numeric(self):\n with pytest.raises(ValueError):\n fs.strict_positive_float(\"banana\")\n\n def test_max_value_boundary_inclusive(self):\n assert fs.strict_positive_float(\"10.0\", max_value=10.0) == 10.0\n\n def test_max_value_boundary_rejected_above(self):\n with pytest.raises(ValueError):\n fs.strict_positive_float(\"10.01\", max_value=10.0)\n\n\nclass TestStrictNonNegInt:\n def test_accepts_zero(self):\n assert fs.strict_nonneg_int(\"0\") == 0\n\n def test_accepts_positive(self):\n assert fs.strict_nonneg_int(\"5\") == 5\n\n def test_rejects_negative(self):\n with pytest.raises(ValueError):\n fs.strict_nonneg_int(\"-1\")\n\n def test_rejects_non_integer(self):\n with pytest.raises(ValueError):\n fs.strict_nonneg_int(\"1.5\")\n\n def test_rejects_inf(self):\n with pytest.raises(ValueError):\n fs.strict_nonneg_int(\"inf\")\n\n def test_rejects_nan(self):\n with pytest.raises(ValueError):\n fs.strict_nonneg_int(\"nan\")\n\n # --- Code review round 3 (user re-review), P2-4: a value beyond\n # float64's 2**53 exact-integer range used to silently round-trip to a\n # DIFFERENT integer via the old float()-based parser, making the \"hard\n # cap\" --max-contracts exists to provide exceedable by one.\n\n def test_p2_4_beyond_2_pow_53_round_trips_exactly(self):\n # 2**53 = 9007199254740992; this value is 3 above it and is NOT\n # exactly representable as a float64 -- float(\"9007199254740995\")\n # rounds to 9007199254740996.0, a different integer. int() must\n # parse it exactly regardless.\n value = \"9007199254740995\"\n assert float(value) != 9_007_199_254_740_995 # confirms the float trap is real here\n assert fs.strict_nonneg_int(value) == 9_007_199_254_740_995\n\n def test_much_larger_than_2_pow_53_still_exact(self):\n value = str(2**100 + 7)\n assert fs.strict_nonneg_int(value) == 2**100 + 7\n\n\n# --- Floor algorithm (PINNED, exact rational arithmetic -- no epsilon) -----\n\n\nclass TestComputeContracts:\n def test_exact_k_times_rpc_gives_k(self):\n # rpc built from a non-binary-exact product (0.1 * 3) so the exact\n # multiple isn't binary-representable. budget is computed AS\n # rpc * 7 (the same rpc, round-tripped), so the exact rational\n # quotient recovers exactly 7 -- no epsilon involved at all now,\n # just exact Fraction arithmetic.\n rpc = 0.1 * 3 # 0.30000000000000004 in float64\n budget = rpc * 7\n assert fs.compute_contracts(budget, rpc) == 7\n\n def test_k_times_rpc_minus_true_shortfall_gives_k_minus_1(self):\n rpc = 0.1 * 3\n # A REAL shortfall much larger than float noise must still floor down.\n budget = rpc * 7 - 0.01\n assert fs.compute_contracts(budget, rpc) == 6\n\n def test_true_two_point_five_stays_two(self):\n rpc = 100.0\n budget = 250.0\n assert fs.compute_contracts(budget, rpc) == 2\n\n def test_large_scale_exact_multiple_still_recovers_exactly(self):\n # q ~ 1e5: the exact-rational floor recovers an exact multiple\n # without ever rounding up a genuine fractional remainder --\n # by construction, not by an epsilon's sizing.\n rpc = 1012.50\n budget = rpc * 100_000\n assert fs.compute_contracts(budget, rpc) == 100_000\n assert fs.compute_contracts(budget - 1.0, rpc) == 99_999\n\n def test_zero_budget_floors_to_zero(self):\n assert fs.compute_contracts(0.0, 1012.50) == 0\n\n def test_sub_one_contract_floors_to_zero(self):\n assert fs.compute_contracts(999.99, 1012.50) == 0\n\n def test_max_contracts_caps_when_lower(self):\n assert fs.compute_contracts(10_000.0, 100.0, max_contracts=3) == 3\n\n def test_max_contracts_no_effect_when_higher(self):\n assert fs.compute_contracts(300.0, 100.0, max_contracts=10) == 3\n\n def test_max_contracts_none_means_uncapped(self):\n assert fs.compute_contracts(1000.0, 100.0, max_contracts=None) == 10\n\n # --- Code review round 3, P1-1 (and its own re-review): the relative\n # epsilon rounded UP past the actual risk budget at large q; the\n # absolute-epsilon + hard-post-condition-loop fix that replaced it\n # could not terminate in practice at large scale (float64 loses the\n # ability to represent `(contracts - 1) * rpc != contracts * rpc` once\n # contracts is huge, so the loop's exit condition never becomes true).\n # Both are now replaced by an EXACT rational floor (Fraction), which\n # has no epsilon, no loop, and no representable-difference failure\n # mode -- it terminates in O(1) and is exact by construction. ---\n\n def test_p1_1_exact_repro_does_not_round_up_past_budget(self):\n # Exact reviewer repro: budget=$99,999,999,950, rpc=$1,000 ->\n # q=99999999.95 (a true 0.05-contract shortfall). The relative-\n # epsilon bug rounded this UP to 100,000,000 -- $50 over budget,\n # still SIZED. Must floor down to 99999999.\n budget = 99_999_999_950.0\n rpc = 1_000.0\n contracts = fs.compute_contracts(budget, rpc)\n assert contracts == 99_999_999\n assert contracts * rpc \u003c= budget\n\n def test_hang_repro_terminates_instantly_and_is_rejected_as_absurd(self):\n # The reviewer's exact hang repro for the (now-removed)\n # epsilon+while design: at this scale, `(contracts - 1) * rpc`\n # and `contracts * rpc` are bit-for-bit identical in float64, so\n # the old post-condition loop's exit test never became true and\n # it decremented toward zero one candidate contract at a time --\n # computationally indistinguishable from a hang (~2.4e285\n # iterations). The exact-rational floor computes the (equally\n # absurd) answer in O(1); CONTRACTS_SANITY_MAX then rejects it\n # outright as economically implausible, exit 2, rather than\n # returning a technically-correct nonsense SIZED report.\n distance = 1.0 # |entry - stop| = |2 - 1|\n multiplier = 1.4296227991821346e-275\n rpc = distance * multiplier\n account_size = 341482236954.82006\n risk_pct = 10.0\n budget = account_size * risk_pct / 100.0\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.compute_contracts(budget, rpc)\n assert exc_info.value.reason == \"contracts_implausibly_large\"\n\n def test_money_safety_correction_3_times_rpc_exceeds_0_9_budget(self):\n # The epsilon-based designs' \"under-count fix\" was itself a bug at\n # this exact scale: rpc = 0.1 * 3 = 0.30000000000000004 (slightly\n # ABOVE the mathematical 0.3), so 3 * rpc = 0.9000000000000001,\n # which EXCEEDS a separately-specified 0.9 budget. 3 contracts\n # would cost more than the budget allows in float64 terms; the\n # money-safe answer is 2, and the exact-rational floor correctly\n # returns 2 (an epsilon nudge would have pushed this to the wrong\n # answer, 3).\n rpc = 0.1 * 3\n budget = 0.9\n assert 3 * rpc > budget # confirms the money-unsafe direction is real\n contracts = fs.compute_contracts(budget, rpc)\n assert contracts == 2\n assert contracts * rpc \u003c= budget\n\n def test_moderately_large_legit_case_still_sized_under_sanity_cap(self):\n # ~1e8 contracts (a $1 trillion account against a cheap contract)\n # is a legitimate case this module's own tests exercise elsewhere\n # (e.g. the P1-1 repro above) -- it must stay well under\n # CONTRACTS_SANITY_MAX and never be rejected.\n budget = 1_000.0 * 100_000_000 # $100,000,000,000\n rpc = 1_000.0\n contracts = fs.compute_contracts(budget, rpc)\n assert contracts == 100_000_000\n assert contracts \u003c fs.CONTRACTS_SANITY_MAX\n\n def test_invariant_holds_across_a_scale_sweep(self):\n # contracts * rpc \u003c= budget must hold regardless of scale -- exact\n # by construction now, not by trusting any epsilon's sizing. Sweep\n # several orders of magnitude, each with a genuine, deliberately\n # non-round fractional shortfall.\n for rpc in (0.30000000000000004, 1.0, 1012.50, 1_000.0, 7.8125):\n for k in (1, 2, 100, 100_000, 10**8):\n budget = rpc * k - 0.5 * rpc # a true (k - 0.5)x budget\n contracts = fs.compute_contracts(budget, rpc)\n assert contracts * rpc \u003c= budget, (rpc, k, contracts, budget)\n assert contracts \u003c= k - 1, (rpc, k, contracts)\n\n\n# --- Geometry ---------------------------------------------------------------\n\n\nclass TestGeometryOk:\n def test_long_stop_below_entry_ok(self):\n assert fs.geometry_ok(\"LONG\", entry=5000.0, stop=4980.0) is True\n\n def test_long_stop_above_entry_rejected(self):\n assert fs.geometry_ok(\"LONG\", entry=5000.0, stop=5010.0) is False\n\n def test_long_stop_equal_entry_rejected(self):\n assert fs.geometry_ok(\"LONG\", entry=5000.0, stop=5000.0) is False\n\n def test_short_stop_above_entry_ok(self):\n assert fs.geometry_ok(\"SHORT\", entry=5000.0, stop=5010.0) is True\n\n def test_short_stop_below_entry_rejected(self):\n assert fs.geometry_ok(\"SHORT\", entry=5000.0, stop=4980.0) is False\n\n def test_short_stop_equal_entry_rejected(self):\n assert fs.geometry_ok(\"SHORT\", entry=5000.0, stop=5000.0) is False\n\n\n# --- Tick grid ---------------------------------------------------------------\n\n\nclass TestIsOnTickGrid:\n def test_on_grid_decimal(self):\n assert fs.is_on_tick_grid(5000.25, 0.25) is True\n\n def test_off_grid_decimal(self):\n assert fs.is_on_tick_grid(5000.10, 0.25) is False\n\n def test_bond_on_grid_32nds_converted(self):\n # 110'16 = 110 + 16/32 = 110.50, which IS on a 1/32 grid.\n assert fs.is_on_tick_grid(110.50, 0.03125) is True\n\n def test_bond_off_grid_mistyped_decimal(self):\n # The classic trap: someone types \"110.16\" thinking it means 110'16.\n assert fs.is_on_tick_grid(110.16, 0.03125) is False\n\n def test_bond_quarter_32nd_on_grid(self):\n # ZF: 1/4-of-1/32 = 0.0078125\n assert fs.is_on_tick_grid(110.5078125, 0.0078125) is True\n\n def test_float_noise_does_not_false_reject(self):\n # 81 ticks of 0.25 accumulated via subtraction can carry float dust.\n entry = 5000.25\n stop = entry - 0.25 * 81\n assert fs.is_on_tick_grid(stop, 0.25) is True\n\n\nclass TestMeetsMinStopDistance:\n def test_one_tick_exactly_ok(self):\n assert fs.meets_min_stop_distance(5000.25, 5000.00, 0.25) is True\n\n def test_multiple_ticks_ok(self):\n assert fs.meets_min_stop_distance(5000.25, 4980.00, 0.25) is True\n\n def test_less_than_one_tick_rejected(self):\n assert fs.meets_min_stop_distance(5000.20, 5000.10, 0.25) is False\n\n def test_zero_distance_rejected(self):\n assert fs.meets_min_stop_distance(5000.0, 5000.0, 0.25) is False\n\n def test_float_noise_one_tick_not_falsely_rejected(self):\n tick = 0.1 * 3 # non-binary-exact tick size\n entry = 100.0\n stop = entry - tick\n assert fs.meets_min_stop_distance(entry, stop, tick) is True\n\n\n# --- resolve_spec -------------------------------------------------------------\n\n\nclass TestResolveSpec:\n def test_known_symbol_returns_table_row(self):\n specs = {\"ES\": ES_SPEC}\n spec = fs.resolve_spec(\"ES\", specs=specs)\n assert spec[\"multiplier\"] == 50\n assert spec[\"tick_size\"] == 0.25\n\n def test_known_symbol_case_insensitive(self):\n specs = {\"ES\": ES_SPEC}\n spec = fs.resolve_spec(\"es\", specs=specs)\n assert spec[\"cot_symbol\"] == \"ES\"\n\n def test_known_symbol_with_override_conflict_raises(self):\n specs = {\"ES\": ES_SPEC}\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.resolve_spec(\"ES\", specs=specs, multiplier=99)\n assert exc_info.value.reason == \"known_symbol_override_conflict\"\n\n def test_unknown_symbol_without_overrides_raises(self):\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.resolve_spec(\"ZZZZ\", specs={})\n assert exc_info.value.reason == \"unknown_symbol_incomplete_override\"\n\n def test_unknown_symbol_with_partial_overrides_raises(self):\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.resolve_spec(\"ZZZZ\", specs={}, multiplier=50, tick_size=0.25)\n assert exc_info.value.reason == \"unknown_symbol_incomplete_override\"\n\n def test_unknown_symbol_with_full_overrides_builds_adhoc_spec(self):\n spec = fs.resolve_spec(\n \"ZZZZ\", specs={}, multiplier=50, tick_size=0.25, contract_currency=\"USD\"\n )\n assert spec[\"multiplier\"] == 50\n assert spec[\"tick_size\"] == 0.25\n assert spec[\"tick_value\"] == pytest.approx(12.5)\n assert spec[\"currency\"] == \"USD\"\n assert spec[\"source_url\"] is None\n assert spec[\"verified_date\"] is None\n\n\n# --- size_futures_position: mode A (explicit) geometry / off-grid / distance\n\n\nclass TestSizeFuturesPositionModeA:\n def base_kwargs(self, **overrides):\n kwargs = dict(\n symbol=\"ES\",\n direction=\"LONG\",\n entry=5000.25,\n stop=4980.00,\n stop_source=\"operator\",\n spec=ES_SPEC,\n account_size=100_000.0,\n risk_pct=2.0,\n max_contracts=None,\n fx_rate=1.0,\n as_of=\"2026-07-17\",\n )\n kwargs.update(overrides)\n return kwargs\n\n def test_hand_checked_es_long_sized(self):\n result = fs.size_futures_position(**self.base_kwargs())\n assert result[\"sizing_status\"] == \"SIZED\"\n assert result[\"stop_distance_points\"] == pytest.approx(20.25)\n assert result[\"stop_distance_ticks\"] == 81\n assert result[\"risk_per_contract_usd\"] == pytest.approx(1012.50)\n assert result[\"risk_budget_usd\"] == pytest.approx(2000.00)\n assert result[\"contracts\"] == 1\n assert result[\"total_risk_usd\"] == pytest.approx(1012.50)\n assert result[\"no_trade_reason\"] is None\n\n def test_one_pct_risk_yields_zero_contracts_no_trade(self):\n result = fs.size_futures_position(**self.base_kwargs(risk_pct=1.0))\n assert result[\"sizing_status\"] == \"NO_TRADE\"\n assert result[\"no_trade_reason\"] == \"risk_below_one_contract\"\n assert result[\"contracts\"] == 0\n assert result[\"total_risk_usd\"] is None\n assert result[\"risk_pct_of_account\"] is None\n # Math fields still populated per the output contract.\n assert result[\"risk_per_contract_usd\"] == pytest.approx(1012.50)\n assert result[\"risk_budget_usd\"] == pytest.approx(1000.00)\n\n def test_short_mirror(self):\n result = fs.size_futures_position(\n **self.base_kwargs(direction=\"SHORT\", entry=4980.00, stop=5000.25)\n )\n assert result[\"sizing_status\"] == \"SIZED\"\n assert result[\"stop_distance_points\"] == pytest.approx(20.25)\n assert result[\"contracts\"] == 1\n\n def test_geometry_violation_mode_a_raises_config_error(self):\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(\n **self.base_kwargs(direction=\"LONG\", entry=5000.0, stop=5010.0)\n )\n assert exc_info.value.reason == \"direction_stop_mismatch\"\n\n def test_geometry_equality_mode_a_raises_config_error(self):\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(\n **self.base_kwargs(direction=\"LONG\", entry=5000.0, stop=5000.0)\n )\n assert exc_info.value.reason == \"direction_stop_mismatch\"\n\n def test_stop_too_close_mode_a_raises_config_error(self):\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(**self.base_kwargs(entry=5000.20, stop=5000.10))\n assert exc_info.value.reason == \"stop_too_close\"\n\n def test_off_grid_non_bond_symbol_is_warning_only(self):\n result = fs.size_futures_position(**self.base_kwargs(entry=5000.10, stop=4980.00))\n assert result[\"sizing_status\"] == \"SIZED\"\n assert \"off_tick_grid_entry\" in result[\"warnings\"]\n\n def test_bond_entry_off_grid_mode_a_raises_config_error(self):\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(\n **self.base_kwargs(symbol=\"ZB\", spec=ZB_SPEC, entry=110.16, stop=108.00)\n )\n assert exc_info.value.reason == \"entry_off_tick_grid\"\n\n def test_bond_stop_off_grid_mode_a_raises_config_error(self):\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(\n **self.base_kwargs(symbol=\"ZB\", spec=ZB_SPEC, entry=110.50, stop=108.16)\n )\n assert exc_info.value.reason == \"stop_off_tick_grid\"\n\n def test_extreme_bond_entry_gets_overflow_attribution_not_32nds(self):\n # Code review round 3, P3: an extreme --entry on a BOND symbol used\n # to raise the 32nds-notation message (misleading -- the ratio\n # price/tick_size overflowed, this isn't a genuine off-grid\n # notation mistake). Must now fall through to the correctly-\n # attributed risk_per_contract_overflow error instead.\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(\n **self.base_kwargs(symbol=\"ZB\", spec=ZB_SPEC, entry=1e308, stop=1.0)\n )\n assert exc_info.value.reason == \"risk_per_contract_overflow\"\n assert exc_info.value.reason != \"entry_off_tick_grid\"\n message = str(exc_info.value)\n assert \"32nds\" not in message\n assert \"--entry\" in message\n\n def test_normal_off_grid_bond_entry_still_gets_32nds_message(self):\n # Regression guard: an ordinary (finite) off-grid bond price must\n # still get the 32nds-notation message, not be misrouted into the\n # overflow path.\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(\n **self.base_kwargs(symbol=\"ZB\", spec=ZB_SPEC, entry=110.16, stop=108.00)\n )\n assert exc_info.value.reason == \"entry_off_tick_grid\"\n assert \"32nds\" in str(exc_info.value)\n\n def test_risk_pct_above_2_warns(self):\n result = fs.size_futures_position(**self.base_kwargs(risk_pct=3.0))\n assert \"risk_pct_above_2\" in result[\"warnings\"]\n\n def test_risk_pct_at_2_does_not_warn(self):\n result = fs.size_futures_position(**self.base_kwargs(risk_pct=2.0))\n assert \"risk_pct_above_2\" not in result[\"warnings\"]\n\n def test_max_contracts_cap_applied_flag(self):\n result = fs.size_futures_position(**self.base_kwargs(risk_pct=10.0, max_contracts=2))\n assert result[\"max_contracts_cap_applied\"] is True\n assert result[\"contracts\"] == 2\n\n def test_fx_conversion_applied(self):\n result = fs.size_futures_position(**self.base_kwargs(fx_rate=1.25))\n # Compare against the same round(x, 2) the implementation applies for\n # display -- 1265.625 sits exactly on a float64 rounding boundary\n # (round(1265.625, 2) == 1265.62, not .63), so pytest.approx against\n # the unrounded product would be a test artifact, not a real bug.\n assert result[\"risk_per_contract_usd\"] == round(20.25 * 50 * 1.25, 2)\n assert result[\"fx_rate_used\"] == 1.25\n\n\n# --- size_futures_position: float64 overflow defense-in-depth --------------\n#\n# The CLI layer caps --account-size/--multiplier/--tick-size/--fx-rate so an\n# individually-extreme flag is an ordinary argparse usage error (see\n# test_futures_position_sizer.py). These tests exercise the pure module's\n# OWN isfinite() guards directly, bypassing any CLI-level cap entirely --\n# the module must never crash with an uncaught OverflowError/ValueError\n# regardless of what a caller (CLI today, anything else tomorrow) passes it.\n\n\nclass TestSizeFuturesPositionOverflowGuards:\n def base_kwargs(self, **overrides):\n kwargs = dict(\n symbol=\"ES\",\n direction=\"LONG\",\n entry=5000.25,\n stop=4980.00,\n stop_source=\"operator\",\n spec=ES_SPEC,\n account_size=100_000.0,\n risk_pct=2.0,\n max_contracts=None,\n fx_rate=1.0,\n as_of=\"2026-07-17\",\n )\n kwargs.update(overrides)\n return kwargs\n\n def test_risk_budget_overflow_raises_config_error(self):\n # account_size * risk_pct / 100 overflows to inf. Individually,\n # 1.5e308 is a \"valid\" (finite, positive) float -- only the PRODUCT\n # overflows, which is exactly what the CLI-level per-flag caps can't\n # catch on their own (repro A from code review round 1).\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(**self.base_kwargs(account_size=1.5e308, risk_pct=10.0))\n assert exc_info.value.reason == \"risk_budget_overflow\"\n\n def test_risk_per_contract_overflow_raises_config_error(self):\n # distance * multiplier * fx_rate overflows to inf via an extreme\n # unknown-symbol-override multiplier (repro B from code review\n # round 1).\n huge_spec = dict(ES_SPEC, multiplier=1e308, tick_size=0.01, tick_value=1e306)\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(**self.base_kwargs(spec=huge_spec))\n assert exc_info.value.reason == \"risk_per_contract_overflow\"\n\n def test_risk_per_contract_overflow_via_extreme_entry_not_multiplier(self):\n # A second route to the SAME overflow: an extreme --entry (which the\n # CLI deliberately does NOT cap -- real prices have no reason to be\n # bounded) combined with an ordinary multiplier. This is the case\n # the argparse-level caps on multiplier/tick-size/fx-rate alone\n # cannot prevent -- only the isfinite() guard on the computed\n # product can.\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(**self.base_kwargs(entry=1e308, stop=1.0))\n assert exc_info.value.reason == \"risk_per_contract_overflow\"\n\n def test_one_trillion_account_size_still_sizes_normally(self):\n # Control case: the largest account size the CLI's new cap allows\n # must still size an ordinary trade correctly -- the guard must\n # never false-positive on a merely LARGE (but finite) value.\n result = fs.size_futures_position(**self.base_kwargs(account_size=1e12, risk_pct=1.0))\n assert result[\"sizing_status\"] == \"SIZED\"\n assert result[\"risk_budget_usd\"] == pytest.approx(1e12 * 1.0 / 100.0)\n assert math.isfinite(result[\"risk_budget_usd\"])\n\n # --- Code review round 3, P1-2: denormal underflow -> risk_per_contract\n # == 0.0 -> ZeroDivisionError inside compute_contracts(), exit 1. 0.0 is\n # perfectly finite, so the isfinite() guard alone never caught this. ---\n\n def test_p1_2_denormal_underflow_raises_config_error_not_zerodiv(self):\n # Exact reviewer repro: entry/stop/multiplier/tick_size all ~1e-308\n # (individually finite and positive, passing every validator), but\n # distance * multiplier underflows to exactly 0.0.\n denormal_spec = dict(ES_SPEC, multiplier=1e-308, tick_size=1e-308, tick_value=1e-616)\n assert 2e-308 * 1e-308 == 0.0 # confirms this repro underflows in this environment\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(\n **self.base_kwargs(entry=2e-308, stop=1e-308, spec=denormal_spec)\n )\n assert exc_info.value.reason == \"risk_per_contract_non_positive\"\n\n def test_risk_per_contract_exactly_zero_raises_config_error(self):\n # Direct construction (not relying on the exact underflow repro\n # above continuing to underflow identically on every platform).\n zero_multiplier_spec = dict(ES_SPEC, multiplier=0.0, tick_value=0.0)\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(**self.base_kwargs(spec=zero_multiplier_spec))\n assert exc_info.value.reason == \"risk_per_contract_non_positive\"\n\n def test_quotient_overflow_now_caught_as_implausibly_large(self):\n # Follow-up to P1-2, superseded by the round-3 exact-rational floor\n # (round-3, second re-review): risk_per_contract can be finite and\n # POSITIVE (not underflowed to exactly 0.0) yet still so tiny\n # relative to a large-but-capped risk_budget that the naive FLOAT\n # quotient would have overflowed to inf. Fraction arithmetic has no\n # such overflow ceiling -- the exact quotient is instead a\n # legitimately astronomical (but finite, exact) integer, which the\n # CONTRACTS_SANITY_MAX check now catches as economically\n # implausible rather than as a numeric overflow. Same end result\n # (a clean ConfigError, exit 2), different, more accurate reason.\n tiny_spec = dict(ES_SPEC, multiplier=1e-300, tick_size=0.25, tick_value=2.5e-301)\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(\n **self.base_kwargs(spec=tiny_spec, account_size=1e12, risk_pct=10.0)\n )\n assert exc_info.value.reason == \"contracts_implausibly_large\"\n\n\n# --- size_futures_position: mode B (gate-supplied stop) --------------------\n\n\nclass TestSizeFuturesPositionModeB:\n def base_kwargs(self, **overrides):\n kwargs = dict(\n symbol=\"ES\",\n direction=\"LONG\",\n entry=5000.25,\n stop=4980.00,\n stop_source=\"gate\",\n spec=ES_SPEC,\n account_size=100_000.0,\n risk_pct=2.0,\n max_contracts=None,\n fx_rate=1.0,\n as_of=\"2026-07-17\",\n gate_block={\n \"report_path\": \"reports/gate.json\",\n \"setup_status\": \"READY_FOR_PLAN\",\n \"gate_confidence\": \"HIGH\",\n \"warnings\": [],\n },\n )\n kwargs.update(overrides)\n return kwargs\n\n def test_geometry_violation_mode_b_is_no_trade_not_config_error(self):\n result = fs.size_futures_position(\n **self.base_kwargs(direction=\"LONG\", entry=5000.0, stop=5010.0)\n )\n assert result[\"sizing_status\"] == \"NO_TRADE\"\n assert result[\"no_trade_reason\"] == \"entry_on_wrong_side_of_stop\"\n\n def test_stop_too_close_mode_b_is_no_trade(self):\n result = fs.size_futures_position(**self.base_kwargs(entry=5000.20, stop=5000.10))\n assert result[\"sizing_status\"] == \"NO_TRADE\"\n assert result[\"no_trade_reason\"] == \"gate_stop_too_close\"\n\n def test_bond_stop_off_grid_mode_b_is_no_trade(self):\n result = fs.size_futures_position(\n **self.base_kwargs(symbol=\"ZB\", spec=ZB_SPEC, entry=110.50, stop=108.16)\n )\n assert result[\"sizing_status\"] == \"NO_TRADE\"\n assert result[\"no_trade_reason\"] == \"gate_stop_off_tick_grid\"\n\n def test_bond_entry_off_grid_mode_b_still_config_error(self):\n # Entry is ALWAYS operator-supplied, even in mode B -- this must\n # still raise, not degrade to NO_TRADE.\n with pytest.raises(fs.ConfigError) as exc_info:\n fs.size_futures_position(\n **self.base_kwargs(symbol=\"ZB\", spec=ZB_SPEC, entry=110.16, stop=108.00)\n )\n assert exc_info.value.reason == \"entry_off_tick_grid\"\n\n def test_sized_result_carries_gate_block(self):\n result = fs.size_futures_position(**self.base_kwargs())\n assert result[\"gate\"][\"setup_status\"] == \"READY_FOR_PLAN\"\n assert result[\"gate\"][\"gate_confidence\"] == \"HIGH\"\n\n\n# --- Gate report normalization ----------------------------------------------\n\n\ndef _ready_gate_report(**overrides):\n report = {\n \"schema_version\": \"1.0\",\n \"symbol\": \"B6\",\n \"setup_status\": \"READY_FOR_PLAN\",\n \"direction\": \"SHORT\",\n \"invalidation_level\": 1.3450,\n \"gate_confidence\": \"HIGH\",\n \"warnings\": [\"price_action_confidence_medium\"],\n }\n report.update(overrides)\n return report\n\n\nclass TestNormalizeGateReport:\n def test_unreadable_load_error(self):\n g = fs.normalize_gate_report(None, \"unreadable\", symbol=None)\n assert g.usable is False\n assert g.reason == \"gate_json_unreadable\"\n\n def test_parse_error_load_error(self):\n g = fs.normalize_gate_report(None, \"parse_error\", symbol=None)\n assert g.usable is False\n assert g.reason == \"gate_json_parse_error\"\n\n def test_non_finite_load_error(self):\n g = fs.normalize_gate_report(None, \"non_finite\", symbol=None)\n assert g.usable is False\n assert g.reason == \"gate_json_non_finite\"\n\n def test_top_level_not_dict_is_malformed(self):\n g = fs.normalize_gate_report([1, 2, 3], None, symbol=None)\n assert g.usable is False\n assert g.reason == \"gate_json_malformed\"\n\n def test_unsupported_schema_version(self):\n g = fs.normalize_gate_report(_ready_gate_report(schema_version=\"2.0\"), None, symbol=\"B6\")\n assert g.usable is False\n assert g.reason == \"gate_json_schema_unsupported\"\n\n def test_missing_schema_version(self):\n report = _ready_gate_report()\n del report[\"schema_version\"]\n g = fs.normalize_gate_report(report, None, symbol=\"B6\")\n assert g.usable is False\n assert g.reason == \"gate_json_schema_unsupported\"\n\n def test_symbol_mismatch(self):\n g = fs.normalize_gate_report(_ready_gate_report(), None, symbol=\"ES\")\n assert g.usable is False\n assert g.reason == \"gate_symbol_mismatch\"\n\n # --- Code review round 3 (user re-review), P1-3: an invalid gate-file\n # symbol used to slip past validation (whitespace-only) or reach an\n # output filename unsanitized (path-hostile characters). Both must now\n # fail closed as gate_json_malformed -- exit 0, a report IS written,\n # naming the reason -- never a crash and never an unsafe filename.\n\n def test_whitespace_only_symbol_is_malformed_not_usable(self):\n # A non-empty string of spaces is truthy in Python, so a naive\n # `not report_symbol` check does NOT catch this -- it must be\n # caught by validating the STRIPPED value against the allowlist.\n g = fs.normalize_gate_report(_ready_gate_report(symbol=\" \"), None, symbol=None)\n assert g.usable is False\n assert g.reason == \"gate_json_malformed\"\n\n def test_path_hostile_symbol_is_malformed_not_usable(self):\n # \"A/B\" must never become GateNormalized.symbol -- it would\n # otherwise flow straight into an output filename path component.\n g = fs.normalize_gate_report(_ready_gate_report(symbol=\"A/B\"), None, symbol=None)\n assert g.usable is False\n assert g.reason == \"gate_json_malformed\"\n assert g.symbol is None\n\n def test_symbol_with_leading_trailing_whitespace_is_normalized_and_accepted(self):\n # A genuinely valid symbol surrounded by incidental whitespace\n # should still work -- strip-then-validate, not reject outright.\n g = fs.normalize_gate_report(_ready_gate_report(symbol=\" B6 \"), None, symbol=None)\n assert g.usable is True\n assert g.symbol == \"B6\"\n\n def test_overlong_symbol_is_malformed(self):\n g = fs.normalize_gate_report(_ready_gate_report(symbol=\"A\" * 13), None, symbol=None)\n assert g.usable is False\n assert g.reason == \"gate_json_malformed\"\n\n def test_symbol_taken_from_report_when_omitted(self):\n g = fs.normalize_gate_report(_ready_gate_report(), None, symbol=None)\n assert g.usable is True\n assert g.symbol == \"B6\"\n\n def test_symbol_case_insensitive_match(self):\n g = fs.normalize_gate_report(_ready_gate_report(), None, symbol=\"b6\")\n assert g.usable is True\n\n def test_non_ready_status_refused(self):\n for status in (\n \"WATCHING_PRICE\",\n \"CROWDED\",\n \"REJECTED\",\n \"INSUFFICIENT_EVIDENCE\",\n \"SOMETHING_UNKNOWN\",\n ):\n g = fs.normalize_gate_report(\n _ready_gate_report(setup_status=status, direction=None, invalidation_level=None),\n None,\n symbol=\"B6\",\n )\n assert g.usable is False, status\n assert g.reason == \"gate_not_ready\", status\n assert g.setup_status == status\n\n def test_not_ready_still_echoes_confidence_and_warnings(self):\n g = fs.normalize_gate_report(\n _ready_gate_report(\n setup_status=\"WATCHING_PRICE\", direction=None, invalidation_level=None\n ),\n None,\n symbol=\"B6\",\n )\n assert g.gate_confidence == \"HIGH\"\n assert \"price_action_confidence_medium\" in g.warnings\n\n def test_invalid_direction_type(self):\n g = fs.normalize_gate_report(_ready_gate_report(direction=\"SIDEWAYS\"), None, symbol=\"B6\")\n assert g.usable is False\n assert g.reason == \"gate_json_invalid_direction\"\n\n def test_missing_direction(self):\n report = _ready_gate_report()\n report[\"direction\"] = None\n g = fs.normalize_gate_report(report, None, symbol=\"B6\")\n assert g.usable is False\n assert g.reason == \"gate_json_invalid_direction\"\n\n @pytest.mark.parametrize(\n \"bad_value\",\n [0, -1.5, float(\"inf\"), float(\"nan\"), True, \"1.30\", None],\n )\n def test_invalid_invalidation_level(self, bad_value):\n g = fs.normalize_gate_report(\n _ready_gate_report(invalidation_level=bad_value), None, symbol=\"B6\"\n )\n assert g.usable is False\n assert g.reason == \"gate_json_invalid_invalidation_level\"\n\n def test_fully_valid_ready_report(self):\n g = fs.normalize_gate_report(_ready_gate_report(), None, symbol=\"B6\")\n assert g.usable is True\n assert g.reason is None\n assert g.direction == \"SHORT\"\n assert g.invalidation_level == pytest.approx(1.3450)\n assert g.gate_confidence == \"HIGH\"\n assert g.setup_status == \"READY_FOR_PLAN\"\n assert \"price_action_confidence_medium\" in g.warnings\n\n def test_non_list_warnings_degrades_to_empty(self):\n g = fs.normalize_gate_report(_ready_gate_report(warnings=\"not-a-list\"), None, symbol=\"B6\")\n assert g.usable is True\n assert g.warnings == ()\n\n def test_non_string_gate_confidence_degrades_to_none(self):\n g = fs.normalize_gate_report(_ready_gate_report(gate_confidence=123), None, symbol=\"B6\")\n assert g.usable is True\n assert g.gate_confidence is None\n\n\n# --- build_gate_failure_result -----------------------------------------------\n\n\nclass TestBuildGateFailureResult:\n def test_carries_reason_and_entry(self):\n result = fs.build_gate_failure_result(\n symbol=\"B6\",\n entry=1.345,\n reason=\"gate_not_ready\",\n as_of=\"2026-07-17\",\n report_path=\"reports/gate.json\",\n setup_status=\"CROWDED\",\n )\n assert result[\"sizing_status\"] == \"NO_TRADE\"\n assert result[\"no_trade_reason\"] == \"gate_not_ready\"\n assert result[\"entry\"] == 1.345\n assert result[\"stop\"] is None\n assert result[\"contracts\"] == 0\n assert result[\"gate\"][\"setup_status\"] == \"CROWDED\"\n # allow_nan=False safety: nothing non-finite anywhere in the result.\n assert not fs.contains_non_finite(result)\n\n\n# --- Currency / fx-rate guard -------------------------------------------------\n\n\nclass TestRequiresFxRate:\n def test_usd_does_not_require_fx_rate(self):\n assert fs.requires_fx_rate(\"USD\") is False\n\n def test_non_usd_requires_fx_rate(self):\n assert fs.requires_fx_rate(\"GBP\") is True\n\n def test_case_insensitive(self):\n assert fs.requires_fx_rate(\"usd\") is False\n\n\n# --- contains_non_finite (reused iterative whole-structure scan) -----------\n\n\nclass TestContainsNonFinite:\n def test_clean_structure_false(self):\n assert fs.contains_non_finite({\"a\": 1, \"b\": [1.0, 2.0, {\"c\": \"x\"}]}) is False\n\n def test_top_level_nan(self):\n assert fs.contains_non_finite(float(\"nan\")) is True\n\n def test_nested_inf(self):\n assert fs.contains_non_finite({\"a\": [1, {\"b\": float(\"inf\")}]}) is True\n\n def test_deep_but_finite_structure_no_recursion_error(self):\n deep = 1.0\n for _ in range(5000):\n deep = [deep]\n assert fs.contains_non_finite(deep) is False\n\n\n# --- Contract-spec table: table-wide invariant + independent spot-checks ---\n#\n# The table-wide test mechanically catches a transcription error (tick_value\n# not matching multiplier * tick_size) on ANY of the 23 rows. The spot-check\n# literals below are pinned INDEPENDENTLY of the table -- i.e. hand-typed\n# from the official contract-spec pages, not derived by reading\n# fs.CONTRACT_SPECS -- so they would catch a wrong EDIT to the table itself\n# (a bad tick_value that still happens to equal multiplier * tick_size would\n# pass the invariant test but fail these).\n\nCORE_SYMBOLS = frozenset(\n {\n \"ES\", \"NQ\", \"YM\", \"QR\", \"VX\",\n \"ZT\", \"ZF\", \"ZN\", \"ZB\",\n \"DX\", \"E6\", \"J6\", \"B6\", \"A6\", \"D6\", \"S6\",\n \"GC\", \"SI\", \"HG\", \"PL\",\n \"CL\", \"NG\",\n \"BT\",\n }\n) # fmt: skip\n\n\nclass TestContractSpecTableCompleteness:\n def test_table_has_exactly_the_23_core_symbols(self):\n assert set(fs.CONTRACT_SPECS) == CORE_SYMBOLS\n\n def test_every_row_has_required_keys(self):\n required = {\n \"exchange_product\",\n \"multiplier\",\n \"tick_size\",\n \"tick_value\",\n \"currency\",\n \"exchange\",\n \"source_url\",\n \"verified_date\",\n }\n for symbol, row in fs.CONTRACT_SPECS.items():\n assert required.issubset(row.keys()), symbol\n\n\nclass TestContractSpecTableInvariants:\n def test_tick_value_equals_multiplier_times_tick_size_for_every_row(self):\n for symbol, row in fs.CONTRACT_SPECS.items():\n expected = row[\"multiplier\"] * row[\"tick_size\"]\n assert row[\"tick_value\"] == pytest.approx(expected, rel=1e-9), symbol\n\n def test_every_row_is_usd_quoted(self):\n # Named gotcha: CME FX futures (E6/J6/B6/A6/D6/S6) and ICE DX have\n # non-USD contract SIZES (e.g. B6 is GBP 62,500) but are all\n # QUOTE-currency USD -- this is the currency this table records.\n for symbol, row in fs.CONTRACT_SPECS.items():\n assert row[\"currency\"] == \"USD\", symbol\n\n def test_every_row_is_verified(self):\n # Fails until every row carries a real source_url/verified_date --\n # this is the money-critical gate: the table must never ship with a\n # PROVISIONAL/unverified row.\n for symbol, row in fs.CONTRACT_SPECS.items():\n assert row[\"source_url\"], f\"{symbol} has no verified source_url\"\n assert row[\"verified_date\"], f\"{symbol} has no verified_date\"\n\n\nclass TestContractSpecSpotChecks:\n \"\"\"Independent literal spot-checks, NOT derived from the table -- these\n catch a wrong edit to CONTRACT_SPECS itself, not just an internal\n inconsistency within a row.\"\"\"\n\n def test_es_tick_value_is_12_50(self):\n assert fs.CONTRACT_SPECS[\"ES\"][\"tick_value\"] == pytest.approx(12.50)\n assert fs.CONTRACT_SPECS[\"ES\"][\"multiplier\"] == 50\n assert fs.CONTRACT_SPECS[\"ES\"][\"tick_size\"] == pytest.approx(0.25)\n\n def test_gc_tick_value_is_10_00(self):\n assert fs.CONTRACT_SPECS[\"GC\"][\"tick_value\"] == pytest.approx(10.00)\n assert fs.CONTRACT_SPECS[\"GC\"][\"multiplier\"] == 100\n assert fs.CONTRACT_SPECS[\"GC\"][\"tick_size\"] == pytest.approx(0.10)\n\n def test_zb_tick_value_is_31_25(self):\n # 1/32 of a point on a $100,000-par, $1000/point contract.\n assert fs.CONTRACT_SPECS[\"ZB\"][\"tick_value\"] == pytest.approx(31.25)\n assert fs.CONTRACT_SPECS[\"ZB\"][\"multiplier\"] == 1000\n assert fs.CONTRACT_SPECS[\"ZB\"][\"tick_size\"] == pytest.approx(1 / 32)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":42562,"content_sha256":"a52e88bc3187978026ee588367d349f1a5e817ba1990f5bf9eda5c9da7d29efb"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Futures Position Sizer","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"Shapiro pipeline step 4: convert a direction, entry price, and stop-loss into a contract count, given an account risk budget and a verified contract spec (multiplier, tick size, tick value). This is a NEW, separate skill from ","type":"text"},{"text":"position-sizer","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- futures contracts are leveraged, multiplier-based instruments with wildly different dollar-per-point values (a $0.25 move is $12.50 on ES but $5.00 on NQ and $31.25 on ZB); reusing the equity share-count sizer for futures would silently produce wrong position sizes.","type":"text"}]},{"type":"paragraph","content":[{"text":"Two ways to size a trade:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mode A (explicit)","type":"text","marks":[{"type":"strong"}]},{"text":": supply ","type":"text"},{"text":"--symbol --direction --entry --stop","type":"text","marks":[{"type":"code_inline"}]},{"text":" directly.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mode B (gate handoff)","type":"text","marks":[{"type":"strong"}]},{"text":": supply ","type":"text"},{"text":"--gate-json \u003ccontrarian-setup-gate report> --entry","type":"text","marks":[{"type":"code_inline"}]},{"text":". Direction and stop (the gate's ","type":"text"},{"text":"invalidation_level","type":"text","marks":[{"type":"code_inline"}]},{"text":") come from the gate's ","type":"text"},{"text":"READY_FOR_PLAN","type":"text","marks":[{"type":"code_inline"}]},{"text":" report -- the sizer never sizes a setup the gate has not confirmed as READY, and never accepts an explicit ","type":"text"},{"text":"--direction","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"--stop","type":"text","marks":[{"type":"code_inline"}]},{"text":" alongside ","type":"text"},{"text":"--gate-json","type":"text","marks":[{"type":"code_inline"}]},{"text":" (the gate is authoritative when provided).","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"--entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ALWAYS required, in both modes -- neither this skill nor the gate ever derives an entry price; the operator supplies it.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After ","type":"text"},{"text":"contrarian-setup-gate","type":"text","marks":[{"type":"code_inline"}]},{"text":" reaches ","type":"text"},{"text":"READY_FOR_PLAN","type":"text","marks":[{"type":"code_inline"}]},{"text":" and you need a contract count for the confirmed direction and stop","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User asks \"how many ES/NQ/GC/CL/... contracts should I trade?\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User has a futures trade idea with a known entry and stop and wants risk-based sizing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User wants to check the verified contract spec (multiplier/tick size/tick value) for a symbol before sizing (","type":"text"},{"text":"--list-specs","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Python 3.9+, standard library only -- no API keys, fully offline","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A direction, entry, and stop (mode A), or a ","type":"text"},{"text":"contrarian-setup-gate","type":"text","marks":[{"type":"code_inline"}]},{"text":" JSON report with ","type":"text"},{"text":"setup_status: READY_FOR_PLAN","type":"text","marks":[{"type":"code_inline"}]},{"text":" (mode B)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For a symbol outside the verified 23-market core table: its multiplier, tick size, and quote currency (all three, together)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1: Size the Position","type":"text"}]},{"type":"paragraph","content":[{"text":"Mode A -- explicit:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 skills/futures-position-sizer/scripts/futures_position_sizer.py \\\n --symbol ES --direction LONG --entry 5000.25 --stop 4980.00 \\\n --account-size 100000 --risk-pct 1.0 \\\n --output-dir reports/ --format both","type":"text"}]},{"type":"paragraph","content":[{"text":"Mode B -- gate handoff:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 skills/futures-position-sizer/scripts/futures_position_sizer.py \\\n --gate-json reports/contrarian_setup_gate_B6_2026-07-15.json \\\n --entry 1.3400 \\\n --account-size 100000 --risk-pct 1.0 \\\n --output-dir reports/ --format both","type":"text"}]},{"type":"paragraph","content":[{"text":"--symbol","type":"text","marks":[{"type":"code_inline"}]},{"text":" may be omitted in mode B -- it is taken from the gate report. If both are given, they must match (","type":"text"},{"text":"gate_symbol_mismatch","type":"text","marks":[{"type":"code_inline"}]},{"text":" otherwise). ","type":"text"},{"text":"--direction","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"--stop","type":"text","marks":[{"type":"code_inline"}]},{"text":" are rejected alongside ","type":"text"},{"text":"--gate-json","type":"text","marks":[{"type":"code_inline"}]},{"text":" (usage error, exit 2) -- pass one mode or the other, never both.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2: Read the Result","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sizing_status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SIZED","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"contracts","type":"text","marks":[{"type":"code_inline"}]},{"text":" >= 1; ","type":"text"},{"text":"total_risk_usd","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"risk_pct_of_account","type":"text","marks":[{"type":"code_inline"}]},{"text":" are the actual risk taken","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"NO_TRADE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Never a crash -- always carries ","type":"text"},{"text":"no_trade_reason","type":"text","marks":[{"type":"code_inline"}]},{"text":". See the reason glossary below","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"A ","type":"text"},{"text":"NO_TRADE","type":"text","marks":[{"type":"code_inline"}]},{"text":" result from ","type":"text"},{"text":"risk_below_one_contract","type":"text","marks":[{"type":"code_inline"}]},{"text":" still reports the full risk math (risk per contract, risk budget, stop distance) -- the account simply cannot afford one contract at this risk percentage and stop distance. Consider a smaller contract (e.g. a micro), deliberately reassess the risk percentage within your existing risk policy, or skip the trade. Keep the stop at the setup-defined invalidation level: widening it increases risk per contract and makes affordability worse; do not tighten it merely to fit the budget.","type":"text"}]},{"type":"paragraph","content":[{"text":"For a smaller contract, use its own symbol and verified specifications; if it is outside the core table, supply ","type":"text"},{"text":"--multiplier","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--tick-size","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"--contract-currency","type":"text","marks":[{"type":"code_inline"}]},{"text":" together (plus ","type":"text"},{"text":"--fx-rate","type":"text","marks":[{"type":"code_inline"}]},{"text":" for non-USD quotes). Do not lower an existing symbol's multiplier to simulate a micro. In mode B, use a ","type":"text"},{"text":"READY_FOR_PLAN","type":"text","marks":[{"type":"code_inline"}]},{"text":" gate for that same smaller-contract symbol; do not relabel the original gate to bypass the symbol check.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3: Check Warnings","type":"text"}]},{"type":"paragraph","content":[{"text":"warnings","type":"text","marks":[{"type":"code_inline"}]},{"text":" (top-level list) never blocks sizing -- it flags audit-worthy conditions: ","type":"text"},{"text":"risk_pct_above_2","type":"text","marks":[{"type":"code_inline"}]},{"text":" (risk above the 2% guideline), ","type":"text"},{"text":"off_tick_grid_entry","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"off_tick_grid_stop","type":"text","marks":[{"type":"code_inline"}]},{"text":" (a non-bond symbol's price is not exactly on the tick grid -- legitimate for a mid-quote, but worth a second look).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4: Inspect the Verified Contract Spec Table","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 skills/futures-position-sizer/scripts/futures_position_sizer.py --list-specs","type":"text"}]},{"type":"paragraph","content":[{"text":"Prints the full 23-market core table (multiplier, tick size, tick value, currency, exchange) sourced from official exchange contract-spec pages -- see ","type":"text"},{"text":"references/futures-contract-specs.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the per-row source URLs and verification dates.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Worked Example: Bond Off-Grid Guard (32nds -> Decimal)","type":"text"}]},{"type":"paragraph","content":[{"text":"Bond/note futures (ZT, ZF, ZN, ZB) quote in fractions of a point (32nds, or 32nds-of-32nds), commonly written with an apostrophe: ","type":"text"},{"text":"110'16","type":"text","marks":[{"type":"code_inline"}]},{"text":" means ","type":"text"},{"text":"110 + 16/32 = 110.50","type":"text","marks":[{"type":"code_inline"}]},{"text":". Typing ","type":"text"},{"text":"110.16","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead -- reading the digits after the apostrophe as if they were decimal cents -- is a classic, silent, wrong-money-math mistake: ","type":"text"},{"text":"110.16","type":"text","marks":[{"type":"code_inline"}]},{"text":" is not on the ZB tick grid (","type":"text"},{"text":"0.03125","type":"text","marks":[{"type":"code_inline"}]},{"text":" = 1/32) at all.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# WRONG -- 110.16 is not on the 1/32 grid; this is almost certainly a\n# mistyped \"110'16\" (which means 110.50). Exits 2, no report written:\npython3 skills/futures-position-sizer/scripts/futures_position_sizer.py \\\n --symbol ZB --direction LONG --entry 110.16 --stop 108.00 \\\n --account-size 100000 --risk-pct 1.0\n\n# CORRECT -- decimal points, not the raw 32nds digits:\npython3 skills/futures-position-sizer/scripts/futures_position_sizer.py \\\n --symbol ZB --direction LONG --entry 110.50 --stop 108.00 \\\n --account-size 100000 --risk-pct 1.0","type":"text"}]},{"type":"paragraph","content":[{"text":"Every other symbol in the table quotes in plain decimal points -- an off-grid price there (a mid-quote, for instance) is only a soft ","type":"text"},{"text":"off_tick_grid_*","type":"text","marks":[{"type":"code_inline"}]},{"text":" warning, never a rejection.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Contract","type":"text"}]},{"type":"paragraph","content":[{"text":"Writes ","type":"text"},{"text":"futures_position_size_\u003cSYMBOL>_\u003cas-of>.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"--output-dir","type":"text","marks":[{"type":"code_inline"}]},{"text":" when ","type":"text"},{"text":"--format json|both","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"--format text|both","type":"text","marks":[{"type":"code_inline"}]},{"text":" prints a formatted summary to stdout. ","type":"text"},{"text":"--as-of","type":"text","marks":[{"type":"code_inline"}]},{"text":" defaults to today (this is an operator-time sizing tool, not a backtest tool).","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"schema_version: \"1.0\"\nsymbol: ES\ndirection: LONG\nsizing_status: SIZED | NO_TRADE\nno_trade_reason: null | risk_below_one_contract | gate_not_ready | gate_symbol_mismatch | ...\nentry: 5000.25\nstop: 4980.00\nstop_distance_points: 20.25\nstop_distance_ticks: 81\ncontract_spec: {multiplier: 50, tick_size: 0.25, tick_value: 12.5, currency: USD, source: cme, verified: \"2026-07-17\"}\nrisk_per_contract_usd: 1012.50\nrisk_budget_usd: 2000.00\ncontracts: 1\ntotal_risk_usd: 1012.50\nrisk_pct_of_account: 1.01\nmax_contracts_cap_applied: false\nfx_rate_used: 1.0\nmargin_note: \"Exchange margin requirements are broker/time-dependent and NOT computed here; verify initial/maintenance margin with your broker.\"\ngate: {report_path, setup_status, gate_confidence, warnings} # mode B only\nwarnings: []\nrun_context: {symbol, as_of, schema_version, skill}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Guardrails","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never sizes a position without an explicit stop.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"--stop","type":"text","marks":[{"type":"code_inline"}]},{"text":" is required in mode A; mode B refuses to size (","type":"text"},{"text":"gate_not_ready","type":"text","marks":[{"type":"code_inline"}]},{"text":") until the gate itself reports ","type":"text"},{"text":"READY_FOR_PLAN","type":"text","marks":[{"type":"code_inline"}]},{"text":" with a valid ","type":"text"},{"text":"invalidation_level","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Floor, never round up -- exact by construction, no epsilon.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"contracts = floor(risk_budget / risk_per_contract)","type":"text","marks":[{"type":"code_inline"}]},{"text":" is computed with exact rational arithmetic (Python's ","type":"text"},{"text":"Fraction","type":"text","marks":[{"type":"code_inline"}]},{"text":", not float division), so ","type":"text"},{"text":"contracts * risk_per_contract \u003c= risk_budget","type":"text","marks":[{"type":"code_inline"}]},{"text":" holds by construction -- no epsilon nudge, no float-representation edge case, and no risk of ever exceeding the budget. Also rejected outright if the resulting count is economically implausible (an absurd input like a denormal-scale multiplier override). Zero contracts is a legitimate, fail-closed ","type":"text"},{"text":"NO_TRADE","type":"text","marks":[{"type":"code_inline"}]},{"text":" outcome, not an error.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Two fail-closed classes, matched to who supplied the bad value.","type":"text","marks":[{"type":"strong"}]},{"text":" An operator-caused problem (an explicit ","type":"text"},{"text":"--stop","type":"text","marks":[{"type":"code_inline"}]},{"text":" on the wrong side of ","type":"text"},{"text":"--entry","type":"text","marks":[{"type":"code_inline"}]},{"text":", a stop closer than one tick, a bond price typed off the tick grid) is a usage error: exit 2, no report written. The identical class of problem on a value that came from the untrusted gate-report file (mode B's stop) is instead a fail-closed ","type":"text"},{"text":"NO_TRADE","type":"text","marks":[{"type":"code_inline"}]},{"text":" result: exit 0, a report IS written, naming the reason -- this never crashes on a bad or not-yet-ready gate file, matching every other skill in this pipeline.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bond-family off-grid prices are a hard rejection, not a warning.","type":"text","marks":[{"type":"strong"}]},{"text":" ZT/ZF/ZN/ZB quote in 32nds/64ths notation; a price that doesn't land on the tick grid is almost certainly a notation mistype and would silently produce wrong money math if sized. Every other symbol only warns.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Margin is never computed.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"margin_note","type":"text","marks":[{"type":"code_inline"}]},{"text":" is a static, never-stale reminder -- margin requirements are broker- and time-dependent; this skill does not estimate them.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Currency-aware.","type":"text","marks":[{"type":"strong"}]},{"text":" Every core-table symbol is USD-quoted (confirmed by a table-wide unit test), including the CME FX futures whose contract SIZE is denominated in a foreign currency (e.g. B6's GBP 62,500) but which trade and settle in USD. A symbol quoted in a non-USD currency (via ","type":"text"},{"text":"--contract-currency","type":"text","marks":[{"type":"code_inline"}]},{"text":" override) requires an explicit ","type":"text"},{"text":"--fx-rate","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- there is no silent default.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not investment advice.","type":"text","marks":[{"type":"strong"}]},{"text":" This skill performs risk-based arithmetic on operator-supplied or gate-confirmed inputs; it does not recommend a trade, a direction, or an entry.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Resources","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/futures_position_sizer.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- CLI: argument parsing, hardened gate-json loading (unreadable / parse_error incl. RecursionError / non_finite via an iterative whole-file scan), report generation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/futures_sizing.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- Pure sizing core: numeric validators, the verified 23-market contract-spec table, risk math, the floor algorithm, tick-grid guards, and gate-report shape normalization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/futures-contract-specs.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- The verified contract-spec table with per-row official source URLs and verification dates","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/sizing-methodology.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- Formulas, the exact-rational floor algorithm's rationale, the fail-closed exit-code convention, and worked examples (ES long, B6 short via gate handoff)","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"futures-position-sizer","author":"@skillopedia","source":{"stars":3552,"repo_name":"claude-trading-skills","origin_url":"https://github.com/tradermonty/claude-trading-skills/blob/HEAD/skills/futures-position-sizer/SKILL.md","repo_owner":"tradermonty","body_sha256":"4ffecedcbe629b6381c0521f6ef6cac020c8d63403dd3edc022b2324b9b6ad13","cluster_key":"a117ffb606a507e48f0a72592bd5a6ea6e3c90becf4b9acb800da8205f304004","clean_bundle":{"format":"clean-skill-bundle-v1","source":"tradermonty/claude-trading-skills/skills/futures-position-sizer/SKILL.md","text_bytes":178781,"source_commit":"d8e71ce2f0b069eb5b6fca352857c168c35e6c46","skills_sh_rank":7212,"attachment_count":8,"text_attachments":8,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/futures-position-sizer/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"finance-legal-compliance","category_label":"Finance"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"finance-legal-compliance","import_tag":"skills-sh-top20k-2026-11","description":"Calculate contract-based futures position sizes from a direction, entry, and stop-loss, using verified per-symbol contract specs (multiplier, tick size, tick value). Use when the user asks how many futures contracts to trade, wants to size a futures position (ES, NQ, ZB, GC, CL, 6E/E6, VX, BT, ...), or is handing off a contrarian-setup-gate READY_FOR_PLAN direction/invalidation_level for sizing. Pure, offline calculation -- no API keys, no network."}},"renderedAt":1793671491923}

Futures Position Sizer Overview Shapiro pipeline step 4: convert a direction, entry price, and stop-loss into a contract count, given an account risk budget and a verified contract spec (multiplier, tick size, tick value). This is a NEW, separate skill from -- futures contracts are leveraged, multiplier-based instruments with wildly different dollar-per-point values (a $0.25 move is $12.50 on ES but $5.00 on NQ and $31.25 on ZB); reusing the equity share-count sizer for futures would silently produce wrong position sizes. Two ways to size a trade: - Mode A (explicit) : supply directly. - Mode…