feat: split audit logs into live/dryrun subdirs, add catlog --dry-run, list all kline intervals

- Write live trades to logs/live/ and dry-run trades to logs/dryrun/
- Add -d/--dry-run flag to catlog to read dry-run audit logs
- List all 16 Binance kline interval options in --help and docs

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 10:27:22 +08:00
parent e37993c8b5
commit cf26a3dd3a
5 changed files with 31 additions and 18 deletions

View File

@@ -22,30 +22,38 @@ def _resolve_audit_dir(paths: RuntimePaths) -> Path:
return _audit_dir_cache[key]
def _audit_path(paths: RuntimePaths | None = None) -> Path:
def _audit_path(paths: RuntimePaths | None = None, *, dry_run: bool = False) -> Path:
paths = ensure_runtime_dirs(paths or get_runtime_paths())
logs_dir = _resolve_audit_dir(paths)
logs_dir.mkdir(parents=True, exist_ok=True)
return logs_dir / f"audit_{datetime.now(timezone.utc).strftime('%Y%m%d')}.jsonl"
subdir = logs_dir / ("dryrun" if dry_run else "live")
subdir.mkdir(parents=True, exist_ok=True)
return subdir / f"audit_{datetime.now(timezone.utc).strftime('%Y%m%d')}.jsonl"
def audit_event(event: str, payload: dict[str, Any], paths: RuntimePaths | None = None) -> dict[str, Any]:
def audit_event(
event: str, payload: dict[str, Any], paths: RuntimePaths | None = None, *, dry_run: bool = False
) -> dict[str, Any]:
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"event": event,
**payload,
}
with _audit_path(paths).open("a", encoding="utf-8") as handle:
with _audit_path(paths, dry_run=dry_run).open("a", encoding="utf-8") as handle:
handle.write(json.dumps(entry, ensure_ascii=False, default=json_default) + "\n")
return entry
def read_audit_log(paths: RuntimePaths | None = None, limit: int = 10, offset: int = 0) -> list[dict[str, Any]]:
def read_audit_log(
paths: RuntimePaths | None = None, limit: int = 10, offset: int = 0, *, dry_run: bool = False
) -> list[dict[str, Any]]:
paths = ensure_runtime_dirs(paths or get_runtime_paths())
logs_dir = _resolve_audit_dir(paths)
if not logs_dir.exists():
return []
audit_files = sorted(logs_dir.glob("audit_*.jsonl"), reverse=True)
subdir = logs_dir / ("dryrun" if dry_run else "live")
if not subdir.exists():
return []
audit_files = sorted(subdir.glob("audit_*.jsonl"), reverse=True)
needed = offset + limit
chunks: list[list[dict[str, Any]]] = []
total = 0

View File

@@ -191,7 +191,7 @@ JSON Output:
}
Fields:
interval candle interval (enum: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w)
interval candle interval (enum: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M)
limit number of candles requested (int)
symbol normalized trading pair
open_time candle open timestamp in ms (int)
@@ -212,7 +212,7 @@ JSON Output:
}
Fields:
interval candle interval (enum: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w)
interval candle interval (enum: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M)
limit number of candles requested (int)
symbol normalized trading pair
open_time candle open timestamp in ms (int)
@@ -730,7 +730,10 @@ def build_parser() -> argparse.ArgumentParser:
description="Fetch OHLCV candlestick data for one or more symbols.",
)
klines_parser.add_argument("symbols", nargs="+", metavar="SYM", help="Symbols to query")
klines_parser.add_argument("-i", "--interval", default="1h", help="Kline interval (default: 1h)")
klines_parser.add_argument(
"-i", "--interval", default="1h",
help="Kline interval: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M (default: 1h)",
)
klines_parser.add_argument("-l", "--limit", type=int, default=100, help="Number of candles (default: 100)")
_add_global_flags(klines_parser)
@@ -784,6 +787,7 @@ def build_parser() -> argparse.ArgumentParser:
catlog_parser.add_argument(
"-o", "--offset", type=int, default=0, help="Skip the most recent N entries (default: 0)"
)
catlog_parser.add_argument("-d", "--dry-run", action="store_true", help="Read dry-run audit logs")
_add_global_flags(catlog_parser)
completion_parser = subparsers.add_parser(
@@ -1057,9 +1061,9 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "catlog":
with with_spinner("Reading audit logs...", enabled=not args.agent):
entries = read_audit_log(limit=args.limit, offset=args.offset)
entries = read_audit_log(limit=args.limit, offset=args.offset, dry_run=args.dry_run)
print_output(
{"entries": entries, "limit": args.limit, "offset": args.offset, "total": len(entries)},
{"entries": entries, "limit": args.limit, "offset": args.offset, "total": len(entries), "dry_run": args.dry_run},
agent=args.agent,
)
return 0

View File

@@ -112,7 +112,7 @@ def execute_spot_trade(
dry_run=is_dry_run,
)
audit_event("trade_submitted", _trade_log_payload(intent, payload, status="submitted"))
audit_event("trade_submitted", _trade_log_payload(intent, payload, status="submitted"), dry_run=intent.dry_run)
if is_dry_run:
response = {"dry_run": True, "status": "DRY_RUN", "request": payload}
result = asdict(
@@ -128,14 +128,14 @@ def execute_spot_trade(
)
)
audit_event(
"trade_filled", {**_trade_log_payload(intent, payload, status="DRY_RUN"), "response_payload": response}
"trade_filled", {**_trade_log_payload(intent, payload, status="DRY_RUN"), "response_payload": response}, dry_run=intent.dry_run
)
return {"trade": result}
try:
response = spot_client.new_order(**payload)
except Exception as exc:
audit_event("trade_failed", _trade_log_payload(intent, payload, status="failed", error=str(exc)))
audit_event("trade_failed", _trade_log_payload(intent, payload, status="failed", error=str(exc)), dry_run=intent.dry_run)
raise RuntimeError(f"Spot order failed: {exc}") from exc
result = asdict(
@@ -151,6 +151,7 @@ def execute_spot_trade(
)
)
audit_event(
"trade_filled", {**_trade_log_payload(intent, payload, status=result["status"]), "response_payload": response}
"trade_filled", {**_trade_log_payload(intent, payload, status=result["status"]), "response_payload": response},
dry_run=intent.dry_run,
)
return {"trade": result}