Skip to content
Merged
12 changes: 6 additions & 6 deletions blueprints/analyzer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import csv
import io
import json
import traceback

from datetime import datetime, timedelta

import pytz
Expand Down Expand Up @@ -128,7 +128,7 @@ def get_filtered_requests(start_date=None, end_date=None):

return requests
except Exception as e:
logger.exception(f"Error getting filtered requests: {str(e)}\n{traceback.format_exc()}")
logger.exception(f"Error getting filtered requests: {e}")
return []


Expand Down Expand Up @@ -173,7 +173,7 @@ def generate_csv(requests):

return output.getvalue()
except Exception as e:
logger.exception(f"Error generating CSV: {str(e)}\n{traceback.format_exc()}")
logger.exception(f"Error generating CSV: {str(e)}")
return ""


Expand Down Expand Up @@ -216,7 +216,7 @@ def analyzer():
end_date=end_date,
)
except Exception as e:
logger.exception(f"Error rendering analyzer: {str(e)}\n{traceback.format_exc()}")
logger.exception(f"Error rendering analyzer: {str(e)}")
flash("Error loading analyzer dashboard", "error")
return redirect(url_for("core_bp.home"))

Expand Down Expand Up @@ -268,7 +268,7 @@ def api_get_data():
{"status": "success", "data": {"stats": stats_transformed, "requests": requests_data}}
)
except Exception as e:
logger.exception(f"Error getting analyzer data: {str(e)}\n{traceback.format_exc()}")
logger.exception(f"Error getting analyzer data: {str(e)}")
return jsonify(
{"status": "error", "message": f"Error loading analyzer data: {str(e)}"}
), 500
Expand Down Expand Up @@ -352,6 +352,6 @@ def export_requests():
)
return output
except Exception as e:
logger.exception(f"Error exporting requests: {str(e)}\n{traceback.format_exc()}")
logger.exception(f"Error exporting requests: {str(e)}")
flash("Error exporting requests", "error")
return redirect(url_for("analyzer_bp.analyzer"))
15 changes: 7 additions & 8 deletions blueprints/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import csv
import io
import json
import traceback

from datetime import datetime

