File size: 8,080 Bytes
3589030
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
=============================================================================
 load_reference.py — AMFORGE/tabicl-universal-finetuned
=============================================================================
 Reference loading script for this checkpoint.

 WHY THIS FILE EXISTS
 ---------------------
 TabICL (`FinetunedTabICLClassifier`) is an in-context learner: `.fit(X, y)`
 does two things, not one --
   1. gradient-updates the backbone weights (this is what "fine-tuning" means
      here, and it IS preserved by pickle.dump()/pickle.load()).
   2. encodes the (X, y) context you gave it into an internal representation
      used at inference time (cached as `_X_encoder_` and friends). This
      encoded context is NOT preserved across a pickle round-trip.

 Consequence: `pickle.load()` on `tabicl_finetuned.pkl` will succeed with no
 error, the object looks fully formed, but calling `.predict_proba()`
 directly will raise NotFittedError (missing `_X_encoder_`), because the
 in-context state was never restored -- only the backbone weights were.

 THE FIX
 -------
 Call `.fit()` exactly ONCE per process, right after loading, with a
 representative reference/context batch (built with the exact 60-column
 feature layout below, scaled with the shipped `feature_scaler.pkl`). This
 does not retrain the backbone -- it rebuilds the in-context encoder so
 `.predict_proba()` works.

 FEATURE LAYOUT (60 columns, RAW/unscaled, before scaler.transform)
 --------------------------------------------------------------------------
 Columns 0-48 (49 base features): for tf in [M1, M5, M15, M30, H1, H4, D1]
 (finest -> coarsest), for feat in [close, trend_strength, candle_pattern,
 volatility, market_regime, candlestick_pattern, chart_pattern]:

   close_M1, trend_strength_M1, candle_pattern_M1, volatility_M1,
   market_regime_M1, candlestick_pattern_M1, chart_pattern_M1,
   close_M5, ... chart_pattern_M5,
   close_M15, ... chart_pattern_M15,
   close_M30, ... chart_pattern_M30,
   close_H1, ... chart_pattern_H1,
   close_H4, ... chart_pattern_H4,
   close_D1, ... chart_pattern_D1

 Columns 49-59 (asset-family one-hot, appended AFTER the 49 base columns,
 in this exact order -- 11 categories, this checkpoint covers real assets
 (Gold, BTC) AND synthetic indices in a single model):
   xauusd, btcusd, boom, crash, volatility, flipx, gainx, painx, fxvol,
   sfxvol, other
 -- 1.0 for the symbol's family, 0.0 elsewhere. A symbol not recognized by
 your own family-detection logic falls into "other".

 Each *_tf block is computed on that timeframe's OWN native OHLCV bars (not
 resampled from a finer timeframe), then aligned onto the finest available
 timeframe's grid via merge_asof(direction="backward") -- never a
 not-yet-closed bar from a coarser timeframe. A timeframe not available for
 a given symbol gets its 7 columns filled with 0.0 (deliberate fallback,
 matches production behavior -- not a bug).

 Formulas (c=close, o=open, h=high, l=low, on that timeframe's own bars):
   - close: raw close price c (NOT a return / pct-change).
   - trend_strength: (MA10(c) - MA30(c)) / MA30(c), 0.0 during warm-up.
   - volatility: rolling 99-bar std of c.pct_change(), 0.0 during warm-up.
   - market_regime: 1.0 if MA10(c) > MA50(c) else -1.0; 0.0 while either MA
     is still NaN (warm-up).
   - candle_pattern (single-bar, first match wins):
       body=|c-o|, rng=h-l, upper_shadow=h-max(o,c), lower_shadow=min(o,c)-l
       1 (doji)    if rng!=0 and body/rng < 0.1
       2 (hammer)  elif lower_shadow > 2*body and upper_shadow < 0.5*body
       3 (shoot.*) elif upper_shadow > 2*body and lower_shadow < 0.5*body
       0 otherwise
   - candlestick_pattern (multi-bar, first match wins, shift(1)/shift(2)):
       1 (bull. engulfing) if c>o, prev_c<prev_o, c>prev_o, o<prev_c
       2 (evening star) elif prev2_c>prev2_o, prior bar is a doji
         (|prev_c-prev_o| < 0.1*(prev_h-prev_l)), and c<o
       3 (morning star) elif prev2_c<prev2_o, same doji condition, and c>o
       0 otherwise
   - chart_pattern (first match wins):
       1 (double top)    - two local peaks ~2 bars apart in a 5-bar window,
                            tops within 0.1% of their mean
       2 (double bottom) - same with local bottoms
       3 (head & shoulders) - 7-bar window, left shoulder/head/right
                            shoulder, shoulders within 0.1% of their mean
       0 otherwise

 Labels used during training (0=Buy, 1=Sell, 2=Hold) come from a
 triple-barrier scheme (TP=2.0xATR14, SL=1.0xATR14, horizon=60 bars of the
 finest active timeframe) -> only relevant for reproducing training labels,
 not for inference features.

 IMPORTANT: feature_scaler.pkl was fit on the FULL 60-dim vector, including
 the one-hot columns. Do not refit your own scaler, and do not scale the
 one-hot block separately -- always reuse the shipped feature_scaler.pkl.
 Training also capped samples at 1600 per symbol (balanced representation
 across families) -- irrelevant for inference, only mentioned for context.

 Copyright (c) 2026 AMEFORGE. All rights reserved.
=============================================================================
"""

import pickle
import numpy as np
from huggingface_hub import hf_hub_download

REPO_ID = "AMFORGE/tabicl-universal-finetuned"

TIMEFRAMES_ORDER = ["M1", "M5", "M15", "M30", "H1", "H4", "D1"]
BASE_FEATURE_NAMES = ["close", "trend_strength", "candle_pattern",
                      "volatility", "market_regime", "candlestick_pattern", "chart_pattern"]
FEATURE_COLUMNS = [f"{feat}_{tf}" for tf in TIMEFRAMES_ORDER for feat in BASE_FEATURE_NAMES]
ASSET_FAMILIES = ["xauusd", "btcusd", "boom", "crash", "volatility", "flipx",
                   "gainx", "painx", "fxvol", "sfxvol", "other"]


def load_checkpoint():
    """Downloads and loads the fine-tuned classifier + its feature scaler.
    Returns (clf, scaler). clf is NOT yet ready for predict_proba() at this
    point -- see rebuild_context() below."""
    ckpt_path = hf_hub_download(repo_id=REPO_ID, filename="tabicl_finetuned.pkl")
    scaler_path = hf_hub_download(repo_id=REPO_ID, filename="feature_scaler.pkl")

    with open(ckpt_path, "rb") as f:
        clf = pickle.load(f)
    with open(scaler_path, "rb") as f:
        scaler = pickle.load(f)
    return clf, scaler


def rebuild_context(clf, scaler, X_context, y_context):
    """
    REQUIRED after every load_checkpoint() call, in every fresh process.

    X_context: np.ndarray of shape (n, 60), RAW/unscaled, built with the
               exact FEATURE_COLUMNS + ASSET_FAMILIES layout documented
               above.
    y_context: np.ndarray of shape (n,), labels in {0, 1, 2} (Buy/Sell/Hold).

    This does NOT retrain the backbone weights -- it rebuilds the in-context
    encoder (_X_encoder_) that pickle could not preserve. A slice of your
    own recent, correctly-labeled history is fine as the context set; it
    does not need to be the original training data.
    """
    X_scaled = scaler.transform(np.asarray(X_context, dtype=float))
    clf.fit(X_scaled, np.asarray(y_context))
    return clf


def predict(clf, scaler, X_new):
    """X_new: np.ndarray of shape (n, 60), RAW/unscaled, same 60-column layout.
    Returns array of shape (n, 3): [p_buy, p_sell, p_hold]."""
    X_scaled = scaler.transform(np.asarray(X_new, dtype=float))
    return clf.predict_proba(X_scaled)


if __name__ == "__main__":
    # Minimal end-to-end example. Replace X_context/y_context/X_new with
    # your own real feature vectors built from the layout documented above.
    clf, scaler = load_checkpoint()

    n_context, n_features = 500, len(FEATURE_COLUMNS) + len(ASSET_FAMILIES)
    X_context = np.random.randn(n_context, n_features)  # placeholder -- use real data
    y_context = np.random.randint(0, 3, size=n_context)  # placeholder -- use real labels

    clf = rebuild_context(clf, scaler, X_context, y_context)

    X_new = np.random.randn(3, n_features)  # placeholder -- use real data
    proba = predict(clf, scaler, X_new)
    print("Buy/Sell/Hold probabilities:\n", proba)