fix(hub): align Binance chart ticks and improve monitor mark price
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,8 @@ from hub_ohlcv_lib import (
|
|||||||
format_price_by_tick,
|
format_price_by_tick,
|
||||||
last_closed_bar_open_ms,
|
last_closed_bar_open_ms,
|
||||||
normalize_chart_timeframe,
|
normalize_chart_timeframe,
|
||||||
|
normalize_price_tick,
|
||||||
|
round_ohlcv_bars_to_tick,
|
||||||
window_start_ms,
|
window_start_ms,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,6 +75,72 @@ def init_db(db_path: Path | None = None) -> None:
|
|||||||
ON ohlcv_bars (exchange_key, symbol, timeframe, open_time_ms)
|
ON ohlcv_bars (exchange_key, symbol, timeframe, open_time_ms)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS ohlcv_symbol_meta (
|
||||||
|
exchange_key TEXT NOT NULL,
|
||||||
|
symbol TEXT NOT NULL,
|
||||||
|
price_tick REAL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (exchange_key, symbol)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def save_symbol_price_tick(
|
||||||
|
exchange_key: str,
|
||||||
|
symbol: str,
|
||||||
|
price_tick: float | None,
|
||||||
|
db_path: Path | None = None,
|
||||||
|
) -> None:
|
||||||
|
tick = price_tick
|
||||||
|
if tick is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
t = float(tick)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return
|
||||||
|
if t <= 0:
|
||||||
|
return
|
||||||
|
ex_k = (exchange_key or "").strip().lower()
|
||||||
|
sym = (symbol or "").strip().upper()
|
||||||
|
conn = _connect(db_path)
|
||||||
|
try:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO ohlcv_symbol_meta (exchange_key, symbol, price_tick, updated_at)
|
||||||
|
VALUES (?,?,?,?)
|
||||||
|
ON CONFLICT(exchange_key, symbol) DO UPDATE SET
|
||||||
|
price_tick=excluded.price_tick,
|
||||||
|
updated_at=excluded.updated_at
|
||||||
|
""",
|
||||||
|
(ex_k, sym, t, int(time.time())),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def load_symbol_price_tick(
|
||||||
|
exchange_key: str,
|
||||||
|
symbol: str,
|
||||||
|
db_path: Path | None = None,
|
||||||
|
) -> float | None:
|
||||||
|
ex_k = (exchange_key or "").strip().lower()
|
||||||
|
sym = (symbol or "").strip().upper()
|
||||||
|
conn = _connect(db_path)
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT price_tick FROM ohlcv_symbol_meta WHERE exchange_key=? AND symbol=?",
|
||||||
|
(ex_k, sym),
|
||||||
|
).fetchone()
|
||||||
|
if not row or row["price_tick"] is None:
|
||||||
|
return None
|
||||||
|
return float(row["price_tick"])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -273,6 +341,8 @@ def resolve_chart_bars(
|
|||||||
if remote.get("ok") and remote.get("bars"):
|
if remote.get("ok") and remote.get("bars"):
|
||||||
fetched = upsert_bars(ex_k, sym, tf, remote["bars"], db_path)
|
fetched = upsert_bars(ex_k, sym, tf, remote["bars"], db_path)
|
||||||
price_tick = remote.get("price_tick")
|
price_tick = remote.get("price_tick")
|
||||||
|
if price_tick is not None:
|
||||||
|
save_symbol_price_tick(ex_k, sym, price_tick, db_path)
|
||||||
db_rows = load_bars_range(ex_k, sym, tf, fetch_start_ms, now_ms, db_path)
|
db_rows = load_bars_range(ex_k, sym, tf, fetch_start_ms, now_ms, db_path)
|
||||||
else:
|
else:
|
||||||
remote_err = remote.get("msg") or remote.get("error") or "实例拉取 K 线失败"
|
remote_err = remote.get("msg") or remote.get("error") or "实例拉取 K 线失败"
|
||||||
@@ -282,6 +352,8 @@ def resolve_chart_bars(
|
|||||||
if len(db_rows) > need:
|
if len(db_rows) > need:
|
||||||
db_rows = db_rows[-need:]
|
db_rows = db_rows[-need:]
|
||||||
|
|
||||||
|
if price_tick is None:
|
||||||
|
price_tick = load_symbol_price_tick(ex_k, sym, db_path)
|
||||||
if price_tick is None:
|
if price_tick is None:
|
||||||
try:
|
try:
|
||||||
tick_probe = remote_fetch(
|
tick_probe = remote_fetch(
|
||||||
@@ -292,9 +364,15 @@ def resolve_chart_bars(
|
|||||||
)
|
)
|
||||||
if tick_probe.get("ok"):
|
if tick_probe.get("ok"):
|
||||||
price_tick = tick_probe.get("price_tick")
|
price_tick = tick_probe.get("price_tick")
|
||||||
|
if price_tick is not None:
|
||||||
|
save_symbol_price_tick(ex_k, sym, price_tick, db_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
price_tick = normalize_price_tick(price_tick)
|
||||||
|
if db_rows and price_tick is not None:
|
||||||
|
round_ohlcv_bars_to_tick(db_rows, price_tick)
|
||||||
|
|
||||||
candles = _to_chart_candles(db_rows)
|
candles = _to_chart_candles(db_rows)
|
||||||
if not candles:
|
if not candles:
|
||||||
return {"ok": False, "msg": remote_err or "无 K 线数据", "purged": purged}
|
return {"ok": False, "msg": remote_err or "无 K 线数据", "purged": purged}
|
||||||
|
|||||||
+73
-8
@@ -85,6 +85,75 @@ def chart_fetch_start_ms(timeframe: str, need: int, now_ms: int | None = None) -
|
|||||||
return max(0, now - max(1, int(need)) * period)
|
return max(0, now - max(1, int(need)) * period)
|
||||||
|
|
||||||
|
|
||||||
|
def _positive_float(value: Any) -> Optional[float]:
|
||||||
|
if value in (None, ""):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
v = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
return v if v > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def _price_tick_from_market_info(info: dict) -> Optional[float]:
|
||||||
|
"""从 market.info 解析 tick(含币安 PRICE_FILTER.filters)。"""
|
||||||
|
for key in ("tickSize", "tickSz", "price_increment", "order_price_round", "quote_increment"):
|
||||||
|
v = _positive_float(info.get(key))
|
||||||
|
if v is not None:
|
||||||
|
return v
|
||||||
|
|
||||||
|
for key in ("pricePrecision", "price_precision"):
|
||||||
|
raw = info.get(key)
|
||||||
|
if raw in (None, ""):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
p = float(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if p >= 1 and abs(p - round(p)) < 1e-9 and p <= 12:
|
||||||
|
return 10 ** (-int(p))
|
||||||
|
if 0 < p < 1:
|
||||||
|
return p
|
||||||
|
|
||||||
|
filters = info.get("filters")
|
||||||
|
if isinstance(filters, list):
|
||||||
|
for f in filters:
|
||||||
|
if not isinstance(f, dict):
|
||||||
|
continue
|
||||||
|
if str(f.get("filterType") or "").upper() != "PRICE_FILTER":
|
||||||
|
continue
|
||||||
|
v = _positive_float(f.get("tickSize"))
|
||||||
|
if v is not None:
|
||||||
|
return v
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def round_price_to_tick(value: Any, tick: Optional[float]) -> Optional[float]:
|
||||||
|
"""按交易所 tick 对齐价格(K 线/标记线与坐标轴一致)。"""
|
||||||
|
t = normalize_price_tick(tick)
|
||||||
|
if t is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
v = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
n = round(v / t) * t
|
||||||
|
d = _decimals_from_tick(t)
|
||||||
|
return float(f"{n:.{d}f}")
|
||||||
|
|
||||||
|
|
||||||
|
def round_ohlcv_bars_to_tick(bars: list[dict[str, Any]], tick: Optional[float]) -> None:
|
||||||
|
t = normalize_price_tick(tick)
|
||||||
|
if t is None:
|
||||||
|
return
|
||||||
|
for b in bars:
|
||||||
|
for key in ("open", "high", "low", "close"):
|
||||||
|
if key in b:
|
||||||
|
rounded = round_price_to_tick(b.get(key), t)
|
||||||
|
if rounded is not None:
|
||||||
|
b[key] = rounded
|
||||||
|
|
||||||
|
|
||||||
def price_tick_from_market(exchange, exchange_symbol: str) -> Optional[float]:
|
def price_tick_from_market(exchange, exchange_symbol: str) -> Optional[float]:
|
||||||
"""最小价格变动单位(与交易所 tick / price_to_precision 一致)。"""
|
"""最小价格变动单位(与交易所 tick / price_to_precision 一致)。"""
|
||||||
try:
|
try:
|
||||||
@@ -96,14 +165,9 @@ def price_tick_from_market(exchange, exchange_symbol: str) -> Optional[float]:
|
|||||||
|
|
||||||
info = market.get("info") or {}
|
info = market.get("info") or {}
|
||||||
if isinstance(info, dict):
|
if isinstance(info, dict):
|
||||||
for key in ("tickSize", "tickSz", "price_increment", "order_price_round", "quote_increment"):
|
tick = _price_tick_from_market_info(info)
|
||||||
if info.get(key) not in (None, ""):
|
if tick is not None:
|
||||||
try:
|
return tick
|
||||||
v = float(info[key])
|
|
||||||
if v > 0:
|
|
||||||
return v
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
limits = market.get("limits") or {}
|
limits = market.get("limits") or {}
|
||||||
price_limits = limits.get("price") or {}
|
price_limits = limits.get("price") or {}
|
||||||
@@ -434,6 +498,7 @@ def fetch_ohlcv_for_hub(
|
|||||||
return {"ok": False, "msg": "交易所未返回 K 线"}
|
return {"ok": False, "msg": "交易所未返回 K 线"}
|
||||||
|
|
||||||
tick = normalize_price_tick(price_tick_from_market(exchange, ex_sym))
|
tick = normalize_price_tick(price_tick_from_market(exchange, ex_sym))
|
||||||
|
round_ohlcv_bars_to_tick(merged, tick)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
|
|||||||
@@ -416,9 +416,43 @@ def _position_mark_price(p: dict[str, Any]) -> float | None:
|
|||||||
for key in (
|
for key in (
|
||||||
p.get("markPrice"),
|
p.get("markPrice"),
|
||||||
p.get("mark_price"),
|
p.get("mark_price"),
|
||||||
|
p.get("mark"),
|
||||||
info.get("markPx"),
|
info.get("markPx"),
|
||||||
info.get("mark_price"),
|
info.get("mark_price"),
|
||||||
info.get("markPrice"),
|
info.get("markPrice"),
|
||||||
|
info.get("last"),
|
||||||
|
info.get("lastPrice"),
|
||||||
|
):
|
||||||
|
px = _finite_or_none(key)
|
||||||
|
if px is not None and px > 0:
|
||||||
|
return px
|
||||||
|
contracts = _position_contracts(p)
|
||||||
|
if abs(contracts) >= 1e-12:
|
||||||
|
notional = _finite_or_none(p.get("notional"))
|
||||||
|
if notional is not None and abs(notional) > 0:
|
||||||
|
return abs(notional) / abs(contracts)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _ticker_mark_price(ex: Any, symbol: str) -> float | None:
|
||||||
|
"""持仓行无 mark 时,用 ticker 补标记价(last/mark)。"""
|
||||||
|
sym = (symbol or "").strip()
|
||||||
|
if not sym:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
t = ex.fetch_ticker(sym)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if not isinstance(t, dict):
|
||||||
|
return None
|
||||||
|
info = t.get("info") if isinstance(t.get("info"), dict) else {}
|
||||||
|
for key in (
|
||||||
|
t.get("mark"),
|
||||||
|
t.get("last"),
|
||||||
|
t.get("close"),
|
||||||
|
info.get("markPrice"),
|
||||||
|
info.get("mark_price"),
|
||||||
|
info.get("markPx"),
|
||||||
):
|
):
|
||||||
px = _finite_or_none(key)
|
px = _finite_or_none(key)
|
||||||
if px is not None and px > 0:
|
if px is not None and px > 0:
|
||||||
@@ -602,6 +636,8 @@ def _status_inner(x_control_token: str | None) -> Any:
|
|||||||
entry_f = _position_entry_price(p)
|
entry_f = _position_entry_price(p)
|
||||||
_, entry_fmt, price_tick = _position_price_fmt(ex, sym, entry_f)
|
_, entry_fmt, price_tick = _position_price_fmt(ex, sym, entry_f)
|
||||||
mark_f = _position_mark_price(p)
|
mark_f = _position_mark_price(p)
|
||||||
|
if mark_f is None and sym:
|
||||||
|
mark_f = _ticker_mark_price(ex, sym)
|
||||||
_, mark_fmt, mark_tick = _position_price_fmt(ex, sym, mark_f)
|
_, mark_fmt, mark_tick = _position_price_fmt(ex, sym, mark_f)
|
||||||
if price_tick is None and mark_tick is not None:
|
if price_tick is None and mark_tick is not None:
|
||||||
price_tick = mark_tick
|
price_tick = mark_tick
|
||||||
|
|||||||
@@ -949,9 +949,11 @@
|
|||||||
}
|
}
|
||||||
specs.forEach(function (s) {
|
specs.forEach(function (s) {
|
||||||
if (s.price == null || !Number.isFinite(Number(s.price))) return;
|
if (s.price == null || !Number.isFinite(Number(s.price))) return;
|
||||||
|
const px = roundToTick(s.price);
|
||||||
|
if (px == null || !Number.isFinite(Number(px))) return;
|
||||||
positionLines.push(
|
positionLines.push(
|
||||||
candleSeries.createPriceLine({
|
candleSeries.createPriceLine({
|
||||||
price: Number(s.price),
|
price: Number(px),
|
||||||
color: s.color,
|
color: s.color,
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
lineStyle: 2,
|
lineStyle: 2,
|
||||||
@@ -1009,30 +1011,52 @@
|
|||||||
return Math.max(0, Math.min(12, Math.round(-Math.log10(minMove))));
|
return Math.max(0, Math.min(12, Math.round(-Math.log10(minMove))));
|
||||||
}
|
}
|
||||||
|
|
||||||
const SAFE_PRICE_FORMAT = { type: "price", precision: 4, minMove: 0.0001, base: 10000 };
|
const SAFE_PRICE_FORMAT = { type: "price", precision: 4, minMove: 0.0001 };
|
||||||
|
|
||||||
function tickToPriceFormat(tick) {
|
function tickToPriceFormat(tick) {
|
||||||
try {
|
try {
|
||||||
if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) {
|
if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) {
|
||||||
return { type: "price", precision: 2, minMove: 0.01, base: 100 };
|
return { type: "price", precision: 2, minMove: 0.01 };
|
||||||
}
|
}
|
||||||
const raw = Number(tick);
|
const minMove = Number(tick);
|
||||||
if (raw >= 1) {
|
let prec = decimalsFromTick(minMove);
|
||||||
return { type: "price", precision: 0, minMove: 1 };
|
|
||||||
}
|
|
||||||
let prec = decimalsFromTick(raw);
|
|
||||||
if (prec == null || prec < 0) prec = 4;
|
if (prec == null || prec < 0) prec = 4;
|
||||||
prec = Math.min(8, Math.max(0, Math.floor(prec)));
|
prec = Math.min(12, Math.max(0, Math.floor(prec)));
|
||||||
const base = Math.pow(10, prec);
|
return { type: "price", precision: prec, minMove: minMove };
|
||||||
if (!Number.isFinite(base) || base < 1 || base > 1e12) {
|
|
||||||
return SAFE_PRICE_FORMAT;
|
|
||||||
}
|
|
||||||
return { type: "price", precision: prec, minMove: 1 / base, base: base };
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return SAFE_PRICE_FORMAT;
|
return SAFE_PRICE_FORMAT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function roundToTick(v) {
|
||||||
|
if (v == null || Number.isNaN(Number(v))) return v;
|
||||||
|
const n = Number(v);
|
||||||
|
const tick = priceTick;
|
||||||
|
if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) return n;
|
||||||
|
const t = Number(tick);
|
||||||
|
const rounded = Math.round(n / t) * t;
|
||||||
|
const dec = decimalsFromTick(t);
|
||||||
|
if (dec == null) return rounded;
|
||||||
|
return parseFloat(rounded.toFixed(dec));
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignCandlesToTick(candles) {
|
||||||
|
if (!Array.isArray(candles) || !candles.length) return candles || [];
|
||||||
|
if (priceTick == null || !Number.isFinite(Number(priceTick)) || Number(priceTick) <= 0) {
|
||||||
|
return candles;
|
||||||
|
}
|
||||||
|
return candles.map(function (c) {
|
||||||
|
return {
|
||||||
|
time: c.time,
|
||||||
|
open: roundToTick(c.open),
|
||||||
|
high: roundToTick(c.high),
|
||||||
|
low: roundToTick(c.low),
|
||||||
|
close: roundToTick(c.close),
|
||||||
|
volume: c.volume,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function applyPriceFormatToSeries(series, pf) {
|
function applyPriceFormatToSeries(series, pf) {
|
||||||
if (!series || !series.applyOptions) return;
|
if (!series || !series.applyOptions) return;
|
||||||
try {
|
try {
|
||||||
@@ -1065,7 +1089,8 @@
|
|||||||
|
|
||||||
function fmtPrice(v) {
|
function fmtPrice(v) {
|
||||||
if (v == null || Number.isNaN(Number(v))) return "-";
|
if (v == null || Number.isNaN(Number(v))) return "-";
|
||||||
const n = Number(v);
|
const aligned = roundToTick(v);
|
||||||
|
const n = Number(aligned);
|
||||||
if (n === 0) return "0";
|
if (n === 0) return "0";
|
||||||
const dec = decimalsFromTick(priceTick);
|
const dec = decimalsFromTick(priceTick);
|
||||||
if (dec != null) return n.toFixed(dec);
|
if (dec != null) return n.toFixed(dec);
|
||||||
@@ -1172,7 +1197,7 @@
|
|||||||
if (!bar || bar.close == null) return;
|
if (!bar || bar.close == null) return;
|
||||||
const up = Number(bar.close) >= Number(bar.open);
|
const up = Number(bar.close) >= Number(bar.open);
|
||||||
currentPriceLine = candleSeries.createPriceLine({
|
currentPriceLine = candleSeries.createPriceLine({
|
||||||
price: Number(bar.close),
|
price: Number(roundToTick(bar.close)),
|
||||||
color: up ? "#00ff9d" : "#ff4d6d",
|
color: up ? "#00ff9d" : "#ff4d6d",
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
lineStyle: 2,
|
lineStyle: 2,
|
||||||
@@ -1416,7 +1441,7 @@
|
|||||||
|
|
||||||
rangeMarkers.push(
|
rangeMarkers.push(
|
||||||
candleSeries.createPriceLine({
|
candleSeries.createPriceLine({
|
||||||
price: Number(hi.high),
|
price: Number(roundToTick(hi.high)),
|
||||||
color: "#ffb84d",
|
color: "#ffb84d",
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
lineStyle: 2,
|
lineStyle: 2,
|
||||||
@@ -1426,7 +1451,7 @@
|
|||||||
);
|
);
|
||||||
rangeMarkers.push(
|
rangeMarkers.push(
|
||||||
candleSeries.createPriceLine({
|
candleSeries.createPriceLine({
|
||||||
price: Number(lo.low),
|
price: Number(roundToTick(lo.low)),
|
||||||
color: "#4cd97f",
|
color: "#4cd97f",
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
lineStyle: 2,
|
lineStyle: 2,
|
||||||
@@ -1535,7 +1560,7 @@
|
|||||||
priceTick = null;
|
priceTick = null;
|
||||||
applyChartPriceFormat();
|
applyChartPriceFormat();
|
||||||
}
|
}
|
||||||
lastCandles = data.candles;
|
lastCandles = alignCandlesToTick(data.candles);
|
||||||
indexCandles(lastCandles);
|
indexCandles(lastCandles);
|
||||||
candleSeries.setData(lastCandles);
|
candleSeries.setData(lastCandles);
|
||||||
volumeSeries.setData(buildVolumeData(lastCandles));
|
volumeSeries.setData(buildVolumeData(lastCandles));
|
||||||
|
|||||||
@@ -244,7 +244,7 @@
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart.js?v=20260528-hub-no-30m"></script>
|
<script src="/assets/chart.js?v=20260603-hub-binance-tick"></script>
|
||||||
<script src="/assets/app.js?v=20260603-hub-mark-price"></script>
|
<script src="/assets/app.js?v=20260604-hub-mark-price2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pathlib import Path
|
|||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(ROOT / "manual_trading_hub"))
|
sys.path.insert(0, str(ROOT / "manual_trading_hub"))
|
||||||
|
|
||||||
from agent import _position_mark_price # noqa: E402
|
from agent import _position_mark_price, _ticker_mark_price # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
class TestHubAgentMarkPrice(unittest.TestCase):
|
class TestHubAgentMarkPrice(unittest.TestCase):
|
||||||
@@ -27,6 +27,18 @@ class TestHubAgentMarkPrice(unittest.TestCase):
|
|||||||
def test_missing_returns_none(self):
|
def test_missing_returns_none(self):
|
||||||
self.assertIsNone(_position_mark_price({"info": {}}))
|
self.assertIsNone(_position_mark_price({"info": {}}))
|
||||||
|
|
||||||
|
def test_infer_from_notional_and_contracts(self):
|
||||||
|
p = {"notional": 1000, "contracts": 10, "info": {}}
|
||||||
|
px = _position_mark_price(p)
|
||||||
|
self.assertAlmostEqual(px, 100.0)
|
||||||
|
|
||||||
|
def test_ticker_fallback(self):
|
||||||
|
class _Ex:
|
||||||
|
def fetch_ticker(self, sym):
|
||||||
|
return {"mark": 99.5, "info": {}}
|
||||||
|
|
||||||
|
self.assertAlmostEqual(_ticker_mark_price(_Ex(), "BTC/USDT:USDT"), 99.5)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -54,6 +54,35 @@ class TestHubOhlcvLib(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertAlmostEqual(tick, 0.01)
|
self.assertAlmostEqual(tick, 0.01)
|
||||||
|
|
||||||
|
def test_price_tick_from_binance_price_filter(self):
|
||||||
|
class _Ex:
|
||||||
|
markets = {
|
||||||
|
"BTC/USDT:USDT": {
|
||||||
|
"precision": {"price": 2},
|
||||||
|
"info": {
|
||||||
|
"filters": [
|
||||||
|
{"filterType": "PRICE_FILTER", "tickSize": "0.10"},
|
||||||
|
{"filterType": "LOT_SIZE", "stepSize": "0.001"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"limits": {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_markets(self):
|
||||||
|
return self.markets
|
||||||
|
|
||||||
|
def market(self, sym):
|
||||||
|
return self.markets[sym]
|
||||||
|
|
||||||
|
def price_to_precision(self, sym, price):
|
||||||
|
return "12345.6"
|
||||||
|
|
||||||
|
from hub_ohlcv_lib import price_tick_from_market
|
||||||
|
|
||||||
|
tick = price_tick_from_market(_Ex(), "BTC/USDT:USDT")
|
||||||
|
self.assertAlmostEqual(tick, 0.10)
|
||||||
|
|
||||||
def test_price_tick_from_info_tick_size(self):
|
def test_price_tick_from_info_tick_size(self):
|
||||||
class _Ex:
|
class _Ex:
|
||||||
markets = {
|
markets = {
|
||||||
|
|||||||
Reference in New Issue
Block a user