File size: 8,627 Bytes
c201e66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0ac64a
c201e66
 
 
 
 
 
9d6050e
 
 
 
 
c201e66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0ac64a
 
c201e66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d6050e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0ac64a
 
 
 
 
 
 
9d6050e
f0ac64a
 
 
 
 
 
 
 
 
9d6050e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0ac64a
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
"""Click bridge: design markup in, Python state out.

The design is built almost entirely from `<button>` elements -- 23 of them
against a single `<input>` in the whole file. Gradio's own controls render a
completely different DOM (`gr.Radio` becomes a `<fieldset>` of labels wrapping
hidden radio inputs), which is why restyling them from outside only ever
approximates the design.

So the visible layer is the design's own markup, and this module is how a real
`<button>` reaches Python:

    <button data-bit="tf:1h">1h</button>
                |
                |  delegated listener (installed once, in <head>)
                v
    hidden Textbox #bit-action  <- value set + native `input` event dispatched
                |
                v
    Gradio .change() -> parse_action() -> new state -> re-render

A nonce is appended to every action because Gradio's `.change()` only fires when
the value actually differs; clicking the same button twice must still register.

Nothing here evaluates anything. Actions are `key:value` pairs, the key must be
on `ALLOWED_KEYS`, and the value is returned as an opaque string for the caller
to validate against its own domain.
"""

from __future__ import annotations

from dataclasses import dataclass

# Every action the UI is allowed to emit. An unknown key is dropped, so a
# hand-crafted click cannot reach code paths the UI does not offer.
ALLOWED_KEYS = frozenset({
    "tab",          # switch a top-level tab
    "subtab",       # switch a Compare sub-tab
    "acc",          # toggle a left-panel accordion section
    "strategy",     # choose a preset
    "asset",        # choose an asset
    "tf",           # choose a timeframe
    "range",        # choose a date range
    "model",        # choose a forecast model
    "param",        # set a strategy parameter (param:key=value)
    "costs",        # toggle costs on/off
    "slippage",     # choose a slippage model
    "sizing",       # choose a sizing mode
    "validation",   # choose a validation mode
    "metric",       # leaderboard rank metric
    "filter",       # leaderboard filter toggle (filter:kind=value)
    "topn",         # leaderboard row count
    "sigasset",     # signal aggregator asset
    "sigtf",        # signal aggregator timeframe
    "run",          # run the backtest
    "example",      # load the worked example
    "reset",        # reset leaderboard filters
    "logscale",     # toggle log scale
    "cvd",          # toggle colorblind-safe prices
    "noop",         # explicit no-op, used by disabled controls
})

ACTION_ELEMENT_ID = "bit-action"
TRIGGER_ELEMENT_ID = "bit-trigger"

# Separates the action from its nonce. A pipe is used rather than a space
# because action values are human-readable names -- "SMA Crossover", "Buy & Hold
# (benchmark)" -- which legitimately contain spaces. No value contains a pipe.
NONCE_SEP = "|"

# The listener body. Installed two ways, because neither alone is reliable:
# `gr.Blocks(head=...)` works locally but Hugging Face Spaces serves the page
# through its own template and drops it, while `demo.load(js=...)` runs
# client-side wherever the page came from. Both call the same idempotent
# installer, guarded by a window flag, so running twice is harmless.
BRIDGE_JS = """
<script>
(function () {
  if (window.__bitBridgeInstalled) return;
  window.__bitBridgeInstalled = true;

  function holder() {
    var root = document.getElementById('__ELEM_ID__');
    return root ? root.querySelector('textarea, input') : null;
  }

  document.addEventListener('click', function (e) {
    var el = e.target && e.target.closest ? e.target.closest('[data-bit]') : null;
    if (!el) return;
    var action = el.getAttribute('data-bit');
    if (!action || action.indexOf('noop:') === 0) return;
    e.preventDefault();
    e.stopPropagation();

    var box = holder();
    if (!box) return;
    // The nonce makes every click a distinct value, so clicking the same
    // button twice still fires Gradio's change event.
    var nonce = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
    box.value = action + '|' + nonce;
    box.dispatchEvent(new Event('input', { bubbles: true }));
  }, true);

  // Numeric and text fields emit on commit (blur / Enter), not per keystroke,
  // so a backtest is not re-run on every digit typed.
  function commit(el) {
    if (!el || !el.hasAttribute('data-bit-input')) return;
    var box = holder();
    if (!box) return;
    var nonce = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
    box.value = el.getAttribute('data-bit-input') + '=' + el.value + '|' + nonce;
    box.dispatchEvent(new Event('input', { bubbles: true }));
  }

  document.addEventListener('change', function (e) { commit(e.target); }, true);
  document.addEventListener('keydown', function (e) {
    if (e.key === 'Enter') { commit(e.target); }
  }, true);
})();
</script>
""".replace("__ELEM_ID__", ACTION_ELEMENT_ID).replace(
    "__TRIGGER_ID__", TRIGGER_ELEMENT_ID)


