File size: 6,194 Bytes
aa63688
92be97d
aa63688
 
 
 
 
 
 
 
92be97d
 
aa63688
 
 
92be97d
479f6da
 
 
 
b5948d3
 
 
479f6da
 
aa63688
 
92be97d
aa63688
479f6da
 
aa63688
479f6da
aa63688
 
 
 
 
 
 
 
 
 
 
479f6da
aa63688
 
 
479f6da
aa63688
 
 
 
 
92be97d
aa63688
 
 
 
 
 
 
 
 
 
 
 
92be97d
aa63688
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92be97d
aa63688
 
 
0d893fe
 
 
 
 
 
 
aa63688
 
 
 
 
 
 
479f6da
aa63688
 
 
 
 
 
92be97d
aa63688
 
 
 
 
 
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
import os, time
from datetime import datetime, timezone
from pathlib import Path
import requests, pandas as pd, numpy as np
import joblib
from sklearn.ensemble import HistGradientBoostingClassifier

ROOT = Path(__file__).parent
DATA = ROOT / "BTCUSD_1m.parquet"
LIVE_DATA = ROOT / "live_candles.parquet"
DELTA = os.getenv("DELTA_URL", "https://api.india.delta.exchange")
SYMBOL = os.getenv("SYMBOL", "BTCUSD")
RECEIVER_URL = os.getenv("RECEIVER_URL", "").rstrip("/")
MOVE, OFFSET, HORIZONS = .0008, .0004, (5, 15, 30)
MODELS = None

def epoch_seconds(value):
    value = float(value)
    if value > 1e14: return int(value / 1e6)
    if value > 1e11: return int(value / 1e3)
    # Some previously cached Delta responses were stored as epoch milliseconds
    # with three digits dropped (e.g. 1786405). Restore them to epoch seconds.
    if 1e6 < value < 1e9: return int(value * 1000)
    return int(value)

