Upload 6 files
Browse files- BTCUSD_1m.parquet +3 -0
- README.md +2 -1
- app.py +90 -49
- models.joblib +3 -0
- requirements.txt +3 -0
BTCUSD_1m.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e3348c6d825a3478a2396b036f8140ec92c99565a0b7397ca6e1dc64ef425f2c
|
| 3 |
+
size 12516035
|
README.md
CHANGED
|
@@ -9,4 +9,5 @@ Set these Space variables:
|
|
| 9 |
- `RECEIVER_URL=https://YOUR-NETLIFY-SITE.netlify.app/api/signal`
|
| 10 |
The Space exposes the Gradio API function `tick`; Netlify's scheduled
|
| 11 |
function calls it once per minute. It uses Delta public candles and sends only
|
| 12 |
-
paper signal events.
|
|
|
|
|
|
| 9 |
- `RECEIVER_URL=https://YOUR-NETLIFY-SITE.netlify.app/api/signal`
|
| 10 |
The Space exposes the Gradio API function `tick`; Netlify's scheduled
|
| 11 |
function calls it once per minute. It uses Delta public candles and sends only
|
| 12 |
+
paper signal events. `BTCUSD_1m.parquet` is the historical training dataset
|
| 13 |
+
used by the exact multi-horizon model.
|
app.py
CHANGED
|
@@ -1,58 +1,99 @@
|
|
| 1 |
-
import os, time
|
| 2 |
from datetime import datetime, timezone
|
| 3 |
-
|
| 4 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
DELTA = os.getenv("DELTA_URL", "https://api.india.delta.exchange")
|
| 6 |
SYMBOL = os.getenv("SYMBOL", "BTCUSD")
|
| 7 |
-
RECEIVER_URL = os.
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
def
|
|
|
|
| 11 |
now = int(time.time())
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
def
|
| 20 |
-
d
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
ret
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
side = "LONG" if fast > slow else "SHORT"
|
| 32 |
-
price = float(c.iloc[i])
|
| 33 |
-
return {"event":"signal", "symbol":SYMBOL, "side":side,
|
| 34 |
-
"signal_time":datetime.fromtimestamp(int(d.time.iloc[i]),timezone.utc).isoformat(),
|
| 35 |
-
"price":price, "score":edge, "source":"huggingface-paper-space"}
|
| 36 |
|
| 37 |
-
def
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
if
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
if __name__
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, time
|
| 2 |
from datetime import datetime, timezone
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import requests, pandas as pd, numpy as np
|
| 5 |
+
import joblib
|
| 6 |
+
from sklearn.ensemble import HistGradientBoostingClassifier
|
| 7 |
+
|
| 8 |
+
ROOT = Path(__file__).parent
|
| 9 |
+
DATA = ROOT / "BTCUSD_1m.parquet"
|
| 10 |
+
LIVE_DATA = ROOT / "live_candles.parquet"
|
| 11 |
DELTA = os.getenv("DELTA_URL", "https://api.india.delta.exchange")
|
| 12 |
SYMBOL = os.getenv("SYMBOL", "BTCUSD")
|
| 13 |
+
RECEIVER_URL = os.getenv("RECEIVER_URL", "").rstrip("/")
|
| 14 |
+
MOVE, OFFSET, HORIZONS = .0008, .0004, (5, 15, 30)
|
| 15 |
+
MODELS = None
|
| 16 |
|
| 17 |
+
def get_candles():
|
| 18 |
+
"""Incrementally backfill Delta candles from the newest stored timestamp."""
|
| 19 |
now = int(time.time())
|
| 20 |
+
old = pd.read_parquet(LIVE_DATA) if LIVE_DATA.exists() else pd.DataFrame()
|
| 21 |
+
if len(old):
|
| 22 |
+
start = int(old.epoch.max()) + 60
|
| 23 |
+
else:
|
| 24 |
+
base = pd.read_parquet(DATA, columns=["timestamp"])
|
| 25 |
+
start = int(pd.to_datetime(base.timestamp, utc=True).astype("int64").max() // 10**9) + 60
|
| 26 |
+
rows=[]
|
| 27 |
+
while start < now:
|
| 28 |
+
end=min(now, start + 2000*60)
|
| 29 |
+
r=requests.get(DELTA + "/v2/history/candles", params={"resolution":"1m",
|
| 30 |
+
"symbol":SYMBOL,"start":start,"end":end}, timeout=20); r.raise_for_status()
|
| 31 |
+
batch=r.json().get("result", [])
|
| 32 |
+
if not batch: break
|
| 33 |
+
rows.extend(batch)
|
| 34 |
+
latest=max(int(q["time"]) for q in batch)
|
| 35 |
+
if latest < start: break
|
| 36 |
+
start=max(end, latest+60)
|
| 37 |
+
fresh=pd.DataFrame(rows).rename(columns={"time":"epoch"})
|
| 38 |
+
live=pd.concat([old,fresh],ignore_index=True) if len(old) or len(fresh) else pd.DataFrame()
|
| 39 |
+
if len(live):
|
| 40 |
+
live=live.drop_duplicates("epoch",keep="last").sort_values("epoch").reset_index(drop=True)
|
| 41 |
+
live.to_parquet(LIVE_DATA,index=False,compression="zstd")
|
| 42 |
+
return live
|
| 43 |
|
| 44 |
+
def make_features(d):
|
| 45 |
+
o,h,l,c,v=[d[k].to_numpy(float) for k in ("open","high","low","close","volume")]
|
| 46 |
+
r=np.maximum(h-l,1e-12); vm=pd.Series(v).rolling(30).median().to_numpy()
|
| 47 |
+
vwap=(pd.Series(c*v).rolling(30).sum()/pd.Series(v).rolling(30).sum()).to_numpy()
|
| 48 |
+
rm=lambda a,w:pd.Series(a).rolling(w).mean().to_numpy()
|
| 49 |
+
ret=c/np.roll(c,1)-1
|
| 50 |
+
ts = d.epoch if "epoch" in d.columns else pd.to_datetime(d.timestamp,utc=True).astype("int64") // 10**9
|
| 51 |
+
x=np.column_stack([ret,c/np.roll(c,3)-1,c/np.roll(c,10)-1,c/np.roll(c,30)-1,
|
| 52 |
+
rm(r,5)/c,rm(r,20)/c,(c-vwap)/c,v/(vm+1e-12),(c-o)/r,
|
| 53 |
+
(h-np.maximum(o,c))/r,(np.minimum(o,c)-l)/r,r/c,rm(ret,5),rm(ret,20),
|
| 54 |
+
pd.to_datetime(ts,unit="s",utc=True).dt.hour.to_numpy()/24])
|
| 55 |
+
return x
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
def train():
|
| 58 |
+
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)
|
| 59 |
+
n=len(c); labels={}; fills={}
|
| 60 |
+
for si,side in enumerate((1,-1)):
|
| 61 |
+
entry=c*(1-OFFSET if side==1 else 1+OFFSET)
|
| 62 |
+
fills[si]=np.r_[((l[1:]<=entry[:-1]) if side==1 else (h[1:]>=entry[:-1])),False]
|
| 63 |
+
for horizon in HORIZONS:
|
| 64 |
+
y=np.zeros(n,bool)
|
| 65 |
+
for i in range(n-horizon-1):
|
| 66 |
+
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))
|
| 67 |
+
labels[si,horizon]=y
|
| 68 |
+
good=np.isfinite(x).all(1); train_ix=np.arange(120,n,5); train_ix=train_ix[good[train_ix]]; models={}
|
| 69 |
+
for si in range(2):
|
| 70 |
+
for horizon in HORIZONS:
|
| 71 |
+
m=HistGradientBoostingClassifier(max_iter=120,learning_rate=.07,max_leaf_nodes=15,min_samples_leaf=80,l2_regularization=2,random_state=41+horizon+si)
|
| 72 |
+
m.fit(x[train_ix],labels[si,horizon][train_ix].astype(int)); models[si,horizon]=m
|
| 73 |
+
return models
|
| 74 |
|
| 75 |
+
def tick():
|
| 76 |
+
global MODELS
|
| 77 |
+
if MODELS is None:
|
| 78 |
+
MODELS=joblib.load(ROOT / "models.joblib") if (ROOT / "models.joblib").exists() else train()
|
| 79 |
+
live=get_candles(); hist=pd.read_parquet(DATA).tail(5000)
|
| 80 |
+
hist["epoch"]=(pd.to_datetime(hist.timestamp,utc=True).astype("int64")//10**9)
|
| 81 |
+
d=pd.concat([hist,live],ignore_index=True).drop_duplicates("epoch",keep="last").sort_values("epoch").reset_index(drop=True)
|
| 82 |
+
i=len(d)-2; x=make_features(d); probs=np.zeros((2,3))
|
| 83 |
+
for si in range(2):
|
| 84 |
+
for hi,horizon in enumerate(HORIZONS): probs[si,hi]=MODELS[si,horizon].predict_proba(x[i:i+1])[0,1]
|
| 85 |
+
score=3/(1/(probs+1e-6)).sum(axis=1); si=int(np.argmax(score)); side="LONG" if si==0 else "SHORT"
|
| 86 |
+
row=d.iloc[i]; event={"event":"tick","symbol":SYMBOL,"candle":{"time":datetime.fromtimestamp(int(row.epoch),timezone.utc).isoformat(),"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"}
|
| 87 |
+
if np.isfinite(x[i]).all(): event["signal"]={"side":side,"price":float(row.close),"score":float(score[si]),"offset":OFFSET,"target":MOVE}
|
| 88 |
+
event["data_last_updated"] = event["candle"]["time"]
|
| 89 |
+
event["live_cache_rows"] = int(len(live))
|
| 90 |
+
if RECEIVER_URL:
|
| 91 |
+
r=requests.post(RECEIVER_URL,json=event,timeout=15); r.raise_for_status()
|
| 92 |
+
return {"ok":True,"sent":bool(RECEIVER_URL),"event":event}
|
| 93 |
|
| 94 |
+
if __name__=="__main__":
|
| 95 |
+
import gradio as gr
|
| 96 |
+
with gr.Blocks() as demo:
|
| 97 |
+
gr.Markdown("# Exact Hybrid BTC Paper Trader")
|
| 98 |
+
out=gr.JSON(); gr.Button("Run exact tick").click(tick,outputs=out,api_name="tick")
|
| 99 |
+
demo.queue().launch(server_name="0.0.0.0",server_port=7860,ssr_mode=False)
|
models.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8dfc89e4a117846d9b7a6202d0ca59b980541979522123a7c8529eca9d7cf7ee
|
| 3 |
+
size 590138
|
requirements.txt
CHANGED
|
@@ -1,3 +1,6 @@
|
|
| 1 |
requests
|
| 2 |
pandas
|
|
|
|
|
|
|
|
|
|
| 3 |
gradio==6.23.1
|
|
|
|
| 1 |
requests
|
| 2 |
pandas
|
| 3 |
+
pyarrow
|
| 4 |
+
scikit-learn
|
| 5 |
+
joblib
|
| 6 |
gradio==6.23.1
|