RayMelius Claude Opus 4.6 commited on
Commit
7cc35dd
Β·
1 Parent(s): 4937757

Add API order endpoint for external integrations (Soci)

Browse files

POST /ch/api/order accepts {api_key, member_id, symbol, side, quantity, price}
for programmatic order placement by external services like Soci agents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. clearing_house/app.py +51 -0
clearing_house/app.py CHANGED
@@ -40,6 +40,9 @@ def ts_filter(ts):
40
  MATCHER_URL = os.getenv("MATCHER_URL", Config.MATCHER_URL)
41
  SECURITIES_FILE = os.getenv("SECURITIES_FILE", "/app/shared/data/securities.txt")
42
 
 
 
 
43
  # SSE clients
44
  _sse_clients: list[Queue] = []
45
  _sse_lock = threading.Lock()
@@ -435,6 +438,54 @@ def api_set_member_strategy(member_id):
435
  return jsonify({"status": "ok", "member_id": member_id, "strategy": result})
436
 
437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  # ── SSE ────────────────────────────────────────────────────────────────────────
439
 
440
  @app.route("/ch/stream")
 
40
  MATCHER_URL = os.getenv("MATCHER_URL", Config.MATCHER_URL)
41
  SECURITIES_FILE = os.getenv("SECURITIES_FILE", "/app/shared/data/securities.txt")
42
 
43
+ # API key for external integrations (e.g. Soci agents trading)
44
+ CH_API_KEY = os.getenv("CH_API_KEY", "soci-stockex-2024")
45
+
46
  # SSE clients
47
  _sse_clients: list[Queue] = []
48
  _sse_lock = threading.Lock()
 
438
  return jsonify({"status": "ok", "member_id": member_id, "strategy": result})
439
 
440
 
441
+ @app.route("/ch/api/order", methods=["POST"])
442
+ def api_order():
443
+ """Place an order via API key auth (for external integrations like Soci).
444
+
445
+ JSON body: {api_key, member_id, symbol, side, quantity, price}
446
+ """
447
+ data = request.get_json(force=True)
448
+ api_key = data.get("api_key", "")
449
+ if api_key != CH_API_KEY:
450
+ return jsonify({"error": "Invalid API key"}), 403
451
+
452
+ member_id = str(data.get("member_id", "")).upper().strip()
453
+ symbol = str(data.get("symbol", "")).upper()
454
+ side = str(data.get("side", "")).upper()
455
+ quantity = int(data.get("quantity", 0))
456
+ price = float(data.get("price", 0))
457
+
458
+ if side not in ("BUY", "SELL") or quantity <= 0 or price <= 0 or not symbol:
459
+ return jsonify({"error": "Invalid order parameters"}), 400
460
+
461
+ member = db.get_member(member_id)
462
+ if not member:
463
+ return jsonify({"error": f"Member {member_id} not found"}), 404
464
+
465
+ if side == "BUY" and quantity * price > member["capital"]:
466
+ return jsonify({"error": f"Insufficient capital (have €{member['capital']:.2f})"}), 400
467
+
468
+ if side == "SELL":
469
+ holding = db.get_holding(member_id, symbol)
470
+ if holding["quantity"] < quantity:
471
+ return jsonify({"error": f"Insufficient holdings ({holding['quantity']} shares)"}), 400
472
+
473
+ cl_ord_id = f"{member_id}-{int(time.time()*1000)}-API"
474
+ msg = {
475
+ "cl_ord_id": cl_ord_id,
476
+ "symbol": symbol,
477
+ "side": side,
478
+ "quantity": quantity,
479
+ "price": price,
480
+ "ord_type": "LIMIT",
481
+ "time_in_force": "DAY",
482
+ "timestamp": time.time(),
483
+ "source": "CLRH",
484
+ }
485
+ get_producer().send(Config.ORDERS_TOPIC, msg)
486
+ return jsonify({"status": "ok", "cl_ord_id": cl_ord_id, "member_id": member_id})
487
+
488
+
489
  # ── SSE ────────────────────────────────────────────────────────────────────────
490
 
491
  @app.route("/ch/stream")