import pytz
Expand Down Expand Up @@ -66,7 +66,7 @@ def format_log_entry(log, ist):
"created_at": log.created_at.astimezone(ist).strftime("%Y-%m-%d %I:%M:%S %p"),
}
except Exception as e:
logger.exception(f"Error formatting log {log.id}: {str(e)}\n{traceback.format_exc()}")
logger.exception(f"Error formatting log {log.id}: {str(e)}")
return {
"id": log.id,
"api_type": log.api_type,
Expand Down Expand Up @@ -130,7 +130,7 @@ def get_filtered_logs(start_date=None, end_date=None, search_query=None, page=No
return logs, total_pages, total_logs

except Exception as e:
logger.exception(f"Error in get_filtered_logs: {str(e)}\n{traceback.format_exc()}")
logger.exception(f"Error in get_filtered_logs: {str(e)}")
return [], 1, 0


Expand Down Expand Up @@ -203,7 +203,7 @@ def generate_csv(logs):
return si.getvalue()

except Exception as e:
logger.exception(f"Error generating CSV: {str(e)}\n{traceback.format_exc()}")
logger.exception(f"Error generating CSV: {str(e)}")
raise


Expand Down Expand Up @@ -243,7 +243,7 @@ def view_logs():
)

except Exception as e:
logger.exception(f"Error in view_logs: {str(e)}\n{traceback.format_exc()}")
logger.exception(f"Error in view_logs: {str(e)}")
return render_template(
"logs.html",
logs=[],
Expand Down Expand Up @@ -300,6 +300,5 @@ def export_logs():
)

except Exception as e:
error_msg = f"Error exporting logs: {str(e)}\n{traceback.format_exc()}"
logger.exception(error_msg)
return jsonify({"error": error_msg}), 500
logger.exception(f"Error exporting logs: {e}")
return jsonify({"error": "An error occurred while exporting logs"}), 500
63 changes: 52 additions & 11 deletions broker/deltaexchange/api/order_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,20 +228,61 @@ def get_trade_book(auth):
# ---------------------------------------------------------------------------

def get_positions(auth):
"""Fetch all open margined positions."""
"""
Fetch all open positions — both derivatives (margined) and spot (wallet).

Derivatives come from GET /v2/positions/margined.
Spot holdings come from GET /v2/wallet/balances — non-INR assets with
a non-zero balance are synthesised into position-like dicts so they
appear in the OpenAlgo position book alongside derivative positions.
"""
positions = []

# 1. Derivative positions (perpetual futures, options)
try:
result = get_api_response("/v2/positions/margined", auth, method="GET")
if result.get("success"):
return result.get("result", [])
logger.warning(f"[DeltaExchange] get_positions unexpected response: {result}")
return []
positions.extend(result.get("result", []))
else:
logger.warning(f"[DeltaExchange] get_positions/margined unexpected: {result}")
except Exception as e:
logger.error(f"[DeltaExchange] Exception in get_positions: {e}")
return []
logger.error(f"[DeltaExchange] Exception in get_positions/margined: {e}")

# 2. Spot holdings from wallet balances
try:
wallet_result = get_api_response("/v2/wallet/balances", auth, method="GET")
if wallet_result.get("success"):
for asset in wallet_result.get("result", []):
if not isinstance(asset, dict):
continue
symbol = asset.get("asset_symbol", "") or asset.get("symbol", "")
# Skip INR (settlement currency) and zero-balance assets
if symbol in ("INR", "USD", "") or not symbol:
continue
balance = float(asset.get("balance", 0) or 0)
blocked = float(asset.get("blocked_margin", 0) or 0)
size = balance - blocked # available spot holding
if size <= 0:
continue
# Synthesise a position-like dict matching /v2/positions/margined structure
spot_symbol = f"{symbol}_INR"
positions.append({
"product_id": asset.get("asset_id", ""),
"product_symbol": spot_symbol,
"size": size,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Fractional spot quantities are added to positions, but downstream close-all logic truncates size to int, causing sub-1 spot positions to be silently skipped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At broker/deltaexchange/api/order_api.py, line 272:

<comment>Fractional spot quantities are added to positions, but downstream close-all logic truncates size to int, causing sub-1 spot positions to be silently skipped.</comment>

<file context>
@@ -228,20 +228,61 @@ def get_trade_book(auth):
+                positions.append({
+                    "product_id": asset.get("asset_id", ""),
+                    "product_symbol": spot_symbol,
+                    "size": size,
+                    "entry_price": "0",  # Wallet doesn't track entry price
+                    "realized_pnl": "0",
</file context>
Fix with Cubic

"entry_price": "0", # Wallet doesn't track entry price
"realized_pnl": "0",
"unrealized_pnl": "0",
"_is_spot": True, # Internal flag for downstream mapping
})
except Exception as e:
logger.error(f"[DeltaExchange] Exception fetching spot wallet positions: {e}")

return positions


def get_holdings(auth):
"""Delta Exchange is a derivatives-only exchange; equity holdings are not applicable."""
"""Delta Exchange has no equity holdings concept; spot is shown in positions."""
return []


Expand Down Expand Up @@ -406,22 +447,22 @@ def place_smartorder_api(data, auth):
symbol = data.get("symbol")
exchange = data.get("exchange")
product = data.get("product")
position_size = int(data.get("position_size", "0"))
position_size = float(data.get("position_size", "0"))
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Using float for smart-order position arithmetic can produce precision drift that gets truncated by downstream int(qty) conversion, leading to wrong contract size orders.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At broker/deltaexchange/api/order_api.py, line 409:

<comment>Using float for smart-order position arithmetic can produce precision drift that gets truncated by downstream `int(qty)` conversion, leading to wrong contract size orders.</comment>

<file context>
@@ -406,22 +406,22 @@ def place_smartorder_api(data, auth):
     exchange = data.get("exchange")
     product = data.get("product")
-    position_size = int(data.get("position_size", "0"))
+    position_size = float(data.get("position_size", "0"))
 
-    current_position = int(
</file context>
Fix with Cubic


current_position = int(
current_position = float(
get_open_position(symbol, exchange, map_product_type(product), auth)
)
logger.info(
f"[DeltaExchange] SmartOrder: target={position_size} current={current_position}"
)

if position_size == 0 and current_position == 0 and int(data["quantity"]) != 0:
if position_size == 0 and current_position == 0 and float(data["quantity"]) != 0:
return place_order_api(data, auth)

if position_size == current_position:
msg = (
"No OpenPosition Found. Not placing Exit order."
if int(data["quantity"]) == 0
if float(data["quantity"]) == 0
else "No action needed. Position size matches current position"
)
return res, {"status": "success", "message": msg}, None
Expand Down
34 changes: 25 additions & 9 deletions broker/deltaexchange/database/master_contract_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ def _to_canonical_symbol(delta_symbol: str, instrument_type: str, expiry: str) -
"futures": "FUT",
"call_options": "CE",
"put_options": "PE",
"spot": "SPOT",
"move_options": "MOVE",
"interest_rate_swaps": "IRS",
"spreads": "SPREAD",
"options_combos": "COMBO",
Expand Down Expand Up @@ -386,7 +388,7 @@ def process_delta_products(products):
expiry ← settlement_time (None → "" for perpetuals;
ISO string → "DD-MON-YY" for futures/options)
strike ← 0.0 (strike is encoded in the symbol for options)
lotsize ← 1 (1 contract; contract_value gives underlying units)
lotsize ← product_specs.min_order_size or 1 (fractional for spot, e.g. 0.0001 BTC)
instrumenttype ← contract_type (mapped via CONTRACT_TYPE_MAP)
tick_size ← tick_size (string → float)
"""
Expand Down Expand Up @@ -437,16 +439,30 @@ def process_delta_products(products):
except (ValueError, TypeError):
tick_size = 0.0

# Extract strike for option contracts from symbol (e.g. C-BTC-80000-280225 -> 80000.0)
# Extract strike price — use the API field directly when available,
# fall back to parsing from the symbol (e.g. C-BTC-80000-280225 -> 80000.0)
symbol_str = p.get("symbol", "")
strike_val = 0.0
if instrument_type in ("CE", "PE", "TCE", "TPE", "SYNCE", "SYNPE"):
parts_s = symbol_str.split("-")
if len(parts_s) >= 3:
try:
strike_val = float(parts_s[2])
except (ValueError, TypeError):
strike_val = 0.0
try:
strike_val = float(p.get("strike_price") or 0)
except (ValueError, TypeError):
strike_val = 0.0
# Fallback: parse from symbol if API field missing
if strike_val == 0.0:
parts_s = symbol_str.split("-")
if len(parts_s) >= 3:
try:
strike_val = float(parts_s[2])
except (ValueError, TypeError):
strike_val = 0.0

# Lot size: use min_order_size from product_specs (important for spot
# instruments where fractional quantities are allowed, e.g. 0.0001 BTC)
try:
lotsize = float(product_specs.get("min_order_size") or 1)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: lotsize is now produced as float but the DB model keeps lotsize as Integer; fractional spot sizes risk truncation/coercion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At broker/deltaexchange/database/master_contract_db.py, line 463:

<comment>`lotsize` is now produced as float but the DB model keeps `lotsize` as `Integer`; fractional spot sizes risk truncation/coercion.</comment>

<file context>
@@ -437,16 +439,30 @@ def process_delta_products(products):
+        # Lot size: use min_order_size from product_specs (important for spot
+        # instruments where fractional quantities are allowed, e.g. 0.0001 BTC)
+        try:
+            lotsize = float(product_specs.get("min_order_size") or 1)
+        except (ValueError, TypeError):
+            lotsize = 1.0
</file context>
Fix with Cubic

except (ValueError, TypeError):
lotsize = 1.0

# Build OpenAlgo canonical symbol (exchange = CRYPTO, broker-agnostic format)
canonical_symbol = _to_canonical_symbol(symbol_str, instrument_type, expiry)
Expand All @@ -461,7 +477,7 @@ def process_delta_products(products):
"brexchange": "DELTAIN", # Broker identifier (Delta Exchange India)
"expiry": expiry,
"strike": strike_val,
"lotsize": 1,
"lotsize": lotsize,
"instrumenttype": instrument_type,
"tick_size": tick_size,
"contract_value": float(p.get("contract_value") or 1.0),
Expand Down
5 changes: 3 additions & 2 deletions broker/deltaexchange/mapping/margin_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Mapping OpenAlgo margin positions to Delta Exchange margin_required API format
# Delta Exchange endpoint: GET /v2/products/{product_id}/margin_required

from broker.deltaexchange.mapping.transform_data import _order_size
from database.token_db import get_token
from utils.logging import get_logger

Expand All @@ -15,7 +16,7 @@ def transform_margin_positions(positions):
Each OpenAlgo position is converted to a dict with the fields needed
to call GET /v2/products/{product_id}/margin_required:
product_id (int) – from token DB (token = product_id on Delta)
size (int) – absolute quantity
size (int|float) – contracts (int) or spot quantity (float)
side (str) – "buy" or "sell"
order_type (str) – "limit_order" or "market_order"
limit_price (str) – required if order_type == "limit_order"
Expand Down Expand Up @@ -54,7 +55,7 @@ def transform_margin_positions(positions):

entry = {
"product_id": product_id,
"size": int(pos["quantity"]),
"size": _order_size(pos["quantity"], symbol, exchange),
"side": side,
"order_type": order_type,
}
Expand Down
20 changes: 15 additions & 5 deletions broker/deltaexchange/mapping/order_data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json

from database.token_db import get_symbol, get_symbol_info
from database.token_db import get_oa_symbol, get_symbol, get_symbol_info
from utils.logging import get_logger

logger = get_logger(__name__)
Expand Down Expand Up @@ -427,16 +427,26 @@ def map_position_data(position_data):

product_id = position.get("product_id", "")
product_symbol = position.get("product_symbol", "")
is_spot = position.get("_is_spot", False)

# Resolve symbol from DB; fall back to product_symbol
symbol_from_db = get_symbol(str(product_id), "CRYPTO") if product_id else None
# Resolve symbol from DB; fall back to product_symbol.
# For spot wallet entries, product_id is the asset_id (not a product token),
# so look up by brsymbol (e.g. BTC_INR) instead.
if is_spot:
symbol_from_db = get_oa_symbol(product_symbol, "CRYPTO")
else:
symbol_from_db = get_symbol(str(product_id), "CRYPTO") if product_id else None
position["tradingSymbol"] = symbol_from_db or product_symbol

position["exchangeSegment"] = "CRYPTO"
position["productType"] = "NRML"
position["productType"] = "CNC" if is_spot else "NRML"

# Net quantity: positive = long, negative = short
net_qty = int(position.get("size", 0))
# Use float() to support fractional spot sizes (e.g. 0.0001 BTC)
try:
net_qty = float(position.get("size", 0))
except (ValueError, TypeError):
net_qty = 0
position["netQty"] = net_qty

# Average entry price
Expand Down
30 changes: 27 additions & 3 deletions broker/deltaexchange/mapping/transform_data.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
# Mapping OpenAlgo API Request to Delta Exchange API Parameters
# Delta Exchange API docs: https://docs.delta.exchange

from database.token_db import get_br_symbol, get_token
from database.token_db import get_br_symbol, get_symbol_info, get_token
from utils.logging import get_logger

logger = get_logger(__name__)


def _order_size(quantity, symbol, exchange):
"""
Convert quantity to the correct type for the Delta Exchange size parameter.
- Spot instruments: fractional float (e.g. 0.05 SOL)
- Derivatives (futures/options/perps): integer number of contracts

Raises ValueError if a fractional quantity is passed for a non-spot instrument.
"""
qty = float(quantity)
info = get_symbol_info(symbol, exchange)
if info and info.instrumenttype == "SPOT":
return qty
if qty != int(qty):
raise ValueError(
f"Fractional quantity ({qty}) not allowed for derivative contracts. "
f"Use whole numbers for {symbol}."
)
return int(qty)


def transform_data(data, token):
"""
Transforms the OpenAlgo API request structure to Delta Exchange POST /v2/orders payload.
Expand Down Expand Up @@ -38,10 +58,12 @@ def transform_data(data, token):
order_type = map_order_type(data["pricetype"])
side = data["action"].lower() # "buy" or "sell"

size = _order_size(data["quantity"], data["symbol"], data["exchange"])

transformed = {
"product_id": int(token),
"product_symbol": symbol,
"size": int(data["quantity"]),
"size": size,
"side": side,
"order_type": order_type,
"time_in_force": "gtc",
Expand Down Expand Up @@ -134,10 +156,12 @@ def transform_modify_order_data(data):
else:
limit_price = str(data.get("price", "0"))

size = _order_size(data["quantity"], data["symbol"], data["exchange"])

transformed = {
"id": order_id,
"product_id": product_id,
"size": int(data["quantity"]),
"size": size,
"limit_price": limit_price,
}

Expand Down
Loading
Loading