61e8da1e8b
Read-only 1H index candles, point amplitude metrics, history save and CSV export; no order-path changes. Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
3.7 KiB
Python
123 lines
3.7 KiB
Python
"""振幅统计历史作业存储(中控 JSON)."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import threading
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
_LOCK = threading.Lock()
|
|
_STORE_NAME = "amp_stats_history.json"
|
|
_MAX_ITEMS = 80
|
|
|
|
|
|
def _store_path() -> Path:
|
|
return Path(__file__).resolve().parent / _STORE_NAME
|
|
|
|
|
|
def _now_iso() -> str:
|
|
return datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
def _load() -> dict[str, Any]:
|
|
path = _store_path()
|
|
if not path.is_file():
|
|
return {"items": []}
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
return {"items": []}
|
|
if not isinstance(data, dict):
|
|
return {"items": []}
|
|
items = data.get("items")
|
|
if not isinstance(items, list):
|
|
items = []
|
|
return {"items": items}
|
|
|
|
|
|
def _save(data: dict[str, Any]) -> None:
|
|
path = _store_path()
|
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
|
|
def list_history(*, symbol: str = "", limit: int = 50) -> list[dict[str, Any]]:
|
|
with _LOCK:
|
|
items = list(_load().get("items") or [])
|
|
sym = (symbol or "").strip().lower()
|
|
if sym:
|
|
items = [x for x in items if str(x.get("symbol") or "").lower() == sym]
|
|
limit = max(1, min(200, int(limit or 50)))
|
|
out = []
|
|
for it in items[:limit]:
|
|
out.append(
|
|
{
|
|
"id": it.get("id"),
|
|
"created_at": it.get("created_at"),
|
|
"symbol": it.get("symbol"),
|
|
"symbol_label": it.get("symbol_label"),
|
|
"start_hour": it.get("start_hour"),
|
|
"end_hour": it.get("end_hour"),
|
|
"period": it.get("period"),
|
|
"price_source": it.get("price_source"),
|
|
"sample_count": (it.get("summary") or {}).get("sample_count"),
|
|
"max_amplitude": (it.get("summary") or {}).get("max_amplitude"),
|
|
"max_amplitude_day": (it.get("summary") or {}).get("max_amplitude_day"),
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def get_history(item_id: str) -> Optional[dict[str, Any]]:
|
|
iid = (item_id or "").strip()
|
|
if not iid:
|
|
return None
|
|
with _LOCK:
|
|
for it in _load().get("items") or []:
|
|
if str(it.get("id")) == iid:
|
|
return dict(it)
|
|
return None
|
|
|
|
|
|
def save_history(payload: dict[str, Any]) -> dict[str, Any]:
|
|
item = {
|
|
"id": uuid.uuid4().hex[:12],
|
|
"created_at": _now_iso(),
|
|
"exchange": payload.get("exchange"),
|
|
"symbol": payload.get("symbol"),
|
|
"symbol_label": payload.get("symbol_label"),
|
|
"start_hour": payload.get("start_hour"),
|
|
"end_hour": payload.get("end_hour"),
|
|
"period": payload.get("period"),
|
|
"timeframe": payload.get("timeframe"),
|
|
"price_source": payload.get("price_source"),
|
|
"inst_id": payload.get("inst_id"),
|
|
"timezone": payload.get("timezone"),
|
|
"summary": payload.get("summary") or {},
|
|
"rows": payload.get("rows") or [],
|
|
"missing_count": payload.get("missing_count") or 0,
|
|
}
|
|
with _LOCK:
|
|
data = _load()
|
|
items = list(data.get("items") or [])
|
|
items.insert(0, item)
|
|
data["items"] = items[:_MAX_ITEMS]
|
|
_save(data)
|
|
return item
|
|
|
|
|
|
def delete_history(item_id: str) -> bool:
|
|
iid = (item_id or "").strip()
|
|
if not iid:
|
|
return False
|
|
with _LOCK:
|
|
data = _load()
|
|
items = list(data.get("items") or [])
|
|
new_items = [x for x in items if str(x.get("id")) != iid]
|
|
if len(new_items) == len(items):
|
|
return False
|
|
data["items"] = new_items
|
|
_save(data)
|
|
return True
|