feat: add selectable leverage threshold on ops map (90-300 step 10)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,6 +38,17 @@ def _validate_min_hours(min_hours: float) -> float:
|
|||||||
return float(int(round(h)))
|
return float(int(round(h)))
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_min_leverage(min_leverage: float) -> float:
|
||||||
|
# 90–300,步进 10
|
||||||
|
v = float(min_leverage)
|
||||||
|
if v < 90 or v > 300 or abs(v - round(v)) > 1e-9 or int(round(v)) % 10 != 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="min_leverage must be multiple of 10 in [90, 300]",
|
||||||
|
)
|
||||||
|
return float(int(round(v)))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/leverage")
|
@router.get("/leverage")
|
||||||
def leverage_stats(
|
def leverage_stats(
|
||||||
range: str = Query(default="day", pattern="^(day|week|month)$"),
|
range: str = Query(default="day", pattern="^(day|week|month)$"),
|
||||||
@@ -50,9 +61,16 @@ def leverage_stats(
|
|||||||
le=48,
|
le=48,
|
||||||
description="采样时距离到期 ≥ 该小时数才计入",
|
description="采样时距离到期 ≥ 该小时数才计入",
|
||||||
),
|
),
|
||||||
|
min_leverage: float = Query(
|
||||||
|
default=100,
|
||||||
|
ge=90,
|
||||||
|
le=300,
|
||||||
|
description="杠杆达标线(≥ 该值计入达标占比)",
|
||||||
|
),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
mh = _validate_min_hours(min_hours)
|
mh = _validate_min_hours(min_hours)
|
||||||
|
ml = _validate_min_leverage(min_leverage)
|
||||||
info = _range_info(range, date)
|
info = _range_info(range, date)
|
||||||
repo = Repository(s.db_path)
|
repo = Repository(s.db_path)
|
||||||
try:
|
try:
|
||||||
@@ -67,7 +85,7 @@ def leverage_stats(
|
|||||||
rows,
|
rows,
|
||||||
range_info=info,
|
range_info=info,
|
||||||
bucket_minutes=bucket_minutes,
|
bucket_minutes=bucket_minutes,
|
||||||
min_leverage=float(s.min_option_leverage),
|
min_leverage=ml,
|
||||||
side=side,
|
side=side,
|
||||||
)
|
)
|
||||||
payload["min_hours"] = mh
|
payload["min_hours"] = mh
|
||||||
@@ -135,10 +153,12 @@ def ops_map(
|
|||||||
side: str = Query(default="both", pattern="^(C|P|both)$"),
|
side: str = Query(default="both", pattern="^(C|P|both)$"),
|
||||||
bucket_minutes: int = Query(default=60, ge=15, le=120),
|
bucket_minutes: int = Query(default=60, ge=15, le=120),
|
||||||
min_hours: float = Query(default=12, ge=8, le=48),
|
min_hours: float = Query(default=12, ge=8, le=48),
|
||||||
|
min_leverage: float = Query(default=100, ge=90, le=300),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
# 直接算一遍,避免路由函数互相调用时 Query 默认值问题
|
# 直接算一遍,避免路由函数互相调用时 Query 默认值问题
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
mh = _validate_min_hours(min_hours)
|
mh = _validate_min_hours(min_hours)
|
||||||
|
ml = _validate_min_leverage(min_leverage)
|
||||||
info = _range_info(range, date)
|
info = _range_info(range, date)
|
||||||
repo = Repository(s.db_path)
|
repo = Repository(s.db_path)
|
||||||
try:
|
try:
|
||||||
@@ -153,7 +173,7 @@ def ops_map(
|
|||||||
rows,
|
rows,
|
||||||
range_info=info,
|
range_info=info,
|
||||||
bucket_minutes=bucket_minutes,
|
bucket_minutes=bucket_minutes,
|
||||||
min_leverage=float(s.min_option_leverage),
|
min_leverage=ml,
|
||||||
side=side,
|
side=side,
|
||||||
)
|
)
|
||||||
lev["min_hours"] = mh
|
lev["min_hours"] = mh
|
||||||
@@ -185,6 +205,7 @@ def ops_map(
|
|||||||
"side": side,
|
"side": side,
|
||||||
"bucket_minutes": bucket_minutes,
|
"bucket_minutes": bucket_minutes,
|
||||||
"min_hours": mh,
|
"min_hours": mh,
|
||||||
|
"min_leverage": ml,
|
||||||
"expiry_filter": filt,
|
"expiry_filter": filt,
|
||||||
"leverage": lev,
|
"leverage": lev,
|
||||||
"move_points": mov,
|
"move_points": mov,
|
||||||
|
|||||||
Vendored
+19
-1
@@ -126,6 +126,9 @@
|
|||||||
<label class="date-field">距离到期 ≥
|
<label class="date-field">距离到期 ≥
|
||||||
<select id="minHours" aria-label="距离到期最小小时数"></select>
|
<select id="minHours" aria-label="距离到期最小小时数"></select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="date-field">达标线 ≥
|
||||||
|
<select id="minLeverage" aria-label="杠杆达标线"></select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="tile"><div class="label">范围</div><div class="value" id="opsRange" style="font-size:1.05rem">—</div><div class="hint" id="opsSamples">—</div></div>
|
<div class="tile"><div class="label">范围</div><div class="value" id="opsRange" style="font-size:1.05rem">—</div><div class="hint" id="opsSamples">—</div></div>
|
||||||
@@ -214,7 +217,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const TOKEN_KEY = "mi_token";
|
const TOKEN_KEY = "mi_token";
|
||||||
const state = { page: "dash", range: "day", side: "both", moveMode: "abs", minHours: 12, lastMov: null, authRequired: false, loadedUsername: "admin" };
|
const state = { page: "dash", range: "day", side: "both", moveMode: "abs", minHours: 12, minLeverage: 100, lastMov: null, authRequired: false, loadedUsername: "admin" };
|
||||||
function getToken() { try { return localStorage.getItem(TOKEN_KEY); } catch { return null; } }
|
function getToken() { try { return localStorage.getItem(TOKEN_KEY); } catch { return null; } }
|
||||||
function setToken(t) { try { if (t) localStorage.setItem(TOKEN_KEY, t); else localStorage.removeItem(TOKEN_KEY); } catch {} }
|
function setToken(t) { try { if (t) localStorage.setItem(TOKEN_KEY, t); else localStorage.removeItem(TOKEN_KEY); } catch {} }
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
@@ -246,6 +249,16 @@
|
|||||||
sel.appendChild(opt);
|
sel.appendChild(opt);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
(function initMinLeverage() {
|
||||||
|
const sel = document.getElementById("minLeverage");
|
||||||
|
for (let v = 90; v <= 300; v += 10) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = String(v);
|
||||||
|
opt.textContent = String(v);
|
||||||
|
if (v === state.minLeverage) opt.selected = true;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
function showLogin() {
|
function showLogin() {
|
||||||
document.getElementById("page-login").classList.remove("hidden");
|
document.getElementById("page-login").classList.remove("hidden");
|
||||||
@@ -557,6 +570,10 @@
|
|||||||
state.minHours = Number(document.getElementById("minHours").value) || 12;
|
state.minHours = Number(document.getElementById("minHours").value) || 12;
|
||||||
loadOps();
|
loadOps();
|
||||||
});
|
});
|
||||||
|
document.getElementById("minLeverage").addEventListener("change", () => {
|
||||||
|
state.minLeverage = Number(document.getElementById("minLeverage").value) || 100;
|
||||||
|
loadOps();
|
||||||
|
});
|
||||||
|
|
||||||
function drawChart(buckets, minLev) {
|
function drawChart(buckets, minLev) {
|
||||||
const svg = document.getElementById("levChart");
|
const svg = document.getElementById("levChart");
|
||||||
@@ -704,6 +721,7 @@
|
|||||||
bucket_minutes: "60",
|
bucket_minutes: "60",
|
||||||
date,
|
date,
|
||||||
min_hours: String(state.minHours || 12),
|
min_hours: String(state.minHours || 12),
|
||||||
|
min_leverage: String(state.minLeverage || 100),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const d = await apiFetch("/api/stats/ops-map?" + q).then(r => {
|
const d = await apiFetch("/api/stats/ops-map?" + q).then(r => {
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export async function fetchOpsMap(params: {
|
|||||||
side?: string;
|
side?: string;
|
||||||
bucket_minutes?: number;
|
bucket_minutes?: number;
|
||||||
min_hours?: number;
|
min_hours?: number;
|
||||||
|
min_leverage?: number;
|
||||||
}): Promise<OpsMap> {
|
}): Promise<OpsMap> {
|
||||||
const q = new URLSearchParams();
|
const q = new URLSearchParams();
|
||||||
q.set("range", params.range);
|
q.set("range", params.range);
|
||||||
@@ -271,6 +272,7 @@ export async function fetchOpsMap(params: {
|
|||||||
if (params.side) q.set("side", params.side);
|
if (params.side) q.set("side", params.side);
|
||||||
if (params.bucket_minutes) q.set("bucket_minutes", String(params.bucket_minutes));
|
if (params.bucket_minutes) q.set("bucket_minutes", String(params.bucket_minutes));
|
||||||
if (params.min_hours != null) q.set("min_hours", String(params.min_hours));
|
if (params.min_hours != null) q.set("min_hours", String(params.min_hours));
|
||||||
|
if (params.min_leverage != null) q.set("min_leverage", String(params.min_leverage));
|
||||||
const r = await apiFetch(`/api/stats/ops-map?${q}`);
|
const r = await apiFetch(`/api/stats/ops-map?${q}`);
|
||||||
if (!r.ok) throw new Error(r.status === 401 ? "unauthorized" : "ops-map failed");
|
if (!r.ok) throw new Error(r.status === 401 ? "unauthorized" : "ops-map failed");
|
||||||
return r.json();
|
return r.json();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type RangeKey = "day" | "week" | "month";
|
|||||||
type SideKey = "both" | "C" | "P";
|
type SideKey = "both" | "C" | "P";
|
||||||
|
|
||||||
const MIN_HOURS_OPTIONS = Array.from({ length: (48 - 8) / 2 + 1 }, (_, i) => 8 + i * 2);
|
const MIN_HOURS_OPTIONS = Array.from({ length: (48 - 8) / 2 + 1 }, (_, i) => 8 + i * 2);
|
||||||
|
const MIN_LEVERAGE_OPTIONS = Array.from({ length: (300 - 90) / 10 + 1 }, (_, i) => 90 + i * 10);
|
||||||
|
|
||||||
function todayYmd() {
|
function todayYmd() {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
@@ -32,6 +33,7 @@ export default function OpsMapPage() {
|
|||||||
const [side, setSide] = useState<SideKey>("both");
|
const [side, setSide] = useState<SideKey>("both");
|
||||||
const [date, setDate] = useState(todayYmd());
|
const [date, setDate] = useState(todayYmd());
|
||||||
const [minHours, setMinHours] = useState(12);
|
const [minHours, setMinHours] = useState(12);
|
||||||
|
const [minLeverage, setMinLeverage] = useState(100);
|
||||||
const [moveMode, setMoveMode] = useState<"abs" | "signed">("abs");
|
const [moveMode, setMoveMode] = useState<"abs" | "signed">("abs");
|
||||||
const [data, setData] = useState<OpsMap | null>(null);
|
const [data, setData] = useState<OpsMap | null>(null);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
@@ -55,7 +57,14 @@ export default function OpsMapPage() {
|
|||||||
if (!ready || needLogin) return;
|
if (!ready || needLogin) return;
|
||||||
let alive = true;
|
let alive = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchOpsMap({ range, date, side, bucket_minutes: 60, min_hours: minHours })
|
fetchOpsMap({
|
||||||
|
range,
|
||||||
|
date,
|
||||||
|
side,
|
||||||
|
bucket_minutes: 60,
|
||||||
|
min_hours: minHours,
|
||||||
|
min_leverage: minLeverage,
|
||||||
|
})
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
if (!alive) return;
|
if (!alive) return;
|
||||||
setData(d);
|
setData(d);
|
||||||
@@ -73,7 +82,7 @@ export default function OpsMapPage() {
|
|||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
}, [range, side, date, minHours, ready, needLogin]);
|
}, [range, side, date, minHours, minLeverage, ready, needLogin]);
|
||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return (
|
return (
|
||||||
@@ -156,6 +165,20 @@ export default function OpsMapPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="date-field">
|
||||||
|
达标线 ≥
|
||||||
|
<select
|
||||||
|
value={minLeverage}
|
||||||
|
onChange={(e) => setMinLeverage(Number(e.target.value))}
|
||||||
|
aria-label="杠杆达标线"
|
||||||
|
>
|
||||||
|
{MIN_LEVERAGE_OPTIONS.map((v) => (
|
||||||
|
<option key={v} value={v}>
|
||||||
|
{v}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{err && <p style={{ color: "#e85d5d" }}>{err}</p>}
|
{err && <p style={{ color: "#e85d5d" }}>{err}</p>}
|
||||||
|
|||||||
Reference in New Issue
Block a user