@dataclass(frozen=True)
class Action:
    key: str
    value: str

    @property
    def is_noop(self) -> bool:
        return self.key == "noop"


def emit(key: str, value: str = "") -> str:
    """Build the `data-bit` attribute value for a clickable element."""
    if key not in ALLOWED_KEYS:
        raise ValueError(f"{key!r} is not an allowed UI action")
    if NONCE_SEP in str(value):
        raise ValueError(f"action values may not contain {NONCE_SEP!r}")
    return f"{key}:{value}"


def parse_action(raw):
    """Parse what the bridge wrote into the hidden textbox.

    Returns None for anything unparseable or not on the allow-list, so a
    malformed or hand-crafted payload is ignored rather than raising.
    """
    if not raw or not isinstance(raw, str):
        return None
    payload = raw.split(NONCE_SEP, 1)[0]
    if ":" not in payload:
        return None
    key, _, value = payload.partition(":")
    key = key.strip()
    if key not in ALLOWED_KEYS:
        return None
    return Action(key=key, value=value.strip())


def parse_pair(value: str):
    """Split a compound action value like `fast_ma=20` -> ("fast_ma", "20")."""
    name, _, val = value.partition("=")
    return name.strip(), val.strip()


# `demo.load(js=...)` takes a JS *function*. This is the same installer as
# BRIDGE_JS, in the form Gradio's load hook expects, and is the path that
# actually works on Hugging Face Spaces.
BRIDGE_LOAD_JS = """
() => {
  if (window.__bitBridgeInstalled) return;
  window.__bitBridgeInstalled = true;

  function holder() {
    var root = document.getElementById('__ELEM_ID__');
    return root ? root.querySelector('textarea, input') : null;
  }

  function send(payload) {
    var box = holder();
    if (!box) return;
    var nonce = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);

    // Setting .value directly does not reliably wake Svelte's binding, so the
    // native setter is used and both events are dispatched...
    var proto = box.tagName === 'TEXTAREA'
      ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
    var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
    setter.call(box, payload + '|' + nonce);
    box.dispatchEvent(new Event('input', { bubbles: true }));
    box.dispatchEvent(new Event('change', { bubbles: true }));

    // ...and a hidden Gradio Button is then clicked. A real click on a real
    // Button is the one event path Gradio always honours, so the handler fires
    // even if the textbox binding did not register.
    var trigger = document.getElementById('__TRIGGER_ID__');
    var btn = trigger ? (trigger.tagName === 'BUTTON'
                         ? trigger : trigger.querySelector('button')) : null;
    if (btn) { setTimeout(function () { btn.click(); }, 0); }
  }

  document.addEventListener('click', function (e) {
    var el = e.target && e.target.closest ? e.target.closest('[data-bit]') : null;
    if (!el) return;
    var action = el.getAttribute('data-bit');
    if (!action || action.indexOf('noop:') === 0) return;
    e.preventDefault();
    e.stopPropagation();
    send(action);
  }, true);

  function commit(el) {
    if (!el || !el.hasAttribute || !el.hasAttribute('data-bit-input')) return;
    send(el.getAttribute('data-bit-input') + '=' + el.value);
  }
  document.addEventListener('change', function (e) { commit(e.target); }, true);
  document.addEventListener('keydown', function (e) {
    if (e.key === 'Enter') { commit(e.target); }
  }, true);
}
""".replace("__ELEM_ID__", ACTION_ELEMENT_ID).replace(
    "__TRIGGER_ID__", TRIGGER_ELEMENT_ID)