From d6a5bbbec1ebdfe6402188204582a8cabbad9ef2 Mon Sep 17 00:00:00 2001 From: DoRmAmMu1997 Date: Wed, 16 Sep 2026 15:11:14 +0530 Subject: [PATCH 1/4] feat(sl-hunting): v5f - a one-way day seats nobody, and the next open is the receipt 16 Sep. Same pre-open note, same morning, opposite branch - and IH won. The note shipped for today was conditional: gap up -> BUY the trapped sellers, flat or gap down -> SELL with the market. NIFTY opened +82 points (+0.36%), BankNIFTY +0.38%. IH: "today we saw a FLAT opening, and with this kind of opening we are going to follow the CONTINUED SELLING... if a VERY BIG gap up had opened we could not have worked like this." He bought puts across all three indices and booked around his 3-lakh target on a sharp simultaneous breakdown. The agent: "NIFTY gapped +82pts (0.36%) ... after a 457pt breakdown day that seated positional sellers, satisfying the pre-open note's GAP-UP->BUY branch." Four entries, all LONG, inside 53 minutes, basket -Rs.863.75. Three lost; the only winner was closed by an erroneous no-reason exit. The proximate cause is a v4t VIOLATION rather than a knowledge gap. v4t already calls the classification a judgement you must make and state, already calibrates it ("a quarter of a percent is not a gap... something in the region of half a percent is where the gap reading starts to earn itself"), and already says hesitation resolves to flat. 0.36% is below that line. The number was computed and written into all four entry rationales and never once tested against the rule. This is the THIRD session decided by that one word: 31 Aug produced v4t, 01 Sep produced its v4u 3:15 sub-rule at a cost of Rs.1,488.25, this one cost Rs.863.75, and nothing downstream was wrong on any of the three. What IS net-new is the premise UNDERNEATH the branch, and it is why the agent wanted the gap-up branch at all. All four rationales asserted the 457-point breakdown had "seated positional sellers". IH argues the opposite with a mechanism: "when do sellers REMAIN seated? When a retracement happens. When there is no retracement, every trader keeps fearing that a retracement might come" - so on continuous momentum "traders do come, but they also book their target and leave", and "the chances of HOLDING are low here". A violent one-way session ends with almost nobody carrying inventory, however large its range. He then gives the falsification test, which is the part that runs BEFORE the trade: "but if sellers had been seated in good quantity, what would the market have opened as? We would have seen a straight gap up." The open is the receipt for the premise. A small gap is not a weak version of that open; it is the evidence against it. Placed directly after the WHOLE v4t block - including its v4u sub-bullet, which an earlier attempt split and orphaned - so the model reads "decide which word describes the open", then "here is what the open's size is measuring". It is v5c one scale up: there a retracement recruits a crowd inside the move, here a retracement is what lets a crowd survive into the next session. Negative-tested 14 ways, all 14 caught, plus a control mutation of unasserted prose that correctly did not trip the test. One anchor initially matched zero times because it began mid-line rather than at a line start; re-run line-contiguously, it caught. Recorded as open and NOT fixed here, both runtime changes kept out of a knowledge-only PR: 1. The open classification should stop being prose. Three sessions, two existing rules, same failure - which is exactly the corpus's own PROSE RULES DON'T BIND / JUDGEMENT RULES GET TALKED PAST law. The gap percentage against the 3:15 reference is computable before the first decision. 2. An EXIT carrying no reason executed and closed the day's only winner. The tool logged "probable UNINTENDED order" and executed it anyway, closing a +Rs.1,182 basket; the model's next decision called it erroneous itself, and that trade's 23276 target printed at 09:18, about 100 seconds later. The warning exists; the refusal does not. Gates: 606 master, 28 market-data-health, 1497 pytest (+1), ruff 0.16.7, mypy (80 files + master), compileall, bandit, coverage 73.7% policy pass. Co-Authored-By: Claude Opus 5 --- .../SL Hunting AI Agent/sl_hunting_doc.md | 86 +++++++++++++++++++ .../sl_hunting_knowledge.py | 41 +++++++++ .../test_sl_hunting_schema.py | 65 ++++++++++++++ 3 files changed, 192 insertions(+) diff --git a/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md b/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md index 64e618d..21d5cf3 100644 --- a/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md +++ b/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md @@ -7188,3 +7188,89 @@ it. A ninth mutation was written and discarded as invalid -- setting the exception branch to `float("inf")` is filtered by the `isfinite` guard and changes no behaviour, so it proved nothing; it was replaced by mutations that make the exception branch refuse and re-raise, both caught. +## v5f - a one-way day seats nobody, and the next open is the receipt (16 Sep) + +Source: `TXiU5Odet04`, "Live Bank Nifty Option Trading", uploaded 2026-09-16 +10:09 IST. Compared against the agent's own session the same morning: four +entries, all LONG, inside 53 minutes, basket **-Rs.863.75**. + +**Same note, same morning, opposite branch.** The pre-open note shipped for +16 Sep (PR #171) was conditional: gap up -> BUY the trapped sellers, flat or +gap down -> SELL with the market. NIFTY opened +82 points, +0.36%; BankNIFTY ++0.38%. + +- IH: "today we saw a FLAT opening, and with this kind of opening we are going + to follow the CONTINUED SELLING... if a VERY BIG gap up had opened we could + not have worked like this." He bought puts across all three indices, booked + around his 3-lakh target on a sharp simultaneous breakdown, and closed the + session a clear winner. +- The agent: "NIFTY gapped +82pts (0.36%) and BankNIFTY +213pts (0.38%) above + prior close after a 457pt breakdown day that seated positional sellers, + satisfying the pre-open note's GAP-UP->BUY branch." All four entries LONG. + +**The proximate cause is a v4t violation, not a knowledge gap.** v4t already +says the classification is a JUDGEMENT you make and state, already calibrates +it ("a quarter of a percent is not a gap... something in the region of half a +percent is where the gap reading starts to earn itself"), and already says +that hesitation resolves to flat. 0.36% sits below that line. The number was +computed and written into every one of the four entry rationales and never +once tested against the rule. This is the THIRD session in the series decided +by that one word: 31 Aug produced v4t, 01 Sep produced its v4u 3:15 sub-rule +at a cost of Rs.1,488.25, and this one cost Rs.863.75. + +**What IS net-new is the premise UNDERNEATH the branch**, and it is the reason +the agent wanted the gap-up branch at all. All four rationales asserted that +the prior 457-point breakdown had "seated positional sellers". IH argues the +exact opposite, with a mechanism: "when do sellers REMAIN seated? When a +retracement happens. When there is no retracement, every trader keeps fearing +that a retracement might come, that his trade might go wrong" -- so on +continuous momentum "traders do come, but they also book their target and +leave", and "the chances of HOLDING are low here". A violent one-way session +ends with almost nobody carrying inventory, however large its range. + +He then supplies the falsification test, which is the part that can run before +the trade: "but if sellers had been seated in good quantity, what would the +market have opened as? We would have seen a straight gap up." The open is the +RECEIPT for the premise. A small gap is not a weak version of that open; it is +the evidence against it. + +Encoded as **A ONE-WAY DAY LEAVES NOBODY SEATED, AND THE NEXT OPEN IS THE +RECEIPT (v5f)**, placed directly after the whole v4t block (including its v4u +sub-bullet, which it must not split) so the model reads "decide which word +describes the open" and then "here is what the open's size is measuring". It +is v5c one scale up: there a retracement recruits a crowd inside the move, +here a retracement is what lets a crowd survive into the NEXT session. + +Negative-tested 14 ways, all 14 caught, plus a control mutation of unasserted +prose that correctly did not trip the test. One anchor initially matched zero +times because it began mid-line ("That is v5c...") rather than at a line +start; it was re-run line-contiguously and caught. That is the same +wrap-spanning mistake the mutation harnesses keep inviting -- print the raw +line boundaries first. + +**Considered and not encoded.** IH's "a retracement is what seats a crowd" is +partly v5c already; only the cross-session half is new and that is what v5f +carries. His loss-discipline material repeats v3y. His round-number caution +("there is a round number here, a small problem will show, control your heart, +do not run") is already covered by the round-number rules in BNF_SPECIFIC. + +**Open, NOT fixed here, and the strongest code-gate candidate yet.** Two +runtime items from this session: + +1. **The open classification should stop being prose.** Three sessions, two + existing rules, same failure. The corpus's own law -- PROSE RULES DON'T + BIND, and JUDGEMENT RULES GET TALKED PAST -- says the checkable arm belongs + at the tool boundary. The gap percentage against the 3:15 reference is + computable before the first decision, and the model could be handed it as a + fact with the classification already made, rather than being trusted to + derive and apply it. +2. **An EXIT carrying no reason executed and closed the day's only winner.** + At 09:16:38 the order tool logged "EXIT (leg=BOTH) executed with NO reason + from the model. A deliberate exit always carries one, so treat this as a + probable UNINTENDED order" -- and then executed it anyway, closing a + +Rs.1,182 basket. The model's own next decision called it "an erroneous + EXIT call closed both legs of a working long position that had no valid + exit trigger (premise intact, price 23234.80 well clear of stop 23183.0, + approaching target 23276.0)". The trade's 23276 target printed at 09:18, + about 100 seconds after it was closed. The warning exists; the refusal does + not. diff --git a/Signal Generators/SL Hunting AI Agent/sl_hunting_knowledge.py b/Signal Generators/SL Hunting AI Agent/sl_hunting_knowledge.py index 8376b78..661cccd 100644 --- a/Signal Generators/SL Hunting AI Agent/sl_hunting_knowledge.py +++ b/Signal Generators/SL Hunting AI Agent/sl_hunting_knowledge.py @@ -595,6 +595,47 @@ SHARED-GAP REQUIREMENT applied in the direction it does not currently name: that rule covers a flat BankNIFTY beside a gapped NIFTY, and this is the mirror case. A gap in ONE index is not a gapped market. +- A ONE-WAY DAY LEAVES NOBODY SEATED, AND THE NEXT OPEN IS THE RECEIPT (v5f). + The rule above decides which WORD describes the open. This one says what the + open's SIZE is actually measuring, which is the question the branch was asking + all along: is the crowd you plan to hunt even there? + A BIG MOVE IS NOT EVIDENCE OF A SEATED CROWD, and this is the half that gets + assumed rather than checked. IH gives the mechanism plainly: "when do sellers + REMAIN seated? When a retracement happens. When there is no retracement, every + trader keeps fearing that a retracement might come, that his trade might go + wrong." So on continuous momentum "traders do come, but they also book their + target and leave", and "the chances of HOLDING are low here". A violent one-way + session therefore ends with almost NOBODY carrying inventory, however large its + range. That is v5c one scale up: there a retracement recruits a crowd inside + the move, here a retracement is what lets a crowd survive into the NEXT session. + THE OPEN IS THE MEASUREMENT, AND IT CAN FALSIFY THE PREMISE BEFORE YOU ACT ON + IT. "But if sellers had been seated in good quantity, what would the market + have opened as? We would have seen a straight gap up." Run that as a test, in + that order: state what the open WOULD look like if your trapped crowd existed, + then look at what it actually did. A small gap is not a weak version of that + open -- it is the evidence AGAINST it, because the crowd that would have forced + a big gap is not there to force one. + MEASURED ON THIS BOOK (2026-09-16), and it inverted the entire day. The prior + session ran 457 points in one continuous direction and closed at the bottom of + its range. Four entries inside 53 minutes all asserted that this had "seated + positional sellers", and all four took the pre-open note's gap-up-to-BUY branch + on an open of +0.36% on NIFTY and +0.38% on BankNIFTY. IH read the SAME open as + FLAT, followed the selling, and booked a large profit. The book lost on three of + the four, and its only winner was closed by an erroneous exit rather than by a + decision. The 0.36% was computed and written into the entry rationale every + time, and never once tested against the calibration above, which puts the gap + reading near half a percent and says that hesitation resolves to flat. + THIS IS THE THIRD SESSION DECIDED BY THAT ONE WORD, so treat the classification + as the highest-leverage judgement of the morning rather than a label: 31 Aug + read "gap-down" where IH read flat and produced the rule above; 01 Sep measured + against the official close instead of the 3:15 level, flipped the branch and + cost 1,488.25; this one cost 863.75. Nothing downstream was wrong on any of the + three. + PRACTICAL FORM, two questions in order, before any gap branch is cited after a + big directional session: did yesterday RETRACE? If it ran one way, assume no + seated crowd and stop there. If it did retrace, is today's gap the size that + crowd would have produced? If it is not, the crowd is absent, and the plan is + to FOLLOW the move rather than to hunt it. - THE EARLY RETRACEMENT IS THE TRAP, NOT THE TURN (v4t). After a session opens and moves directly one way, the small pull-back that follows is usually there to keep the crowd OUT of the move, not to end it. IH, on exactly that: "when it opens flat diff --git a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_schema.py b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_schema.py index 02d6dde..ea437a0 100644 --- a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_schema.py +++ b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_schema.py @@ -2789,3 +2789,68 @@ def test_v5e_the_stop_must_clear_the_level_the_premise_named(): assert "THE STOP IS A DISTANCE FROM THE FILL" in rule assert "the answer is NO TRADE, never the nearer stop that happens to fit" in rule assert "in both the thing that gives is the SIZE, never the stop" in rule + + +def test_v5f_a_one_way_day_seats_nobody_and_the_open_is_the_receipt(): + """v5f (16 Sep): the premise UNDER the branch, not the branch itself. + + Four entries assumed a 457-point one-way session had seated sellers, then + read a 0.36% open as the gap-up that would trap them. IH read the same open + as flat, followed the selling, and won. Seven ways an edit breaks it: + + 1. Losing the tie to v4t -- that rule picks the WORD, this one says what the + SIZE measures. Apart, they read as two opinions about gaps. + 2. Losing the mechanism: a crowd survives only through a RETRACEMENT, and + continuous momentum makes everyone book and leave. Without it, "a big + move seats a crowd" is the intuitive default and wins again. + 3. Losing the link to v5c, which supplies the same mechanism one scale down. + 4. Losing the falsification test, which is the only part that is actionable + BEFORE the trade: say what the open would look like if the crowd existed. + 5. Softening "a small gap is evidence AGAINST" into "weaker evidence for". + That inversion is the whole rule; a summariser will flatten it. + 6. Losing the three-strike history. One loss reads as bad luck; three + sessions decided by one word is what makes it the morning's key call. + 7. Losing the ordered practical form, where the retracement question comes + FIRST and can stop the enquiry on its own. + """ + prompt = build_system_prompt() + rule = _flat_rule(prompt, "A ONE-WAY DAY LEAVES NOBODY SEATED") + + # 1. Explicitly the size-half of the classification v4t makes. + assert "AND THE NEXT OPEN IS THE RECEIPT" in rule + assert "The rule above decides which WORD describes the open" in rule + assert "what the open's SIZE is actually measuring" in rule + + # 2. The mechanism, in IH's words, both directions. + assert "when do sellers REMAIN seated? When a retracement happens" in rule + assert "every trader keeps fearing that a retracement might come" in rule + assert "traders do come, but they also book their target and leave" in rule + assert "the chances of HOLDING are low here" in rule + assert "ends with almost NOBODY carrying inventory, however large its range" in rule + + # 3. The scale relationship to v5c. + assert "That is v5c one scale up" in rule + assert "a retracement is what lets a crowd survive into the NEXT session" in rule + + # 4. The test that can run before the entry, in its stated order. + assert "if sellers had been seated in good quantity, what would the market have opened as" in rule + assert "state what the open WOULD look like if your trapped crowd existed" in rule + + # 5. Evidence AGAINST, not weaker evidence for. + assert "A small gap is not a weak version of that open" in rule + assert "it is the evidence AGAINST it" in rule + assert "the crowd that would have forced a big gap is not there to force one" in rule + + # 6. The measured case and the repeat history that gives it weight. + assert "ran 457 points in one continuous direction" in rule + assert "+0.36% on NIFTY and +0.38% on BankNIFTY" in rule + assert "IH read the SAME open as FLAT" in rule + assert "its only winner was closed by an erroneous exit" in rule + assert "never once tested against the calibration above" in rule + assert "THIS IS THE THIRD SESSION DECIDED BY THAT ONE WORD" in rule + assert "Nothing downstream was wrong on any of the three" in rule + + # 7. The ordered form, including the early stop. + assert "did yesterday RETRACE?" in rule + assert "If it ran one way, assume no seated crowd and stop there" in rule + assert "the plan is to FOLLOW the move rather than to hunt it" in rule From 99687e243abcca80ebf6a0d9d939862188222b93 Mon Sep 17 00:00:00 2001 From: DoRmAmMu1997 Date: Wed, 16 Sep 2026 15:35:45 +0530 Subject: [PATCH 2/4] fix(sl-hunting): SLH-017 - classify the open in code instead of deriving it Follow-up to v5f on this PR, and the arm of that session that had to become code. Three sessions in this book were decided by one word, with two knowledge rules already in place specifically to get it right: 31 Aug produced v4t, 01 Sep produced its v4u sub-rule at a cost of Rs.1,488.25, and 16 Sep cost Rs.863.75. On 16 Sep the model computed the deciding number correctly - "+82pts (0.36%)" - wrote it into all four entry rationales, and never compared it against v4t's own calibration. That is exactly the shape the corpus already has a law for: PROSE RULES DON'T BIND, and JUDGEMENT RULES GET TALKED PAST. pivot_and_levels now returns an open_classification block carrying previous_close, today_open, gap_points, gap_pct, threshold_pct, a verdict of FLAT / GAP_UP / GAP_DOWN, and the reference it measured against. Neither number is invented. The 0.5% threshold is v4t's own calibration ("a quarter of a percent is not a gap... something in the region of half a percent is where the gap reading starts to earn itself"), and sub-threshold opens read FLAT in BOTH directions because v4t also says hesitation resolves to flat. The reference is v4u's - the previous session's last candle rather than the official 15:30 close, because the closing auction carries settlement prints no crowd traded around. It reaches the model through the levels tool and, for BankNIFTY, through bank_nifty. Run against 16 Sep's real numbers both come back FLAT (NIFTY 0.355%, BankNIFTY 0.381%) - IH's reading of that morning, and the opposite of the one the agent acted on. WIRED, not merely added: v4t now carries an SLH-017 paragraph telling the model to READ the verdict and state it, and not to recompute it or argue past it, because an unwired fact is one the model re-derives its own way. The existing v4t marker test was EXTENDED rather than rewritten, so its original assertions still pin the original prose. Scope kept narrow: this supplies the arithmetic and the threshold. It rejects no orders and does not judge what the classification MEANS - that stays with the model, which is where the actual reasoning is. TDD: six tests RED first, then the implementation. Negative-tested 9 ways, all 9 caught, plus a control mutation of unasserted prose that correctly did not trip the tests. One mutation - measuring from today's LAST price instead of its OPEN - initially PASSED, because every fixture opened and closed at the same price. That was a real defect the tests could not see: a flat open would silently become a "gap up" by mid-morning and re-select the branch hours after the fact. A dedicated drift test was added and the mutation then caught. NOT BUILT, and recorded as such: refusing an EXIT that carries no reason. That was the session's other item, and it reverses a standing operator decision. SLH-007 says an EXIT is never refused over its wording, because bouncing it would strand an open position; SLH-010 explicitly declined to reverse it and added the warning instead; and test_exit_with_no_reason_still_executes_but_warns_loudly exists so that "a later tightening cannot quietly turn the warning into a refusal". Building it would mean deleting a guard written to prevent it. The doc records the narrow one-round-bounce form worth considering if it is ever revisited, and that it needs an explicit operator decision. Gates: 606 master, 28 market-data-health, 1504 pytest (+7), ruff 0.16.7, mypy (80 files + master), compileall, bandit, coverage 73.8% policy pass. Co-Authored-By: Claude Opus 5 --- .../SL Hunting AI Agent/sl_hunting_doc.md | 95 +++++++++++---- .../sl_hunting_indicators.py | 54 +++++++++ .../sl_hunting_knowledge.py | 8 ++ .../test_sl_hunting_indicators.py | 112 ++++++++++++++++++ .../test_sl_hunting_schema.py | 9 ++ 5 files changed, 258 insertions(+), 20 deletions(-) diff --git a/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md b/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md index 21d5cf3..cc2f9a5 100644 --- a/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md +++ b/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md @@ -7254,23 +7254,78 @@ carries. His loss-discipline material repeats v3y. His round-number caution ("there is a round number here, a small problem will show, control your heart, do not run") is already covered by the round-number rules in BNF_SPECIFIC. -**Open, NOT fixed here, and the strongest code-gate candidate yet.** Two -runtime items from this session: - -1. **The open classification should stop being prose.** Three sessions, two - existing rules, same failure. The corpus's own law -- PROSE RULES DON'T - BIND, and JUDGEMENT RULES GET TALKED PAST -- says the checkable arm belongs - at the tool boundary. The gap percentage against the 3:15 reference is - computable before the first decision, and the model could be handed it as a - fact with the classification already made, rather than being trusted to - derive and apply it. -2. **An EXIT carrying no reason executed and closed the day's only winner.** - At 09:16:38 the order tool logged "EXIT (leg=BOTH) executed with NO reason - from the model. A deliberate exit always carries one, so treat this as a - probable UNINTENDED order" -- and then executed it anyway, closing a - +Rs.1,182 basket. The model's own next decision called it "an erroneous - EXIT call closed both legs of a working long position that had no valid - exit trigger (premise intact, price 23234.80 well clear of stop 23183.0, - approaching target 23276.0)". The trade's 23276 target printed at 09:18, - about 100 seconds after it was closed. The warning exists; the refusal does - not. +Two runtime items were raised from this session. The first became SLH-017 +below. The second is recorded there too, and is NOT being built. + +## SLH-017 - the open is classified in code, not derived by the model + +Three sessions in this book were decided by one word, with two knowledge rules +already in place specifically to get it right: 31 Aug produced v4t, 01 Sep +produced its v4u sub-rule at a cost of Rs.1,488.25, and 16 Sep cost Rs.863.75. +On 16 Sep the model computed the deciding number correctly -- "+82pts (0.36%)" +-- wrote it into all four entry rationales, and never compared it with v4t's +own calibration. That is the exact shape the corpus already has a law for: +PROSE RULES DON'T BIND, and JUDGEMENT RULES GET TALKED PAST. + +`pivot_and_levels` now returns an `open_classification` block: + + previous_close / today_open / gap_points / gap_pct + threshold_pct (GAP_CLASSIFICATION_THRESHOLD_PCT = 0.5) + verdict FLAT | GAP_UP | GAP_DOWN + reference "previous session's last candle close, not the official + 15:30 close" + +The threshold is not invented: it is v4t's own calibration ("a quarter of a +percent is not a gap... something in the region of half a percent is where the +gap reading starts to earn itself"), and sub-threshold opens read FLAT in both +directions because v4t also says hesitation resolves to flat. The reference is +v4u's, for the reason v4u gives -- the closing auction carries settlement +prints no crowd traded around. + +It flows to the model through the `levels` tool and, for BankNIFTY, through +`bank_nifty`. Run against 16 Sep's real numbers both come back **FLAT** +(NIFTY 0.355%, BankNIFTY 0.381%) -- which is IH's reading of that morning and +the opposite of the one the agent acted on. + +**Wired, not just added.** v4t now carries an SLH-017 paragraph telling the +model to READ the verdict and state it, and not to recompute or argue past it, +because an unwired fact is one the model will simply re-derive its own way. +The existing v4t marker test was extended rather than rewritten, so its old +assertions still pin the original prose. + +**Scope.** This supplies the arithmetic and the threshold; it does not reject +orders and does not judge what the classification MEANS. That part stays with +the model, which is where the real reasoning is. + +Negative-tested 9 ways, all 9 caught, plus a control mutation of unasserted +prose that correctly did not trip the tests. One mutation -- measuring from +today's LAST price instead of its OPEN -- initially PASSED, because every +fixture opened and closed at the same price. That is a real defect the tests +could not see: a flat open would silently become a "gap up" by mid-morning and +re-select the branch hours after the fact. A dedicated drift test was added +and the mutation then caught. + +### NOT built: refusing an EXIT that carries no reason + +The session's other item was that at 09:16:38 an EXIT with no reason executed +and closed the day's only winner (+Rs.1,182 basket), about 100 seconds before +that trade's 23276 target printed. The order tool logged it as "a probable +UNINTENDED order" and honoured it anyway; the model's next decision called it +erroneous itself. + +Turning that warning into a refusal was considered and **rejected, because it +reverses a standing operator decision**. SLH-007 states that an EXIT is never +refused over its wording -- bouncing it would strand an open position, which +the risk rules forbid -- and SLH-010 explicitly declined to reverse it, adding +the warning instead. `test_exit_with_no_reason_still_executes_but_warns_loudly` +exists precisely so that "a later tightening cannot quietly turn the warning +into a refusal", and it asserts the exit is accepted. + +So the change would mean deleting a guard written to prevent it. If it is ever +revisited, the narrow form worth considering is a ONE-ROUND bounce: reject the +first reasonless EXIT with a message asking for the justification, and honour +the next EXIT unconditionally whatever it carries. That cannot strand a +position beyond a single decision cycle, and the mechanical stop, target, +max-loss and 15:15 square-off are host-owned throughout and never gated. It +still needs an explicit operator decision, because it is SLH-007 that is being +narrowed. diff --git a/Signal Generators/SL Hunting AI Agent/sl_hunting_indicators.py b/Signal Generators/SL Hunting AI Agent/sl_hunting_indicators.py index c6067ce..fb0923a 100644 --- a/Signal Generators/SL Hunting AI Agent/sl_hunting_indicators.py +++ b/Signal Generators/SL Hunting AI Agent/sl_hunting_indicators.py @@ -39,6 +39,14 @@ _RETRACEMENTS = (0.5, 0.618, 0.786) _EXTENSIONS = (1.272, 1.618, 2.618) +# SLH-017: where "flat" stops and "gap" starts, as a PERCENTAGE of the prior +# session's close. The number is not arbitrary -- it is the calibration v4t +# already carries in the knowledge: a quarter of a percent is not a gap, and +# "something in the region of half a percent" is where the gap reading starts +# to earn itself. Anything below reads FLAT, because v4t also says that +# hesitation between the two words resolves to flat. +GAP_CLASSIFICATION_THRESHOLD_PCT = 0.5 + @dataclass(frozen=True) class SLHuntingIndicatorConfig: @@ -277,12 +285,58 @@ def _dist(level: float | None) -> dict[str, Any] | None: pts = round(last_price - level, 2) return {"level": round(level, 2), "points_away": pts, "pct_away": round(100.0 * pts / max(level, 1e-9), 3)} + # SLH-017: classify the open HERE rather than leaving the model to derive + # it. This one word selects the branch of a conditional pre-open plan, and + # three sessions in this book were decided by getting it wrong (31 Aug, + # 01 Sep, 16 Sep) -- on 16 Sep the model computed "+82pts (0.36%)" + # correctly, called it a gap-up anyway, and took four losing longs. The + # arithmetic and the threshold are mechanical, so they belong in code; what + # the classification MEANS is still the model's to reason about. + if prev_day is None: + open_classification: dict[str, Any] = { + "available": False, + "verdict": None, + "reason": "no previous session in the candle window to measure against", + } + else: + prev_close = float(prev_day["close"]) + today_open = float(cast(float, today_levels["open"])) + gap_points = round(today_open - prev_close, 2) + gap_pct = round(100.0 * gap_points / max(abs(prev_close), 1e-9), 3) + if abs(gap_pct) >= GAP_CLASSIFICATION_THRESHOLD_PCT: + verdict = "GAP_UP" if gap_points > 0 else "GAP_DOWN" + else: + verdict = "FLAT" + open_classification = { + "available": True, + "previous_close": round(prev_close, 2), + "today_open": round(today_open, 2), + "gap_points": gap_points, + "gap_pct": gap_pct, + "threshold_pct": GAP_CLASSIFICATION_THRESHOLD_PCT, + "verdict": verdict, + # v4u: the prior session's LAST CANDLE, deliberately not the + # official 15:30 close -- the closing auction carries settlement + # prints no crowd traded around, and positioning is the only thing + # this classification is trying to read. + "reference": ( + "previous session's last candle close, not the official 15:30 close" + ), + "note": ( + "This verdict selects the branch of a conditional pre-open plan. " + "It is measured, not estimated -- do not re-derive it. Below the " + "threshold the open is FLAT whatever the sign, because a move too " + "small to act before the crowd can decide has recruited nobody." + ), + } + return { "available": True, "last_price": round(last_price, 2), "pivot": pivot, "previous_day_ohlc": prev_day, "previous_close": prev_day["close"] if prev_day else None, + "open_classification": open_classification, "today": today_levels, "psych_levels": psych, "psych_step": step, diff --git a/Signal Generators/SL Hunting AI Agent/sl_hunting_knowledge.py b/Signal Generators/SL Hunting AI Agent/sl_hunting_knowledge.py index 661cccd..8bb0168 100644 --- a/Signal Generators/SL Hunting AI Agent/sl_hunting_knowledge.py +++ b/Signal Generators/SL Hunting AI Agent/sl_hunting_knowledge.py @@ -579,6 +579,14 @@ day. State the classification and the reason for it in your entry reasoning, so a wrong call is visible as a wrong CALL rather than hidden inside a good-looking setup. + SLH-017 NOW SUPPLIES THE ARITHMETIC, so the only part still yours is the + meaning. The `levels` tool returns an `open_classification` block carrying the + gap in points and in percent, measured against the previous session's last + candle, together with the threshold and a verdict of FLAT / GAP_UP / GAP_DOWN. + READ THAT VERDICT AND STATE IT. Do not recompute it, and do not argue past it + because a setup looks good on the other branch -- that is precisely how this + went wrong three times. What remains a judgement is what the classification + MEANS for the crowd, never whether the number clears the line. * COMPARE THE OPEN TO THE 3:15 LEVEL, NOT THE OFFICIAL CLOSE (v4u). IH is explicit that this is the reference he uses: "you will see BankNIFTY has closed somewhere here, but more than the closing, we go by where the market diff --git a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_indicators.py b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_indicators.py index 7e54046..e56c4c9 100644 --- a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_indicators.py +++ b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_indicators.py @@ -107,3 +107,115 @@ def test_empty_frame_is_handled(): empty = pd.DataFrame(columns=["timestamp", "open", "high", "low", "close"]) assert pivot_and_levels(empty)["available"] is False assert fibo_levels(empty)["available"] is False + + +# --------------------------------------------------------------------------- +# SLH-017: the open is CLASSIFIED in code, not derived by the model +# --------------------------------------------------------------------------- + +def _two_sessions(prev_close, today_open): + """Prior session ending at `prev_close`, today opening at `today_open`.""" + prev = _ohlc( + [(prev_close, prev_close + 5, prev_close - 5, prev_close)], + start="2026-09-15 09:15", + ) + today = _ohlc( + [(today_open, today_open + 2, today_open - 2, today_open)], + start="2026-09-16 09:15", + ) + return pd.concat([prev, today], ignore_index=True) + + +def test_the_open_classification_measures_the_gap_against_the_prior_session_close(): + """SLH-017. The arithmetic the model kept doing by hand, done once in code. + + On 2026-09-16 the model computed "+82pts (0.36%)" correctly and then called + it a gap-up, which flipped the pre-open note's branch and produced four + losing longs. v4t already calibrated the threshold; nothing applied it. + """ + result = pivot_and_levels(_two_sessions(23118.60, 23200.60)) + + oc = result["open_classification"] + assert oc["available"] is True + assert oc["previous_close"] == 23118.60 + assert oc["today_open"] == 23200.60 + assert oc["gap_points"] == 82.0 + assert oc["gap_pct"] == round(100.0 * 82.0 / 23118.60, 3) + + +def test_a_third_of_a_percent_is_classified_FLAT_not_a_gap(): + """The 16 Sep open itself: +0.355%, below the half-percent line -> FLAT.""" + oc = pivot_and_levels(_two_sessions(23118.60, 23200.60))["open_classification"] + + assert oc["gap_pct"] < 0.5 + assert oc["verdict"] == "FLAT" + + +def test_half_a_percent_or_more_earns_the_gap_reading(): + """v4t: "something in the region of half a percent" is where a gap starts.""" + up = pivot_and_levels(_two_sessions(20000.0, 20100.0))["open_classification"] + assert up["gap_pct"] == 0.5 + assert up["verdict"] == "GAP_UP" + + down = pivot_and_levels(_two_sessions(20000.0, 19900.0))["open_classification"] + assert down["gap_pct"] == -0.5 + assert down["verdict"] == "GAP_DOWN" + + +def test_just_under_the_threshold_still_reads_FLAT_on_both_sides(): + """Hesitation resolves to flat, so the boundary is inclusive of the gap side + and everything below it is flat -- in BOTH directions.""" + up = pivot_and_levels(_two_sessions(20000.0, 20099.0))["open_classification"] + assert up["verdict"] == "FLAT" + + down = pivot_and_levels(_two_sessions(20000.0, 19901.0))["open_classification"] + assert down["verdict"] == "FLAT" + + +def test_the_classification_names_its_reference_and_its_threshold(): + """A verdict the model cannot audit is one it will re-derive its own way.""" + oc = pivot_and_levels(_two_sessions(23118.60, 23200.60))["open_classification"] + + assert oc["threshold_pct"] == 0.5 + # v4u: the reference is the prior session's LAST CANDLE, not the official + # 15:30 close, because settlement prints are not positioning. + assert "last candle" in oc["reference"] + assert "15:30" in oc["reference"] + + +def test_the_gap_is_measured_from_the_OPEN_and_does_not_drift_intraday(): + """Caught by mutation testing: every other fixture here opens and closes at + the same price, so swapping the OPEN for the last price passed them all. + + It matters because the classification is a statement about the OPEN. If it + read the live price instead, a flat open would quietly become a "gap up" by + mid-morning and re-select the branch of the pre-open plan hours after the + fact -- the exact failure this block exists to prevent, arriving by a + different door. + """ + prev = _ohlc([(20000.0, 20005.0, 19995.0, 20000.0)], start="2026-09-15 09:15") + # Opens +0.05% (flat), then rallies 1.5% during the session. + today = _ohlc( + [ + (20010.0, 20015.0, 20005.0, 20012.0), + (20012.0, 20310.0, 20010.0, 20300.0), + ], + start="2026-09-16 09:15", + ) + + oc = pivot_and_levels(pd.concat([prev, today], ignore_index=True))["open_classification"] + + assert oc["today_open"] == 20010.0 + assert oc["gap_points"] == 10.0 + assert oc["verdict"] == "FLAT" + + +def test_a_single_session_cannot_classify_its_own_open(): + """With no prior day there is no reference, and the block says so rather + than guessing from today's first candle.""" + today = _ohlc([(100, 103, 99, 102)], start="2026-09-16 09:15") + + oc = pivot_and_levels(today)["open_classification"] + + assert oc["available"] is False + assert oc["verdict"] is None diff --git a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_schema.py b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_schema.py index ea437a0..4759825 100644 --- a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_schema.py +++ b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_schema.py @@ -2032,6 +2032,15 @@ def test_v4t_open_classification_rule_is_behavioural_and_carries_its_calibration assert "on the wrong side of the day" in rule assert "State the classification and the reason for it" in rule + # SLH-017 wiring: the arithmetic is supplied, so the rule must say so and + # must forbid re-deriving it. An unwired fact is one the model will simply + # recompute its own way, which is the failure this whole rule is about. + assert "SLH-017 NOW SUPPLIES THE ARITHMETIC" in rule + assert "open_classification" in rule + assert "READ THAT VERDICT AND STATE IT" in rule + assert "Do not recompute it" in rule + assert "never whether the number clears the line" in rule + def test_v4t_early_retracement_rule_points_at_follow_not_fade(): """v4t: the first pull-back keeps the crowd OUT, so nobody gets seated. From c864e694c28bbd8de42cfa0231887652805c9f82 Mon Sep 17 00:00:00 2001 From: DoRmAmMu1997 Date: Wed, 16 Sep 2026 17:01:01 +0530 Subject: [PATCH 3/4] fix(sl-hunting): SLH-018 - one round of friction on a reasonless EXIT Follow-up to v5f / SLH-017 on this PR, and an explicit operator decision rather than a fix I could take on my own. On 16 Sep at 09:16:38 an EXIT carrying no reason executed and closed the day's only winner (+Rs.1,182 basket), about 100 seconds before that trade's 23276 target printed. The order tool logged it as "a probable UNINTENDED order" and honoured it anyway; the model's own next decision called the call erroneous. I raised this as a gate and then declined to build it, because refusing such an exit reverses a standing decision: SLH-007 says an EXIT is never refused over its wording, since bouncing it would strand an open position; SLH-010 explicitly declined to reverse that and added the warning instead; and test_exit_with_no_reason_still_executes_but_warns_loudly existed so that "a later tightening cannot quietly turn the warning into a refusal". The operator then took the decision explicitly, choosing the one-round form on the reasoning that it "would filter out the anomalies and let SLH-007 work on genuine wordless exit cases". SLH-007's guarantee is NARROWED, not reversed. THE CONTRACT. The first EXIT that resolves to the no-reason sentinel is sent back once, with a rejection naming both ways forward - state the reason, or send the EXIT again. Any repeat is honoured unconditionally whatever it carries. The worst case is ONE decision cycle, never a stranded position, and the stop, target, max-loss and 15:15 square-off are host-owned throughout and are never gated by any of this. WHERE THE STATE LIVES MATTERS. The armed flag sits on the EXECUTOR, not on the tool context. A context is rebuilt every bar, so context-local state would bounce the first attempt of every bar forever and could genuinely strand a position - the exact outcome SLH-007 forbids. The executor is created once per worker and outlives the pass. Set and cleared duck-typed under contextlib.suppress, because TradeExecutor is a Protocol and a third-party executor need not tolerate the attribute. SCOPED PER EPISODE: any accepted order clears the arm - the repeat exit, a restated reason, or a new entry - so a bounce spent on this morning's slip cannot buy a later position a free pass. Placeholder reasons bounce too: _safe_exit_reason maps "placeholder", "n/a" and friends to the same sentinel as an empty string, so they are the same class of anomaly. test_do_order_never_rejects_an_exit_for_a_bad_reason was rewritten as test_a_junk_exit_reason_bounces_once_then_exits_and_records_the_sentinel, keeping its real assertions: the position still comes out, and the junk still never becomes the permanent record. THE TOOL DESCRIPTION HAD TO CHANGE, or the model keeps a false contract. It said an EXIT "is NEVER rejected". It now states the single exception and the way out of it, and the test asserts the stale absolute is GONE rather than merely that the new wording is present. A model told exits can never be refused, that then receives a refusal, has been handed a contract its tool does not keep - the SLH-017 unwired-fact failure arriving from the other side. TDD: five tests RED first, then the implementation. Negative-tested 7 ways, all 7 caught, plus a control mutation of an unasserted comment that correctly did not trip the tests. The two that matter most are both caught: removing the bounce, and never disarming (which would strand a position by refusing the second attempt as well). Gates: 606 master, 28 market-data-health, 1508 pytest (+4), ruff 0.16.7, mypy (80 files + master), compileall, bandit, coverage 73.8% policy pass. Co-Authored-By: Claude Opus 5 --- .../SL Hunting AI Agent/sl_hunting_doc.md | 75 ++++++--- .../SL Hunting AI Agent/sl_hunting_tools.py | 49 +++++- .../test_sl_hunting_agent.py | 159 +++++++++++++++--- 3 files changed, 234 insertions(+), 49 deletions(-) diff --git a/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md b/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md index cc2f9a5..222f968 100644 --- a/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md +++ b/Signal Generators/SL Hunting AI Agent/sl_hunting_doc.md @@ -7305,27 +7305,62 @@ could not see: a flat open would silently become a "gap up" by mid-morning and re-select the branch hours after the fact. A dedicated drift test was added and the mutation then caught. -### NOT built: refusing an EXIT that carries no reason +## SLH-018 - one round of friction on a reasonless EXIT (operator decision) The session's other item was that at 09:16:38 an EXIT with no reason executed and closed the day's only winner (+Rs.1,182 basket), about 100 seconds before that trade's 23276 target printed. The order tool logged it as "a probable -UNINTENDED order" and honoured it anyway; the model's next decision called it -erroneous itself. - -Turning that warning into a refusal was considered and **rejected, because it -reverses a standing operator decision**. SLH-007 states that an EXIT is never -refused over its wording -- bouncing it would strand an open position, which -the risk rules forbid -- and SLH-010 explicitly declined to reverse it, adding -the warning instead. `test_exit_with_no_reason_still_executes_but_warns_loudly` -exists precisely so that "a later tightening cannot quietly turn the warning -into a refusal", and it asserts the exit is accepted. - -So the change would mean deleting a guard written to prevent it. If it is ever -revisited, the narrow form worth considering is a ONE-ROUND bounce: reject the -first reasonless EXIT with a message asking for the justification, and honour -the next EXIT unconditionally whatever it carries. That cannot strand a -position beyond a single decision cycle, and the mechanical stop, target, -max-loss and 15:15 square-off are host-owned throughout and never gated. It -still needs an explicit operator decision, because it is SLH-007 that is being -narrowed. +UNINTENDED order" and honoured it anyway; the model's own next decision called +the call erroneous. + +This was first raised and **declined**, because refusing such an exit reverses a +standing decision: SLH-007 says an EXIT is never refused over its wording, since +bouncing it would strand an open position; SLH-010 explicitly declined to reverse +that and added the warning instead; and +`test_exit_with_no_reason_still_executes_but_warns_loudly` existed so that "a +later tightening cannot quietly turn the warning into a refusal". + +**The operator then took the decision explicitly (2026-09-16), choosing the +one-round form**, on the reasoning that it "would filter out the anomalies and +let SLH-007 work on genuine wordless exit cases". SLH-007's guarantee is +NARROWED, not reversed, and the tightening is neither quiet nor unilateral. + +**The contract.** The first EXIT that resolves to the no-reason sentinel is sent +back once, with a rejection naming both ways forward -- state the reason, or send +the EXIT again. Any repeat is honoured unconditionally, whatever it carries. So +the worst case is ONE decision cycle, never a stranded position, and the +mechanical stop, target, max-loss and 15:15 square-off are host-owned throughout +and are never gated by any of this. + +**Where the state lives matters.** The armed flag sits on the EXECUTOR, not on +the tool context. A context is rebuilt every bar, so context-local state would +bounce the first attempt of every bar forever and could genuinely strand a +position -- the exact outcome SLH-007 forbids. The executor is created once per +worker (`self._executor = MasterWorkerExecutor(self)`) and outlives the pass. It +is set and cleared duck-typed through `getattr`/`setattr` under +`contextlib.suppress`, because `TradeExecutor` is a Protocol and a third-party +executor need not tolerate the attribute. + +**Scoped per episode.** Any ACCEPTED order clears the arm -- the repeat exit, a +restated reason, or a new entry. A bounce spent on this morning's slip cannot buy +a later position a free pass. + +**Placeholder reasons bounce too.** `_safe_exit_reason` maps "placeholder", +"n/a" and friends to the same sentinel as an empty string, so they are the same +class of anomaly and get the same single bounce. +`test_do_order_never_rejects_an_exit_for_a_bad_reason` was rewritten as +`test_a_junk_exit_reason_bounces_once_then_exits_and_records_the_sentinel`, +keeping its real assertions: the position still comes out, and the junk still +never becomes the permanent record. + +**The tool description had to change, or the model keeps a false contract.** It +said an EXIT "is NEVER rejected". It now states the single exception and the way +out of it, and a test asserts the stale absolute is GONE rather than merely that +the new wording is present -- a model told exits can never be refused, that then +receives a refusal, has been handed a contract its tool does not keep. That is +the SLH-017 unwired-fact failure arriving from the other side. + +Negative-tested 7 ways, all 7 caught, plus a control mutation of an unasserted +comment that correctly did not trip the tests. The two mutations that matter +most are both caught: removing the bounce entirely, and never disarming (which +would strand a position by refusing the second attempt as well). diff --git a/Signal Generators/SL Hunting AI Agent/sl_hunting_tools.py b/Signal Generators/SL Hunting AI Agent/sl_hunting_tools.py index 91460e2..3f708ea 100644 --- a/Signal Generators/SL Hunting AI Agent/sl_hunting_tools.py +++ b/Signal Generators/SL Hunting AI Agent/sl_hunting_tools.py @@ -30,6 +30,7 @@ from __future__ import annotations +import contextlib import json import logging import math @@ -124,9 +125,11 @@ def order_tool_description(venue: str, *, note_active: bool = False) -> str: "ITSELF THE ACTION -- it places a real order at the moment you call it. There " "is no HOLD action and there is no dry run: if your decision is to HOLD, or to " "keep an open position, DO NOT CALL THIS TOOL AT ALL. Note especially that an " - "EXIT is executed immediately and is NEVER rejected -- unlike an entry it is " - "not validated and cannot be corrected or undone, so an EXIT sent by mistake " - "closes the position for good. action is ENTER_LONG " + "EXIT is executed immediately and, unlike an entry, is not validated -- an " + "executed EXIT cannot be corrected or undone, so one sent by mistake closes " + "the position for good. THE ONLY THING THAT CAN SEND AN EXIT BACK IS OMITTING " + "THE REASON, and only ONCE: say why, or send the EXIT again and it is honoured " + "whatever it carries. action is ENTER_LONG " "(buy CALL), ENTER_SHORT (buy PUT) or EXIT. stop and target are NIFTY " "UNDERLYING price levels (required for entries; ignored for EXIT). They must " "BRACKET the current price on the correct side (LONG: stop below, target " @@ -138,8 +141,9 @@ def order_tool_description(venue: str, *, note_active: bool = False) -> str: "NOT saved there, so this is your only chance to say why you acted. Give a real " f"one-line justification naming the setup and the trigger; it is trimmed to " f"{MAX_REASON_CHARS} printable characters, so put the point FIRST. A placeholder " - "or an empty string is REJECTED on entries (resend a real one), and on an exit it " - "is replaced by a sentinel recording that you supplied nothing. " + "or an empty string is REJECTED on entries (resend a real one); on an EXIT it is " + "sent back once, and if you repeat the EXIT it is honoured and a sentinel records " + "that you supplied nothing. " "exit_leg (EXIT only) is NIFTY, BNF or BOTH " "(default BOTH): every NIFTY entry is mirrored by an equal-lot BankNIFTY ATM leg, " "and you may cut ONE leg on premise-invalidation while the other runs — but hard " @@ -532,11 +536,38 @@ def executor_call() -> dict[str, Any]: # only because the model confessed on the next bar. Say it # plainly instead, so the operator sees it the same session. LOGGER.warning( - "SL Hunting: EXIT (leg=%s) executed with NO reason from the model. " + "SL Hunting: EXIT (leg=%s) arrived with NO reason from the model. " "A deliberate exit always carries one, so treat this as a probable " "UNINTENDED order and check whether the position should have been held.", leg, ) + # SLH-018 (operator decision, 2026-09-16): send the FIRST one back + # once, and honour the next unconditionally. SLH-007's guarantee is + # narrowed, not reversed -- the promise it protects is that a + # position is never stranded, and a repeat exits on the very next + # attempt, so the worst case is one decision cycle. What this buys + # is the anomaly: on 16 Sep 09:16:38 a reasonless EXIT closed the + # day's only winner about 100 seconds before its target printed, + # and the model's own next decision called the call erroneous. A + # model that means it says so twice; a slip does not. + # + # The armed flag lives on the EXECUTOR, not on this context: a + # context is rebuilt every bar, so context-local state would bounce + # every first-of-bar attempt forever and could strand a position. + # The executor is created once per worker and outlives the pass. + if not bool(getattr(self.executor, "_reasonless_exit_armed", False)): + with contextlib.suppress(Exception): + self.executor._reasonless_exit_armed = True # type: ignore[attr-defined] + return { + "accepted": False, + "reason": ( + "EXIT refused ONCE because it carried no reason. A deliberate " + "exit always states why, and that string is the permanent " + "record. If the premise really is dead, send the EXIT again " + "-- the next one is honoured whatever it carries. If this " + "call was not intended, HOLD instead." + ), + } def executor_call() -> dict[str, Any]: return self.executor.exit(exit_reason, self.last_price, leg=leg) else: @@ -570,6 +601,12 @@ def executor_call() -> dict[str, Any]: # `_reason_meaning_problem` above rejects placeholders on entries. self._execution_result = {**result, "model_reason": recorded_reason} self.state = ToolContextState.CONSUMED + # SLH-018: the arm is per EPISODE, not per process. Any accepted + # order means the anomaly resolved itself -- the model restated + # its reason, repeated the exit, or opened something new -- so the + # next slip is bounced again rather than cashing in a stale arm. + with contextlib.suppress(Exception): + self.executor._reasonless_exit_armed = False # type: ignore[attr-defined] else: # Rejected validation/execution may be corrected once within the # same pass; only an accepted side effect consumes the context. diff --git a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_agent.py b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_agent.py index c4a66a5..009f063 100644 --- a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_agent.py +++ b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_agent.py @@ -796,8 +796,14 @@ def test_a_realistic_model_reason_is_accepted_unchanged(): assert ex.pos.reason == realistic -def test_do_order_never_rejects_an_exit_for_a_bad_reason(): - """An exit must always be available -- blocking it would strand a position.""" +def test_a_junk_exit_reason_bounces_once_then_exits_and_records_the_sentinel(): + """An exit is still always AVAILABLE -- now on the second attempt (SLH-018). + + "placeholder" resolves to the very same no-reason sentinel as an empty + string, so it is the same class of anomaly and earns the same single + bounce. What SLH-007 guarantees is untouched: the position comes out, and + the junk never becomes the permanent record. + """ ex = StandaloneExecutor() ctx = SLHuntingToolContext.build(_candles(), ex) price = ctx.last_price @@ -805,8 +811,15 @@ def test_do_order_never_rejects_an_exit_for_a_bad_reason(): "ENTER_LONG", stop=price - 30, target=price + 60, reason="pivot bounce with confirmation", )["accepted"] is True - exit_ctx = SLHuntingToolContext.build(_candles(), ex) - out = exit_ctx.do_order("EXIT", stop=0.0, target=0.0, reason="placeholder") + bounced = SLHuntingToolContext.build(_candles(), ex).do_order( + "EXIT", stop=0.0, target=0.0, reason="placeholder" + ) + assert bounced["accepted"] is False + assert ex.snapshot()["in_position"] is True, "the bounce must not close anything" + + out = SLHuntingToolContext.build(_candles(), ex).do_order( + "EXIT", stop=0.0, target=0.0, reason="placeholder" + ) assert out["accepted"] is True assert ex.snapshot()["in_position"] is False # The junk never becomes the permanent record; it is replaced by a sentinel @@ -1196,8 +1209,14 @@ def test_order_tool_description_says_hold_means_not_calling_the_tool(): A model deliberating "HOLD vs EXIT" found no way to express HOLD through the tool, and EXIT was the nearest available option. It must also learn - that an EXIT is unlike an entry: never validated, never rejected, and not - correctable within the pass. + that an EXIT is unlike an entry: not validated, and not correctable once + it has executed. + + SLH-018 narrowed the old "never rejected" wording, so the description must + now state the one exception exactly. A model told an EXIT can never be + refused, that then receives a refusal, has been handed a contract its tool + does not keep -- which is the same unwired-fact failure SLH-017 exists to + prevent, arriving from the other side. """ from sl_hunting_tools import order_tool_description @@ -1205,34 +1224,63 @@ def test_order_tool_description_says_hold_means_not_calling_the_tool(): assert "calling this tool is itself the action" in text assert "do not call this tool at all" in text assert "there is no hold action" in text - # The asymmetry that made this expensive: entries bounce, exits do not. - assert "never rejected" in text + # Still not validated, and still final once it has gone through. assert "cannot be corrected or undone" in text + # The single, bounded exception -- and the way out of it. + assert "the only thing that can send an exit back is omitting the reason" in text + assert "only once" in text + assert "send the exit again and it is honoured" in text + # The stale absolute must be gone, or the model keeps the old contract. + assert "never rejected" not in text + +def _enter(ex): + """Open a position through a fresh context, as a real pass would.""" + ctx = SLHuntingToolContext.build(_candles(), ex, live_active=False, broker=None) + assert ctx.do_order( + "ENTER_SHORT", stop=25060, target=24950, reason="opening drive breakdown" + )["accepted"] is True -def test_exit_with_no_reason_still_executes_but_warns_loudly(caplog): - """SLH-007 is NOT reversed: the exit goes through regardless of wording. - Blocking it would strand an open position, which the risk rules forbid. - What changes is that it stops being silent -- a deliberate exit always - carries a justification, so an empty one is the strongest signal available - that the call was unintended. +def _exit_no_reason(ex, caplog=None): + """One reasonless EXIT through its own fresh context (one decision pass).""" + import logging + + ctx = SLHuntingToolContext.build(_candles(), ex, live_active=False, broker=None) + if caplog is None: + return ctx.do_order("EXIT", stop=0, target=0, reason="") + with caplog.at_level(logging.WARNING, logger="sl_hunting_tools"): + return ctx.do_order("EXIT", stop=0, target=0, reason="") + + +def test_the_first_reasonless_exit_is_bounced_once_and_announced(caplog): + """SLH-018 narrows SLH-007 by an explicit operator decision (2026-09-16). + + SLH-007 said an EXIT is never refused over its wording, because bouncing it + would strand a position. That still governs -- see the test below, where the + second attempt is honoured no matter what it carries. What changed is that + the FIRST one now gets sent back once, because on 16 Sep 09:16:38 a + reasonless EXIT closed the day's only winner (+Rs.1,182 basket) about 100 + seconds before that trade's target printed, and the model's own next + decision called the call erroneous. One round of friction separates the slip + from the genuine wordless exit; it cannot strand anything, because the next + attempt goes through. """ import logging ex = StandaloneExecutor() - ctx = SLHuntingToolContext.build(_candles(), ex, live_active=False, broker=None) - assert ctx.do_order("ENTER_SHORT", stop=25060, target=24950, reason="opening drive breakdown")["accepted"] is True + _enter(ex) - ctx = SLHuntingToolContext.build(_candles(), ex, live_active=False, broker=None) - with caplog.at_level(logging.WARNING, logger="sl_hunting_tools"): - result = ctx.do_order("EXIT", stop=0, target=0, reason="") + result = _exit_no_reason(ex, caplog) - # The exit is honoured. - assert result["accepted"] is True - assert ex.snapshot()["in_position"] is False + # Sent back, not executed. + assert result["accepted"] is False + assert ex.snapshot()["in_position"] is True, "the position must still be open" + # The rejection tells the model exactly how to proceed, both ways. + assert "reason" in result["reason"].lower() + assert "again" in result["reason"].lower() - # ...and it is announced. + # ...and the operator still hears about it the same session. warnings = [r for r in caplog.records if r.levelno >= logging.WARNING] assert warnings, "an EXIT with no reason must warn" message = warnings[0].getMessage().lower() @@ -1240,6 +1288,71 @@ def test_exit_with_no_reason_still_executes_but_warns_loudly(caplog): assert "unintended" in message +def test_the_second_reasonless_exit_is_honoured_unconditionally(): + """SLH-007's guarantee survives: a repeat cannot be refused. + + This is the half that makes the bounce safe. A model that really means it + says so twice, and the position is out on the very next attempt -- so the + worst case is one decision cycle, never a stranded position. + """ + ex = StandaloneExecutor() + _enter(ex) + + assert _exit_no_reason(ex)["accepted"] is False + second = _exit_no_reason(ex) + + assert second["accepted"] is True + assert ex.snapshot()["in_position"] is False + + +def test_a_reasoned_exit_is_never_bounced_even_after_a_bounce(): + """The ordinary path is untouched: supplying the reason exits immediately.""" + ex = StandaloneExecutor() + _enter(ex) + + assert _exit_no_reason(ex)["accepted"] is False + + ctx = SLHuntingToolContext.build(_candles(), ex, live_active=False, broker=None) + result = ctx.do_order("EXIT", stop=0, target=0, reason="premise invalidated at the level") + + assert result["accepted"] is True + assert ex.snapshot()["in_position"] is False + + +def test_the_bounce_re_arms_for_each_new_position(): + """Scoped per episode, not once per process. + + A bounce spent on this morning's slip must not buy a later position a free + pass, so any accepted order clears the armed state. + """ + ex = StandaloneExecutor() + + _enter(ex) + assert _exit_no_reason(ex)["accepted"] is False + assert _exit_no_reason(ex)["accepted"] is True + + # A brand-new position gets its own bounce. + _enter(ex) + assert _exit_no_reason(ex)["accepted"] is False + assert _exit_no_reason(ex)["accepted"] is True + + +def test_a_reasoned_exit_clears_the_armed_state_too(): + """Accepting ANY order resolves the anomaly, so the next slip bounces again.""" + ex = StandaloneExecutor() + _enter(ex) + + assert _exit_no_reason(ex)["accepted"] is False + + ctx = SLHuntingToolContext.build(_candles(), ex, live_active=False, broker=None) + assert ctx.do_order("EXIT", stop=0, target=0, reason="real reason")["accepted"] is True + + _enter(ex) + # Armed state was cleared by the accepted exit, so this bounces rather than + # cashing in the arm left over from before. + assert _exit_no_reason(ex)["accepted"] is False + + def test_exit_with_a_real_reason_does_not_warn(caplog): """The guard must stay quiet on ordinary exits or it becomes noise.""" import logging From aeafe486b70c617fb924fb1f52ab8732737d6bf8 Mon Sep 17 00:00:00 2001 From: DoRmAmMu1997 Date: Wed, 16 Sep 2026 22:35:06 +0530 Subject: [PATCH 4/4] chore(sl-hunting): pre-open note for 17 SEP 2026 Source: 'Prediction For 17 SEP 2026' (SQIo4PisLw0, uploaded 2026-09-16 21:25 IST). Addendum to the v5f / SLH-017 / SLH-018 PR. The branch SHAPE repeats 16 Sep's note - gap up -> BUY, flat or gap down -> SELL, stated for all three indices - so what the test pins is the different reasoning underneath it. Today's selling broke down and then recovered WITHOUT crossing the higher point or the round number, which leaves one question open: which side did the session trap? flat / gap down -> SELL the sellers were only trapped, never seated gap up -> BUY a lone deliberate buyer may be, and only then His mechanism for the sell branch: "seeing the retracement, who comes here? SELLERS come. But because the market stays SIDEWAYS they leave again." The sideways is what removed them, so there is no seller crowd left to squeeze and the plan is simply to go with the selling. The buy branch keys off what price did NOT do - "it did not cross the round number, it did not cross its high point" - so "maybe some good trader slowly bought here and other people could not buy". Because "the higher point was not crossed, retail did not do much work", only a deliberate buyer may be seated, never a crowd, which is exactly why that branch needs the gap up to confirm one exists rather than assuming it. He also flags SENSEX expiry twice, as context for the triple-index read and not as a premise. Levels, with the ASR garbles resolved by the operator against the video: NIFTY R 23420 / 23280 S 23110 / 23000 BANKNIFTY R 56300 / 56650 S 55810 / 55500 SENSEX R 75000 / 74500 S 74000 / 73650 Three needed confirming. NIFTY's first support came through as "2310" and is 23110 - a dropped digit, not a dropped trailing zero, so my 23100 reading was wrong. BankNIFTY's second support came through as "5500" again and is 55500 again. SENSEX was the worst yet: both pairs arrived as single runs, "7574500" and "7473650", and resolve to round thousands - 75000/74500 and 74000/73650 - which neither reconstruction I offered had guessed. Recorded in the test comments so the next garble of the same shape reads round numbers first. Negative-tested 18 ways on the note itself, all 18 caught, plus a control mutation of unasserted prose that correctly did NOT trip the test - including guards on all three corrected levels, so a silent revert to my wrong readings fails. No doc section: notes have not carried one (PR #168 precedent); provenance lives in the test docstring and here. Gates: 606 master, 28 market-data-health, 1508 pytest, ruff 0.16.7, mypy (80 files + master), compileall, bandit. Co-Authored-By: Claude Opus 5 --- .../SL Hunting AI Agent/premarket_note.json | 35 +++-- .../test_sl_hunting_premarket.py | 137 +++++++++--------- 2 files changed, 84 insertions(+), 88 deletions(-) diff --git a/Signal Generators/SL Hunting AI Agent/premarket_note.json b/Signal Generators/SL Hunting AI Agent/premarket_note.json index fb88003..608b28a 100644 --- a/Signal Generators/SL Hunting AI Agent/premarket_note.json +++ b/Signal Generators/SL Hunting AI Agent/premarket_note.json @@ -1,46 +1,45 @@ { - "for_date": "2026-09-16", - "source": "Intraday Hunter, 'Prediction For 16 SEP 2026' (2siXRlm4_jo, uploaded 2026-09-15)", - "context": "15 Sep gave back the prior day's whole positive move -- 'as much positive momentum as came a day before, that much selling has already happened'. The market is ALREADY negative, so POSITIONAL SELLERS are now seated and in profit.", + "for_date": "2026-09-17", + "source": "Intraday Hunter, 'Prediction For 17 SEP 2026' (SQIo4PisLw0, uploaded 2026-09-16)", + "context": "Today's selling broke down and then RECOVERED, without crossing the higher point or the round number. The session therefore leaves one question open -- which side it trapped -- and SENSEX has its expiry tomorrow.", "plan": [ - "THE PLAN IS CONDITIONAL ON THE OPEN, inverting yesterday's all-buy note: a good GAP UP means BUY, FLAT or GAP DOWN means SELL. He repeats it for all three indices, so the open picks the side before any setup is read.", - "GAP UP -> BUY, because the gap traps the seated sellers: 'the market is already negative and made good momentum today too, so there can be POSITIONAL sellers here -- if we get a good gap up we CAN target these sellers'.", - "FLAT OR GAP DOWN -> SELL, and the reason is the ABSENCE of a target rather than weakness: 'they will already be sitting in confidence so we cannot target them -- there we must go WITH the market'.", - "A GAP DOWN IS THE EASY CASE, NOT A WARNING: 'if a gap down opens that is a very good thing, because momentum is already good, so walking with the market will be easy'. Flat is fine too -- 'in flat there is no problem either'.", - "TARGET THE LATE SELLERS, NOT THE EARLY ONES: 'the upper-side seller we cannot make our target, but those sitting having SOLD HERE can be targeted'. The cohort that sold into today's lows is the marginal one.", - "SELLERS IN PROFIT CANNOT BE HUNTED QUICKLY: 'the sellers will come into some profit, and second, confidence will stay inside them, so we cannot make a target quickly'. Profit plus confidence is what removes them as prey." + "SAME CONDITIONAL SHAPE AS YESTERDAY, DIFFERENT REASON: gap up -> BUY, flat or gap down -> SELL, stated for all three indices. The question underneath is WHICH crowd today trapped, and the open is what answers it.", + "FLAT OR GAP DOWN -> SELL, because the sellers were only TRAPPED and never seated: 'seeing the retracement, who comes here? SELLERS come. But because the market stays SIDEWAYS they leave again' -- so no seller crowd is left to squeeze.", + "GAP UP -> BUY, the tell is what price did NOT do: 'it did not cross the round number, it did not cross its high point', so 'maybe some good trader slowly bought here and other people could not buy'. That thin long inventory is the target.", + "THE UNCROSSED HIGH IS WHY RETAIL IS ABSENT: 'because the higher point was not crossed, retail did not do much work'. Only a deliberate buyer may be seated, never a crowd, which is why the buy branch needs the gap up to confirm one exists.", + "SENSEX EXPIRES TOMORROW, flagged twice -- in his opening line and again at its levels. That is context for the triple-index read, not a premise of its own." ], "levels": [ { "index": "NIFTY", "resistance": [ - 23280, - 23340 + 23420, + 23280 ], "support": [ - 23080, + 23110, 23000 ] }, { "index": "BANKNIFTY", "resistance": [ - 56650, - 56300 + 56300, + 56650 ], "support": [ - 55500, - 55200 + 55810, + 55500 ] }, { "index": "SENSEX", "resistance": [ - 74800, + 75000, 74500 ], "support": [ - 73800, + 74000, 73650 ] } diff --git a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_premarket.py b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_premarket.py index e3aeeb1..beb8d24 100644 --- a/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_premarket.py +++ b/Tests/Signal Generators/SL Hunting AI Agent/test_sl_hunting_premarket.py @@ -201,24 +201,26 @@ def test_shipped_note_targets_the_next_TRADING_day_not_the_next_calendar_day(): ) -def test_shipped_note_matches_september_16_intraday_hunter_plan(): - """The committed advisory must match the hand-checked 15 Sep transcript. - - This note INVERTS the previous one, which is the whole reason it is pinned: - 14 Sep said every open shape buys, 15 Sep makes the side depend on the open. - Six things a summarising edit would flatten: - - 1. The conditionality itself. An edit that keeps one branch turns a - two-sided plan into a directional call, which is how the previous note - would be "remembered" over this one. - 2. Gap up means BUY, and the reason is that the gap TRAPS seated sellers -- - a hunt. Lose the reason and it reads as a bullish forecast. - 3. Flat or gap down means SELL because there is NOTHING TO HUNT, not - because the market is weak. That distinction is the method. - 4. A gap down is his EASY case, not a warning. Habit reverses this. - 5. Late sellers are the target, early ones are not. The sharpest line in - the video, and the one most likely to be dropped as a detail. - 6. Profit PLUS confidence is what removes a crowd as prey -- both halves. +def test_shipped_note_matches_september_17_intraday_hunter_plan(): + """The committed advisory must match the hand-checked 16 Sep transcript. + + The branch SHAPE repeats 16 Sep's note, so the thing worth pinning is the + different reasoning underneath it: this is a who-got-trapped question, and + the evidence is a level that was NOT crossed. Six things an edit flattens: + + 1. That the shape repeats but the reason does not. Merged with yesterday's + note it reads as one standing call rather than a fresh read. + 2. The sell branch is about sellers who were trapped and LEFT -- the + sideways is what removed them. Lose it and "flat means sell" is arbitrary. + 3. The buy branch keys off what price did NOT do. That is the whole tell, + and a summariser reaches for what price DID do instead. + 4. "Retail did not do much work" is why only a deliberate buyer may be + seated. Without it the buy branch sounds like a crowd hunt, which is the + opposite of what he is describing. + 5. The gap up is what CONFIRMS that buyer exists; the branch is not a + forecast. + 6. SENSEX expiry is context, not a premise -- he flags it twice and trades + none of it. """ import os @@ -226,67 +228,62 @@ def test_shipped_note_matches_september_16_intraday_hunter_plan(): note = load_premarket_note(os.path.join(here, "premarket_note.json")) assert note is not None - assert note.for_date == "2026-09-16" - assert "2siXRlm4_jo" in note.source - assert "gave back the prior day's whole positive move" in note.context - assert "that much selling has already happened" in note.context - assert "ALREADY negative" in note.context - assert "POSITIONAL SELLERS are now seated and in profit" in note.context - - # 1. Both branches survive, and the note says the open decides first. - cond = next(line for line in note.plan if line.startswith("THE PLAN IS CONDITIONAL")) - assert "inverting yesterday's all-buy note" in cond - assert "a good GAP UP means BUY, FLAT or GAP DOWN means SELL" in cond - assert "the open picks the side before any setup is read" in cond - - # 2. The bull branch is a HUNT of trapped sellers, not a forecast. - up = next(line for line in note.plan if line.startswith("GAP UP ->")) - assert "because the gap traps the seated sellers" in up - assert "there can be POSITIONAL sellers here" in up - assert "if we get a good gap up we CAN target these sellers" in up - - # 3. The bear branch is about an absent target, not about weakness. - down = next(line for line in note.plan if line.startswith("FLAT OR GAP DOWN ->")) - assert "the ABSENCE of a target rather than weakness" in down - assert "they will already be sitting in confidence so we cannot target them" in down - assert "there we must go WITH the market" in down - - # 4. The branch habit is most likely to invert. - easy = next(line for line in note.plan if line.startswith("A GAP DOWN IS THE EASY CASE")) - assert "NOT A WARNING" in easy - assert "that is a very good thing" in easy - assert "walking with the market will be easy" in easy - assert "in flat there is no problem either" in easy - - # 5. Which seller cohort is prey, and which is not. - late = next(line for line in note.plan if line.startswith("TARGET THE LATE SELLERS")) - assert "the upper-side seller we cannot make our target" in late - assert "those sitting having SOLD HERE can be targeted" in late - assert "The cohort that sold into today's lows is the marginal one" in late - - # 6. Both halves of what disqualifies a crowd as prey. - profit = next(line for line in note.plan if line.startswith("SELLERS IN PROFIT")) - assert "the sellers will come into some profit" in profit - assert "confidence will stay inside them" in profit - assert "Profit plus confidence is what removes them as prey" in profit + assert note.for_date == "2026-09-17" + assert "SQIo4PisLw0" in note.source + assert "broke down and then RECOVERED" in note.context + assert "without crossing the higher point or the round number" in note.context + assert "which side it trapped" in note.context + assert "SENSEX has its expiry tomorrow" in note.context + + # 1. Same shape, different reason -- and the open is the discriminator. + shape = next(line for line in note.plan if line.startswith("SAME CONDITIONAL SHAPE")) + assert "DIFFERENT REASON" in shape + assert "gap up -> BUY, flat or gap down -> SELL" in shape + assert "WHICH crowd today trapped" in shape + + # 2. The sell branch: trapped, then removed by the sideways. + sell = next(line for line in note.plan if line.startswith("FLAT OR GAP DOWN ->")) + assert "only TRAPPED and never seated" in sell + assert "because the market stays SIDEWAYS they leave again" in sell + assert "no seller crowd is left to squeeze" in sell + + # 3. The buy branch keys off the level that was NOT crossed. + buy = next(line for line in note.plan if line.startswith("GAP UP ->")) + assert "the tell is what price did NOT do" in buy + assert "it did not cross the round number, it did not cross its high point" in buy + assert "thin long inventory is the target" in buy + + # 4 and 5. Why retail is absent, and what the gap up is FOR. + retail = next(line for line in note.plan if line.startswith("THE UNCROSSED HIGH")) + assert "retail did not do much work" in retail + assert "Only a deliberate buyer may be seated, never a crowd" in retail + assert "needs the gap up to confirm one exists" in retail + + # 6. Expiry is context. + expiry = next(line for line in note.plan if line.startswith("SENSEX EXPIRES TOMORROW")) + assert "flagged twice" in expiry + assert "not a premise of its own" in expiry assert [level.model_dump() for level in note.levels] == [ { + # "2310" -- a digit short; confirmed with the operator as 23110. "index": "NIFTY", - "resistance": [23280.0, 23340.0], - "support": [23080.0, 23000.0], + "resistance": [23420.0, 23280.0], + "support": [23110.0, 23000.0], }, { - # "5500" -- the first support arrived a digit short and was - # confirmed with the operator as 55500, matching the 300-350 point - # spacing of his other pairs and sitting below the ~55,850 close. + # "5500" again, and 55500 again, as on the 16 Sep note. "index": "BANKNIFTY", - "resistance": [56650.0, 56300.0], - "support": [55500.0, 55200.0], + "resistance": [56300.0, 56650.0], + "support": [55810.0, 55500.0], }, { + # Worst garble yet: both pairs arrived as single runs, "7574500" + # and "7473650". The operator resolved them to round thousands -- + # 75000/74500 and 74000/73650 -- which neither reconstruction I + # offered had guessed. "index": "SENSEX", - "resistance": [74800.0, 74500.0], - "support": [73800.0, 73650.0], + "resistance": [75000.0, 74500.0], + "support": [74000.0, 73650.0], }, ]