def get_candles():
    """Incrementally backfill Delta candles from the newest stored timestamp."""
    now = int(time.time())
    old = pd.read_parquet(LIVE_DATA) if LIVE_DATA.exists() else pd.DataFrame()
    if len(old) and epoch_seconds(old.epoch.max()) < 1577836800:
        old = pd.DataFrame()
    if len(old):
        start = epoch_seconds(old.epoch.max()) + 60
    else:
        base = pd.read_parquet(DATA, columns=["timestamp"])
        start = int(pd.to_datetime(base.timestamp, utc=True).astype("int64").max() // 10**9) + 60
    rows=[]
    while start < now:
        end=min(now, start + 2000*60)
        r=requests.get(DELTA + "/v2/history/candles", params={"resolution":"1m",
            "symbol":SYMBOL,"start":start,"end":end}, timeout=20); r.raise_for_status()
        batch=r.json().get("result", [])
        if not batch: break
        rows.extend(batch)
        latest=max(epoch_seconds(q["time"]) for q in batch)
        if latest < start: break
        start=max(end, latest+60)
    fresh=pd.DataFrame(rows).rename(columns={"time":"epoch"})
    if len(fresh): fresh["epoch"] = fresh["epoch"].map(epoch_seconds)
    live=pd.concat([old,fresh],ignore_index=True) if len(old) or len(fresh) else pd.DataFrame()
    if len(live):
        live=live.drop_duplicates("epoch",keep="last").sort_values("epoch").reset_index(drop=True)
        live.to_parquet(LIVE_DATA,index=False,compression="zstd")
    return live

def make_features(d):
    o,h,l,c,v=[d[k].to_numpy(float) for k in ("open","high","low","close","volume")]
    r=np.maximum(h-l,1e-12); vm=pd.Series(v).rolling(30).median().to_numpy()
    vwap=(pd.Series(c*v).rolling(30).sum()/pd.Series(v).rolling(30).sum()).to_numpy()
    rm=lambda a,w:pd.Series(a).rolling(w).mean().to_numpy()
    ret=c/np.roll(c,1)-1
    ts = d.epoch if "epoch" in d.columns else pd.to_datetime(d.timestamp,utc=True).astype("int64") // 10**9
    x=np.column_stack([ret,c/np.roll(c,3)-1,c/np.roll(c,10)-1,c/np.roll(c,30)-1,
      rm(r,5)/c,rm(r,20)/c,(c-vwap)/c,v/(vm+1e-12),(c-o)/r,
      (h-np.maximum(o,c))/r,(np.minimum(o,c)-l)/r,r/c,rm(ret,5),rm(ret,20),
      pd.to_datetime(ts,unit="s",utc=True).dt.hour.to_numpy()/24])
    return x

def train():
    d=pd.read_parquet(DATA); d["epoch"]=(pd.to_datetime(d.timestamp,utc=True).astype("int64")//10**9); x=make_features(d); c=d.close.to_numpy(float); h=d.high.to_numpy(float); l=d.low.to_numpy(float)
    n=len(c); labels={}; fills={}
    for si,side in enumerate((1,-1)):
        entry=c*(1-OFFSET if side==1 else 1+OFFSET)
        fills[si]=np.r_[((l[1:]<=entry[:-1]) if side==1 else (h[1:]>=entry[:-1])),False]
        for horizon in HORIZONS:
            y=np.zeros(n,bool)
            for i in range(n-horizon-1):
                if fills[si][i]: y[i]=(np.max(h[i+1:i+horizon+1])>=entry[i]*(1+MOVE) if side==1 else np.min(l[i+1:i+horizon+1])<=entry[i]*(1-MOVE))
            labels[si,horizon]=y
    good=np.isfinite(x).all(1); train_ix=np.arange(120,n,5); train_ix=train_ix[good[train_ix]]; models={}
    for si in range(2):
        for horizon in HORIZONS:
            m=HistGradientBoostingClassifier(max_iter=120,learning_rate=.07,max_leaf_nodes=15,min_samples_leaf=80,l2_regularization=2,random_state=41+horizon+si)
            m.fit(x[train_ix],labels[si,horizon][train_ix].astype(int)); models[si,horizon]=m
    return models

def tick():
    global MODELS
    if MODELS is None:
        if (ROOT / "models.joblib").exists():
            try:
                MODELS=joblib.load(ROOT / "models.joblib")
            except Exception:
                MODELS=train()
        else:
            MODELS=train()
    live=get_candles(); hist=pd.read_parquet(DATA).tail(5000)
    hist["epoch"]=(pd.to_datetime(hist.timestamp,utc=True).astype("int64")//10**9)
    d=pd.concat([hist,live],ignore_index=True).drop_duplicates("epoch",keep="last").sort_values("epoch").reset_index(drop=True)
    i=len(d)-2; x=make_features(d); probs=np.zeros((2,3))
    for si in range(2):
        for hi,horizon in enumerate(HORIZONS): probs[si,hi]=MODELS[si,horizon].predict_proba(x[i:i+1])[0,1]
    score=3/(1/(probs+1e-6)).sum(axis=1); si=int(np.argmax(score)); side="LONG" if si==0 else "SHORT"
    row=d.iloc[i]; candle_epoch=epoch_seconds(row.epoch); event={"event":"tick","symbol":SYMBOL,"candle":{"time":datetime.fromtimestamp(candle_epoch,timezone.utc).isoformat(),"epoch":candle_epoch,"open":float(row.open),"high":float(row.high),"low":float(row.low),"close":float(row.close)},"signal":None,"probs":probs.tolist(),"score":float(score[si]),"source":"exact-hybrid-multihorizon"}
    if np.isfinite(x[i]).all(): event["signal"]={"side":side,"price":float(row.close),"score":float(score[si]),"offset":OFFSET,"target":MOVE}
    event["data_last_updated"] = event["candle"]["time"]
    event["live_cache_rows"] = int(len(live))
    if RECEIVER_URL:
        r=requests.post(RECEIVER_URL,json=event,timeout=15); r.raise_for_status()
    return {"ok":True,"sent":bool(RECEIVER_URL),"event":event}

if __name__=="__main__":
    import gradio as gr
    with gr.Blocks() as demo:
        gr.Markdown("# Exact Hybrid BTC Paper Trader")
        out=gr.JSON(); gr.Button("Run exact tick").click(tick,outputs=out,api_name="tick")
    demo.queue().launch(server_name="0.0.0.0",server_port=7860,ssr_mode=False)