diff --git a/docs/fiqh-basis.md b/docs/fiqh-basis.md index 0234239..dab6d75 100644 --- a/docs/fiqh-basis.md +++ b/docs/fiqh-basis.md @@ -123,6 +123,49 @@ The fiqh content — that derivatives and difference-settlement are impermissibl (§65.6: what makes speculation *maisir* is non-ownership, non-delivery, difference-settlement) but the RAILS are the charter enforcing it. +### Long-only — *bay' ma la yamlik*, §65.4/§65.6/§65.11 + +keel never sells what it does not hold, and four independent places enforce it: + +- **The rule contract.** `Setup` declares `direction: Literal["long"]`, and `__post_init__` + enforces at runtime the one thing that annotation only asserts statically — so a foreign + rule cannot propose a short, and cannot propose one wearing a long's label (#447). +- **The engine.** `keel/strategy/engine.py` builds every entry `Signal` with `side=Side.BUY`, + unconditionally. No branch produces a `Side.SELL` entry. +- **Rail 10.** Every SELL must cite a defined rule — no arbitrary liquidation. +- **Rails 18/19.** The spot-only charter: nothing that settles by price difference. + +The doctrine, from the same source the rest of this document rests on. §65.11 (Ayub Ch 5.4.2): +*"short-selling has been prohibited by almost all scholars"* — because the subject matter +*"must be existing/existable… capable of ownership/title, capable of delivery/possession… and +the seller must have its title and risk."* §65.6 draws the same boundary from the other side: +what converts speculation into *maisir* is **(a) not owning it, (b) not possessing it, (c) not +taking or making delivery, or (d) settling by price difference** — never frequency, never +volatility. + +**Unlike rails 18/19, this one is a fiqh-derivation and not a charter.** It descends from the +prohibition Ayub states at Ch 6.5.1 (§65.4) as *"do not sell what you do not possess"* — +*bay' ma la yamlik*. That anchor is more direct than riba for this class of failure, and the +difference matters: a sale of what the seller does not hold is impermissible with no interest +anywhere in the transaction, so a riba-framed defence does not reach it. + +⚠️ One qualification, because §65.4 is invoked here for nearly the opposite of what it was read +for. §65.4's own point is that the prohibition is **not** a physical-custody rule — *"what is +meant by possession here is the inability to deliver the goods"* — which is exactly how +exchange-held balances qualify as valid `qabd`. That lenient reading does not weaken the +long-only claim; it sharpens it. On the constructive-possession test the question is whether +anything prevents delivery, and in the failure modes recorded under "Known open questions" the +asset is not there at all. Selling it fails the prohibition on the lenient reading, not merely +the strict one. + +**On the primary text.** The doctrine is conventionally anchored to the hadith of Hakim ibn +Hizam — *"Do not sell what is not with you"* (لا تبع ما ليس عندك), Sunan an-Nasa'i 4613 / +Sunan Abu Dawud 3503. **That reference is not extracted in this repository's knowledge base**, +and is recorded here as the operator's citation: checkable against the collections, but +carrying no `§N.x` row and not treated as one. The in-repo authority for everything above is +§65.4/§65.6/§65.11, corroborated by §28.2's ownership principle and §33's gharar reading of +*"the sale of items not owned"*. + ### Purification (§65.9) and idle-balance rewards (§56.3) `keel/compliance/purification.py` implements Ayub §65.9: interest/reward credits are @@ -224,6 +267,18 @@ Stated, not hidden — each is a place where keel's encoded behaviour could be w Whether "no underlying purpose" is disqualifying "is exactly the kind of judgement the screen defers to a human" (`docs/experiments/2026-07-20-candidate-universe.md`) — deferred, not decided. +- **Long-only is enforced where keel DECIDES, not where it SETTLES.** Everything under + "Long-only" above governs what keel chooses. It does not govern the quantity that reaches + the venue. A SELL is never clamped to the account's available base (#667): the exit sells + what the ledger believes is held, and the ledger drifts from the account through fees taken + in the base asset, partial fills (`filled_quantity` is recorded nowhere on this + deployment), and out-of-band operator transfers. A resting stop leg can outlive the + position that justified it, because no sweep cancels a SELL whose position is gone (#668). + On a cash account each of these is a rejected order — bad, but not a fiqh failure. On a + margin-enabled account the venue fills the difference as a short, and keel has no + cash-account posture check on Coinbase (#666; the check exists only on the Alpaca adapter). + So the long-only ruling above is sound at the decision layer and **not yet closed at the + venue boundary**. Named here rather than left to be discovered. - **ZEC and the rest of the deferrals.** The candidate-universe record lists the open questions the attestation step has to answer and "which this agent must not answer". diff --git a/tests/test_fiqh_basis.py b/tests/test_fiqh_basis.py index 0278c53..d0daaa0 100644 --- a/tests/test_fiqh_basis.py +++ b/tests/test_fiqh_basis.py @@ -376,3 +376,109 @@ def test_the_readme_links_the_document(): assert _DOC in (_ROOT / "README.md").read_text(), ( f"README.md must link {_DOC} from its documentation map" ) + + +_LONG_ONLY_PROHIBITION = "short-selling has been prohibited by" + +_LONG_ONLY_SUBJECT_MATTER = "capable of ownership/title, capable of delivery/possession" + +_LONG_ONLY_TYPE = 'direction: Literal["long"]' + +_LONG_ONLY_SIDE = "side=Side.BUY" + +_HADITH_NOT_IN_KB = "That reference is not extracted in this repository's knowledge base" + +_SELL_SIZE_CONTRACT = "it can only ever make an exit MORE likely to be accepted" + + +def test_the_long_only_ruling_is_pinned_two_sided_to_the_code_that_enforces_it(): + """The doc names two enforcement points by their exact source text; both must still exist. + + Long-only is the one ruling in this document that is neither an attestation nor a rail: + it is structural, and it holds only because the rule contract cannot express a short and + the engine cannot construct one. Either half silently changing would leave the doc making + a fiqh claim about behaviour the code no longer has. + """ + doc = _unwrapped(_doc()) + assert _LONG_ONLY_TYPE in doc and _LONG_ONLY_SIDE in doc, ( + f"{_DOC} must name both long-only enforcement points exactly " + f"({_LONG_ONLY_TYPE!r} and {_LONG_ONLY_SIDE!r}) -- a claim that cannot be checked " + "against a line of code is the kind this document exists to refuse" + ) + base = (_ROOT / "keel/strategy/rules/base.py").read_text() + assert re.search(r'(?m)^\s+direction: Literal\["long"\]\s*$', base), ( + "keel/strategy/rules/base.py must still DECLARE the field as `Literal[\"long\"]`. " + "Matched on the field line, not anywhere in the file: the `__post_init__` docstring " + f"quotes the annotation too, and a prose mention is not a contract {_DOC} can cite" + ) + assert 'if self.direction != "long":' in _rel("keel/strategy/rules/base.py"), ( + "keel/strategy/rules/base.py must still enforce the direction at RUNTIME -- the " + "annotation is a promise to mypy, and a foreign rule never runs mypy (#447)" + ) + assert _LONG_ONLY_SIDE in _rel("keel/strategy/engine.py"), ( + "keel/strategy/engine.py must still build every entry Signal as a BUY; if an entry " + f"can be a SELL, {_DOC}'s long-only section is false" + ) + + +def test_the_long_only_doctrine_is_quoted_from_the_knowledge_base(): + """`bay' ma la yamlik` is cited, not asserted -- and the source must still say it. + + The document's whole method is that a ruling is checkable against an in-repo extract. + Long-only rests on §65.11's short-selling passage, so the doc's quotation and source-65's + text are pinned to each other in both directions. + """ + doc = _unwrapped(_doc()) + source = "docs/superpowers/references/trading-knowledge-base/sources/source-65.md" + text = _rel(source) + for quoted in (_LONG_ONLY_PROHIBITION, _LONG_ONLY_SUBJECT_MATTER): + assert quoted in doc, ( + f"{_DOC} must quote §65.11 verbatim ({quoted!r}) -- long-only is a fiqh " + "derivation here, not a charter, so it must carry its citation" + ) + assert quoted in text, ( + f"{source} must still carry the passage the doc quotes ({quoted!r})" + ) + + +def test_the_hadith_reference_is_marked_as_outside_the_knowledge_base(): + """A primary-text citation with no `§N.x` row must SAY it has none. + + Every other citation in this document resolves to an in-repo extract; the hadith of Hakim + ibn Hizam does not. Recording it without the disclaimer would let a reader assume the same + provenance the `§N.x` rows carry -- exactly the papering-over the KB's honesty rules + forbid. Two-sided: the disclaimer must be there, and it must still be TRUE. + """ + assert _HADITH_NOT_IN_KB in _unwrapped(_doc()), ( + f"{_DOC} must mark the hadith reference as outside the knowledge base " + f"({_HADITH_NOT_IN_KB!r}) -- an uncited primary text may be recorded, never disguised" + ) + sources = _ROOT / "docs/superpowers/references/trading-knowledge-base/sources" + carrying = [p.name for p in sorted(sources.glob("*.md")) if "an-Nasa'i" in p.read_text()] + assert not carrying, ( + f"{carrying} now extracts the hadith the doc says is absent -- the disclaimer has " + "become false; give the citation its §N.x row and drop the caveat" + ) + + +def test_the_venue_boundary_gap_is_stated_not_hidden(): + """Long-only holds where keel decides; the doc must admit it is open where keel settles. + + A SELL is never clamped to the account's available base, so fee dust, an unrecorded + partial fill, or an out-of-band transfer can send an order for more than is held (#667). + That is `bay' ma la yamlik` reached by arithmetic. Pinned to `_sell_base_size`'s own + contract: while that function's only job is to make an exit more likely to be accepted, it + reads no balance, and the open question must stay in the doc. When #667 lands this test + fails -- which is the point: the doc gets updated with the code, not after it. + """ + doc = _unwrapped(_doc()) + assert "not yet closed at the venue boundary" in doc, ( + f"{_DOC} must state that long-only is unclosed at the venue boundary -- the ruling is " + "sound at the decision layer and the operational gap is not the reader's to discover" + ) + for issue in ("#666", "#667", "#668"): + assert issue in doc, f"{_DOC} must name {issue} as the open venue-boundary work" + assert _SELL_SIZE_CONTRACT in _rel("keel/execution/executor.py"), ( + "keel/execution/executor.py's `_sell_base_size` must still carry its stated contract; " + f"if the exit now clamps to a real balance, {_DOC}'s open question is stale" + )