diff --git a/trade_web/app.py b/trade_web/app.py new file mode 100644 index 0000000..b99dc01 --- /dev/null +++ b/trade_web/app.py @@ -0,0 +1,295 @@ +from __future__ import annotations + +from pathlib import Path +import os +import threading +import time +import uuid + +from flask import Flask, jsonify, render_template, request + +from exchange import ExchangeGuard +from trading_engine import ConfigStore, TradingAnalyzer +from storage import TradeStore + + +BASE_DIR = Path(__file__).resolve().parent +app = Flask(__name__) +config_store = ConfigStore(BASE_DIR / "config.json") +store = TradeStore(BASE_DIR / "data" / "trade_web.sqlite3") +analyzer = TradingAnalyzer() +exchange_guard = ExchangeGuard() +optimizer_jobs: dict[str, dict] = {} +optimizer_lock = threading.Lock() + + +@app.get("/") +def index(): + return render_template("index.html") + + +@app.get("/api/config") +def get_config(): + return jsonify(config_store.load()) + + +@app.post("/api/config") +def update_config(): + config = config_store.load() + payload = request.get_json(silent=True) or {} + symbol = str(payload.get("symbol", config.get("symbol", "BTCUSDT"))).upper().strip() + signal_mode = str(payload.get("signal_mode", config.get("signal_mode", "balanced"))).strip() + if symbol: + config["symbol"] = symbol + if symbol not in config.get("available_symbols", []): + config.setdefault("available_symbols", []).append(symbol) + if signal_mode in config.get("available_signal_modes", ["balanced", "high_precision", "lux_style"]): + config["signal_mode"] = signal_mode + config_store.save(config) + return jsonify(config) + + +@app.get("/api/analyze") +def analyze(): + config = config_store.load() + demo = request.args.get("demo", "").lower() in {"1", "true", "yes"} + result = analyzer.analyze(config, use_demo_data=demo) + result["risk_plan"] = analyzer.risk_plan(config, result["signal"]) + return jsonify(result) + + +@app.get("/api/backtest") +def backtest(): + config = config_store.load() + demo = request.args.get("demo", "").lower() in {"1", "true", "yes"} + symbols = request.args.get("symbols") + modes = request.args.get("modes") + result = analyzer.backtest( + config, + symbols=[item.strip().upper() for item in symbols.split(",") if item.strip()] if symbols else None, + modes=[item.strip() for item in modes.split(",") if item.strip()] if modes else None, + candles=int(request.args.get("candles", "360")), + horizon=int(request.args.get("horizon", "12")), + use_demo_data=demo, + ) + run_id = store.save_run("backtest", result, label=",".join(result["settings"]["symbols"])) + result["run_id"] = run_id + return jsonify(result) + + +@app.get("/api/forecast") +def forecast(): + config = config_store.load() + demo = request.args.get("demo", "").lower() in {"1", "true", "yes"} + return jsonify( + analyzer.forecast( + config, + candles=int(request.args.get("candles", "220")), + horizon=int(request.args.get("horizon", "24")), + use_demo_data=demo, + ) + ) + + +@app.get("/api/optimize") +def optimize(): + config = config_store.load() + demo = request.args.get("demo", "").lower() in {"1", "true", "yes"} + symbols = request.args.get("symbols") + result = analyzer.optimize( + config, + symbols=[item.strip().upper() for item in symbols.split(",") if item.strip()] if symbols else None, + candles=int(request.args.get("candles", "320")), + horizon=int(request.args.get("horizon", "12")), + use_demo_data=demo, + ) + return jsonify(result) + + +@app.post("/api/optimize/start") +def optimize_start(): + config = config_store.load() + payload = request.get_json(silent=True) or {} + symbols = payload.get("symbols") + demo = bool(payload.get("demo")) + job_id = uuid.uuid4().hex + with optimizer_lock: + optimizer_jobs[job_id] = { + "id": job_id, + "status": "running", + "done": 0, + "total": 0, + "best": None, + "candidates": [], + "best_history": [], + "result": None, + "error": None, + "cancel": False, + "started_at": int(time.time()), + } + + def progress(update: dict) -> None: + with optimizer_lock: + job = optimizer_jobs.get(job_id) + if not job: + return + job["done"] = update.get("done", job["done"]) + job["total"] = update.get("total", job["total"]) + job["best"] = update.get("best") + if update.get("candidate"): + job["candidates"].append(update["candidate"]) + job["candidates"] = sorted(job["candidates"], key=lambda item: item["score"], reverse=True)[:8] + if update.get("convergence"): + job["best_history"].append(update["convergence"]) + + def should_cancel() -> bool: + with optimizer_lock: + return bool(optimizer_jobs.get(job_id, {}).get("cancel")) + + def run_job() -> None: + try: + result = analyzer.optimize( + config, + symbols=[item.strip().upper() for item in symbols.split(",") if item.strip()] + if isinstance(symbols, str) + else None, + candles=int(payload.get("candles", 320)), + horizon=int(payload.get("horizon", 12)), + use_demo_data=demo, + progress=progress, + should_cancel=should_cancel, + ) + with optimizer_lock: + job = optimizer_jobs[job_id] + run_id = store.save_run("optimizer", result, label=",".join(result["settings"]["symbols"])) + result["run_id"] = run_id + job["result"] = result + job["best"] = result.get("best") + job["status"] = "cancelled" if job.get("cancel") else "done" + job["finished_at"] = int(time.time()) + except Exception as exc: + with optimizer_lock: + job = optimizer_jobs[job_id] + job["status"] = "error" + job["error"] = str(exc) + job["finished_at"] = int(time.time()) + + threading.Thread(target=run_job, daemon=True).start() + return jsonify({"job_id": job_id}) + + +@app.get("/api/optimize/status/") +def optimize_status(job_id: str): + with optimizer_lock: + job = optimizer_jobs.get(job_id) + if not job: + return jsonify({"error": "job nicht gefunden"}), 404 + return jsonify({key: value for key, value in job.items() if key != "cancel"}) + + +@app.post("/api/optimize/cancel/") +def optimize_cancel(job_id: str): + with optimizer_lock: + job = optimizer_jobs.get(job_id) + if not job: + return jsonify({"error": "job nicht gefunden"}), 404 + job["cancel"] = True + return jsonify({"status": "cancel_requested"}) + + +@app.post("/api/optimize/apply") +def apply_optimization(): + config = config_store.load() + payload = request.get_json(silent=True) or {} + params = payload.get("signal_params") + if not isinstance(params, dict): + return jsonify({"error": "signal_params fehlt"}), 400 + config["signal_params"] = params + symbol_params = payload.get("symbol_params") + if isinstance(symbol_params, dict): + config["symbol_params"] = symbol_params + config["optimizer_suggestion"] = { + "applied_at": int(__import__("time").time()), + "params": params, + "symbol_params": config.get("symbol_params", {}), + "source": "api/optimize", + } + config_store.save(config) + return jsonify(config) + + +@app.get("/api/history") +def history(): + kind = request.args.get("kind") + limit = int(request.args.get("limit", "20")) + return jsonify({"runs": store.recent_runs(kind=kind, limit=limit)}) + + +@app.get("/api/health") +def health(): + config = config_store.load() + return jsonify( + { + "status": "ok", + "updated_at": int(time.time()), + "database": str(store.path), + "execution": config.get("execution", {}), + "risk_management": config.get("risk_management", {}), + "exchange_guard": exchange_guard.status(config), + "optimizer_jobs": { + "running": sum(1 for job in optimizer_jobs.values() if job.get("status") == "running"), + "total": len(optimizer_jobs), + }, + } + ) + + +@app.get("/api/paper/orders") +def paper_orders(): + return jsonify({"orders": store.recent_paper_orders(limit=int(request.args.get("limit", "25")))}) + + +@app.post("/api/paper/order") +def paper_order(): + config = config_store.load() + payload = request.get_json(silent=True) or {} + signal = payload.get("signal") + if not isinstance(signal, dict): + analysis = analyzer.analyze(config, use_demo_data=bool(payload.get("demo"))) + signal = analysis["signal"] + plan = analyzer.risk_plan(config, signal) + if not plan["paper_allowed"]: + return jsonify({"error": "paper order blockiert", "risk_plan": plan}), 400 + order = { + "symbol": config.get("symbol", "BTCUSDT"), + "side": plan.get("side", signal.get("side", "BUY")), + "status": "paper_open", + "quantity": plan["quantity"], + "entry": plan["entry"], + "stop": plan["stop"], + "target": plan["target"], + "risk_amount": plan["risk_amount"], + "risk_plan": plan, + "signal": signal, + } + order["id"] = store.save_paper_order(order) + return jsonify(order) + + +@app.get("/api/exchange/status") +def exchange_status(): + return jsonify(exchange_guard.status(config_store.load())) + + +@app.post("/api/exchange/order") +def exchange_order(): + config = config_store.load() + payload = request.get_json(silent=True) or {} + return jsonify(exchange_guard.place_order(config, payload)) + + +if __name__ == "__main__": + host = os.environ.get("FLASK_HOST", "127.0.0.1") + port = int(os.environ.get("PORT", "5050")) + debug = os.environ.get("FLASK_DEBUG", "1") == "1" + app.run(host=host, port=port, debug=debug)