148 lines
8.7 KiB
Markdown
148 lines
8.7 KiB
Markdown
|
|
# VIOLET Sub-Spec — L3 Exchange-Leverage Wrapper (parallel-developable unit)
|
|||
|
|
|
|||
|
|
**Status:** READY TO BUILD, independently. Self-contained brief for an autonomous agent
|
|||
|
|
running **on this host** (`/mnt/dolphinng5_predict`, no git remote). Python
|
|||
|
|
`/home/dolphin/siloqy_env/bin/python3`. Branch `exp/pink-ditav2-sprint0-20260530`.
|
|||
|
|
This is **V3.5** of `VIOLET_DEV_SPEC_AND_PLAN.md`. Develop in parallel with V3.4
|
|||
|
|
(DecisionEngine↔Sizing integration, owned by the lead) — **zero file overlap.**
|
|||
|
|
|
|||
|
|
## 1. Objective
|
|||
|
|
|
|||
|
|
Wrap BLUE's conviction→exchange-leverage mapping into a V-TYPES-bounded VIOLET L3
|
|||
|
|
component, **bit-identical** to the real mapping in `prod/bingx/leverage.py`. This is the
|
|||
|
|
"tradeability" side of the dual-leverage: the bet-sizer's internal **conviction leverage**
|
|||
|
|
[0.5, 9.0] sizes the QUANTITY; the **exchange leverage** [1, 3] (BingX integer, conservatively
|
|||
|
|
capped) is derived from it at the venue boundary. VIOLET needs a typed, observable wrapper
|
|||
|
|
for this so V4 (execution) can set venue leverage faithfully.
|
|||
|
|
|
|||
|
|
## 2. Non-negotiable constraints
|
|||
|
|
|
|||
|
|
- **WRAP, DON'T REIMPLEMENT.** Call the real `prod/bingx/leverage.py` functions; do not
|
|||
|
|
re-derive the linear map / rounding. Bit-identity is the gate.
|
|||
|
|
- **ZERO edits to shared files** (`prod/bingx/leverage.py`, `prod/nautilus_event_trader.py`,
|
|||
|
|
`prod/clean_arch/dita_v2/*`, `nautilus_dolphin/**`, `blue_parity.py`). Per-commit:
|
|||
|
|
`git diff --cached --name-only` must not contain them.
|
|||
|
|
- **V-TYPES** (`prod/clean_arch/violet/domain.py`): refined types at boundaries, `@typed`
|
|||
|
|
(beartype) on public methods, `StrictModel` value objects. Only poison guards
|
|||
|
|
(finite + in-domain); **NO arbitrary magnitude caps BLUE/leverage.py lacks** (a prior
|
|||
|
|
reviewer flagged exactly this liberty in the sizing layer — do not repeat it).
|
|||
|
|
- **Exchange-agnostic naming preserved**: this is the L3 boundary; keep the internal
|
|||
|
|
(conviction) vs exchange distinction explicit in types and field names.
|
|||
|
|
|
|||
|
|
## 3. The wrap target (authoritative — `prod/bingx/leverage.py`, 83 lines, no callers)
|
|||
|
|
|
|||
|
|
Constants: `CONVICTION_MIN=0.5`, `CONVICTION_MAX=9.0`, `EXCHANGE_LEV_MIN=1`,
|
|||
|
|
`EXCHANGE_LEV_MAX=3`, `LEVERAGE_MAPPING_RULE="round_half_even_linear_0.5_to_9.0_to_1_to_exchange_cap"`.
|
|||
|
|
|
|||
|
|
Functions to wrap (exact signatures):
|
|||
|
|
```
|
|||
|
|
def normalize_bingx_leverage_value(leverage, *, exchange_min=EXCHANGE_LEV_MIN,
|
|||
|
|
exchange_max=EXCHANGE_LEV_MAX) -> int
|
|||
|
|
# ROUND_HALF_EVEN(leverage) clamped to [exchange_min, exchange_max] (BingX int-only)
|
|||
|
|
def map_internal_conviction_to_exchange_leverage_target(internal, *, exchange_min=..,
|
|||
|
|
exchange_max=..) -> float
|
|||
|
|
# clamp internal to [0.5,9.0]; linear: exch_min + (internal-0.5)/(9.0-0.5) * (exch_max-exch_min)
|
|||
|
|
def map_internal_conviction_to_exchange_leverage(internal, *, exchange_min=.., exchange_max=..) -> int
|
|||
|
|
# = normalize_bingx_leverage_value(map_..._target(internal), ...) -> final integer exchange leverage
|
|||
|
|
```
|
|||
|
|
**Behaviours that MUST round-trip bit-identically:** the [0.5,9.0] clamp of out-of-range
|
|||
|
|
conviction; the linear interpolation; **ROUND_HALF_EVEN** (banker's rounding — x.5 cases
|
|||
|
|
round to even, e.g. 1.5→2, 2.5→2); the integer clamp to [exchange_min, exchange_max];
|
|||
|
|
non-default `exchange_min/max` args.
|
|||
|
|
|
|||
|
|
## 4. Deliverable — files to CREATE
|
|||
|
|
|
|||
|
|
### 4.1 `prod/clean_arch/violet/exchange_leverage.py`
|
|||
|
|
|
|||
|
|
- **Refined types** (V-TYPES, in this file or import from domain): reuse
|
|||
|
|
`ConvictionLeverage` from `alpha_wrappers.py` (Annotated float gt/ge 0). New:
|
|||
|
|
`ExchangeLeverage = Annotated[int, Field(ge=1)]` (BingX integer; NO upper-cap liberty —
|
|||
|
|
the function clamps to exchange_max itself).
|
|||
|
|
- **`ExchangeLeverageDecision(StrictModel)`**: `internal_conviction: ConvictionLeverage`,
|
|||
|
|
`target_exchange_leverage: float` (the pre-round target, allow_inf_nan=False),
|
|||
|
|
`exchange_leverage: ExchangeLeverage` (final int), `exchange_min: int`, `exchange_max: int`.
|
|||
|
|
Frozen + extra=forbid.
|
|||
|
|
- **`VioletExchangeLeverage`** class:
|
|||
|
|
- `_import_leverage()`: import `prod.bingx.leverage` (it's an in-repo module; plain
|
|||
|
|
`from prod.bingx import leverage` should work since cwd is the root — verify; no
|
|||
|
|
nautilus_dolphin root-injection needed).
|
|||
|
|
- `__init__(self, *, exchange_min=1, exchange_max=3)`: store caps (defaults = gold/BingX).
|
|||
|
|
- `@typed map_target(self, internal_conviction: float) -> float`: wraps
|
|||
|
|
`map_internal_conviction_to_exchange_leverage_target`.
|
|||
|
|
- `@typed normalize(self, leverage: float) -> int`: wraps `normalize_bingx_leverage_value`.
|
|||
|
|
- `@typed to_exchange(self, internal_conviction: float) -> ExchangeLeverageDecision`:
|
|||
|
|
wraps `map_internal_conviction_to_exchange_leverage`, returns the full decision
|
|||
|
|
(target + final + caps) for traceability.
|
|||
|
|
- Module docstring: the dual-leverage doctrine (conviction sizes quantity; exchange leverage
|
|||
|
|
derived at venue boundary), cite `FRACTIONAL_LEVERAGE_TO_BINGX_FIX.md` and
|
|||
|
|
`VIOLET_V3_FINDINGS.md §2`.
|
|||
|
|
|
|||
|
|
### 4.2 `prod/clean_arch/violet/test_violet_exchange_leverage.py`
|
|||
|
|
|
|||
|
|
Mirror the test patterns in `test_violet_sizing.py` / `test_violet_modulation.py`
|
|||
|
|
(hypothesis + drift-guards + `@pytest.mark.gate`). Required tests:
|
|||
|
|
|
|||
|
|
**Unit:**
|
|||
|
|
- defaults: `exchange_min=1`, `exchange_max=3`; constants match leverage.py (drift-guard:
|
|||
|
|
import leverage.py and assert `CONVICTION_MIN/MAX/EXCHANGE_LEV_MIN/MAX` equal the wrapper's).
|
|||
|
|
- conviction 0.5 → target 1.0; conviction 9.0 → target 3.0 (endpoints).
|
|||
|
|
- **ROUND_HALF_EVEN boundary cases**: craft convictions whose target lands on x.5 and assert
|
|||
|
|
the final int matches banker's rounding (e.g. target 1.5 → 2, 2.5 → 2). Compute the exact
|
|||
|
|
conviction that yields target=1.5/2.5 from the linear map and verify.
|
|||
|
|
- out-of-range conviction (`<0.5`, `>9.0`, and the sizing extremes up to 9) clamps like BLUE.
|
|||
|
|
- non-default `exchange_max` (e.g. 5, 9) flows through.
|
|||
|
|
- `ExchangeLeverageDecision` frozen (pydantic raises on mutate).
|
|||
|
|
|
|||
|
|
**Property (hypothesis):**
|
|||
|
|
- `@given` conviction ∈ floats[-5, 64] (incl. out-of-range), exchange_max ∈ ints[1,9]:
|
|||
|
|
wrapper output `==` leverage.py output **exactly** (this is unit-level bit-identity);
|
|||
|
|
final exchange_leverage ∈ [exchange_min, exchange_max] and is an int.
|
|||
|
|
|
|||
|
|
**Gate (`@pytest.mark.gate`):**
|
|||
|
|
- `test_gate_exchange_leverage_bit_identity`: **N≥1e6** Monte-Carlo over the joint space
|
|||
|
|
(conviction ∈ uniform[-1, 64] to hammer clamping + the full sizing range; exchange_min ∈
|
|||
|
|
{1}, exchange_max ∈ {1,2,3,5,9}). Assert VIOLET `to_exchange(...).exchange_leverage` and
|
|||
|
|
`.target_exchange_leverage` are **float/int-for-float `==`** to the real leverage.py
|
|||
|
|
functions across every sample. `np.count_nonzero(blue != violet) == 0`. Write a gate
|
|||
|
|
report to `prod/VIOLET_dev/reports/violet_v3_exchange_leverage_<ts>.json` (mirror
|
|||
|
|
`_write_gate_report` in `test_violet_sizing.py`).
|
|||
|
|
|
|||
|
|
## 5. Validation gate (BINDING)
|
|||
|
|
|
|||
|
|
1. **MC bit-identity** (§4.2 gate) at N≥1e6, exact `==`, joint conviction×exchange-cap space
|
|||
|
|
incl. out-of-domain conviction (clamp coverage) and x.5 rounding boundaries.
|
|||
|
|
2. Full non-gate suite green; **shared-files-clean**; the import of `prod.bingx.leverage`
|
|||
|
|
resolves on-host.
|
|||
|
|
|
|||
|
|
## 6. Acceptance criteria
|
|||
|
|
|
|||
|
|
- `exchange_leverage.py` + `test_violet_exchange_leverage.py` created (no other files touched).
|
|||
|
|
- MC bit-identity gate: 0/1e6 mismatches.
|
|||
|
|
- Unit + property tests green; ROUND_HALF_EVEN boundary explicitly tested.
|
|||
|
|
- `git diff --cached --name-only` ∌ any shared file.
|
|||
|
|
- Commit message documents: wrap target, bit-identity result, V-TYPES boundary.
|
|||
|
|
|
|||
|
|
## 7. Watch-outs (learned from the sizing review)
|
|||
|
|
|
|||
|
|
- **No arbitrary magnitude caps** in the V-TYPES aliases (no `le=64`-style ceilings) — only
|
|||
|
|
what leverage.py itself enforces. The function clamps; the type must not double-guard with
|
|||
|
|
a value BLUE/leverage.py would accept.
|
|||
|
|
- **ROUND_HALF_EVEN ≠ round-half-up.** `2.5 → 2`, not 3. Test the even-rounding explicitly.
|
|||
|
|
- Bit-identity here is "trivial" by design (you call the real function) — that's the point:
|
|||
|
|
the gate proves the V-TYPES boundary + arg passing perturb nothing. Do NOT use `approx`.
|
|||
|
|
- `target` (float, pre-round) and `exchange_leverage` (int, post-round) are BOTH part of the
|
|||
|
|
contract — journal/return both.
|
|||
|
|
|
|||
|
|
## 8. Integration (lead will wire; agent need not)
|
|||
|
|
|
|||
|
|
The lead integrates `VioletExchangeLeverage` at the L3/exec boundary in V4 (venue
|
|||
|
|
leverage-set), consuming `VioletSizer`'s conviction output. The agent's deliverable is the
|
|||
|
|
standalone, gated component + tests. Hand back: the two files + the gate report path.
|
|||
|
|
|
|||
|
|
## 9. References
|
|||
|
|
|
|||
|
|
`prod/bingx/leverage.py` (target) · `prod/docs/FRACTIONAL_LEVERAGE_TO_BINGX_FIX.md`
|
|||
|
|
(dual-leverage origin) · `VIOLET_DEV_SPEC_AND_PLAN.md` (V3.5) · `VIOLET_V3_FINDINGS.md §2`
|
|||
|
|
(dual-leverage) · pattern refs: `prod/clean_arch/violet/modulation.py`,
|
|||
|
|
`test_violet_sizing.py` (gate + `_write_gate_report` style), `domain.py` (V-TYPES).
|