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)
|