Skip to content

Commit 88aea5e

Browse files
authored
Merge pull request #1087 from Kalaiviswa/main
fix: patch 5 medium-severity findings from deep security audit ,Support fractional quantities for crypto spot trading on Delta Exchange ,Fix position book for crypto spot, rmoney FD audit fix and Groww: Optimize master contract and normalize streaming logs
2 parents 198b5ab + 75cf1c1 commit 88aea5e

File tree

21 files changed

+615
-803
lines changed

21 files changed

+615
-803
lines changed

blueprints/analyzer.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import csv
22
import io
33
import json
4-
import traceback
4+
55
from datetime import datetime, timedelta
66

77
import pytz
@@ -128,7 +128,7 @@ def get_filtered_requests(start_date=None, end_date=None):
128128

129129
return requests
130130
except Exception as e:
131-
logger.exception(f"Error getting filtered requests: {str(e)}\n{traceback.format_exc()}")
131+
logger.exception(f"Error getting filtered requests: {e}")
132132
return []
133133

134134

@@ -173,7 +173,7 @@ def generate_csv(requests):
173173

174174
return output.getvalue()
175175
except Exception as e:
176-
logger.exception(f"Error generating CSV: {str(e)}\n{traceback.format_exc()}")
176+
logger.exception(f"Error generating CSV: {str(e)}")
177177
return ""
178178

179179

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

@@ -268,7 +268,7 @@ def api_get_data():
268268
{"status": "success", "data": {"stats": stats_transformed, "requests": requests_data}}
269269
)
270270
except Exception as e:
271-
logger.exception(f"Error getting analyzer data: {str(e)}\n{traceback.format_exc()}")
271+
logger.exception(f"Error getting analyzer data: {str(e)}")
272272
return jsonify(
273273
{"status": "error", "message": f"Error loading analyzer data: {str(e)}"}
274274
), 500
@@ -352,6 +352,6 @@ def export_requests():
352352
)
353353
return output
354354
except Exception as e:
355-
logger.exception(f"Error exporting requests: {str(e)}\n{traceback.format_exc()}")
355+
logger.exception(f"Error exporting requests: {str(e)}")
356356
flash("Error exporting requests", "error")
357357
return redirect(url_for("analyzer_bp.analyzer"))

blueprints/log.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import csv
44
import io
55
import json
6-
import traceback
6+
77
from datetime import datetime
88

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

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

136136

@@ -203,7 +203,7 @@ def generate_csv(logs):
203203
return si.getvalue()
204204

205205
except Exception as e:
206-
logger.exception(f"Error generating CSV: {str(e)}\n{traceback.format_exc()}")
206+
logger.exception(f"Error generating CSV: {str(e)}")
207207
raise
208208

209209

@@ -243,7 +243,7 @@ def view_logs():
243243
)
244244

245245
except Exception as e:
246-
logger.exception(f"Error in view_logs: {str(e)}\n{traceback.format_exc()}")
246+
logger.exception(f"Error in view_logs: {str(e)}")
247247
return render_template(
248248
"logs.html",
249249
logs=[],
@@ -300,6 +300,5 @@ def export_logs():
300300
)
301301

302302
except Exception as e:
303-
error_msg = f"Error exporting logs: {str(e)}\n{traceback.format_exc()}"
304-
logger.exception(error_msg)
305-
return jsonify({"error": error_msg}), 500
303+
logger.exception(f"Error exporting logs: {e}")
304+
return jsonify({"error": "An error occurred while exporting logs"}), 500

broker/deltaexchange/api/order_api.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -228,20 +228,61 @@ def get_trade_book(auth):
228228
# ---------------------------------------------------------------------------
229229

230230
def get_positions(auth):
231-
"""Fetch all open margined positions."""
231+
"""
232+
Fetch all open positions — both derivatives (margined) and spot (wallet).
233+
234+
Derivatives come from GET /v2/positions/margined.
235+
Spot holdings come from GET /v2/wallet/balances — non-INR assets with
236+
a non-zero balance are synthesised into position-like dicts so they
237+
appear in the OpenAlgo position book alongside derivative positions.
238+
"""
239+
positions = []
240+
241+
# 1. Derivative positions (perpetual futures, options)
232242
try:
233243
result = get_api_response("/v2/positions/margined", auth, method="GET")
234244
if result.get("success"):
235-
return result.get("result", [])
236-
logger.warning(f"[DeltaExchange] get_positions unexpected response: {result}")
237-
return []
245+
positions.extend(result.get("result", []))
246+
else:
247+
logger.warning(f"[DeltaExchange] get_positions/margined unexpected: {result}")
238248
except Exception as e:
239-
logger.error(f"[DeltaExchange] Exception in get_positions: {e}")
240-
return []
249+
logger.error(f"[DeltaExchange] Exception in get_positions/margined: {e}")
250+
251+
# 2. Spot holdings from wallet balances
252+
try:
253+
wallet_result = get_api_response("/v2/wallet/balances", auth, method="GET")
254+
if wallet_result.get("success"):
255+
for asset in wallet_result.get("result", []):
256+
if not isinstance(asset, dict):
257+
continue
258+
symbol = asset.get("asset_symbol", "") or asset.get("symbol", "")
259+
# Skip INR (settlement currency) and zero-balance assets
260+
if symbol in ("INR", "USD", "") or not symbol:
261+
continue
262+
balance = float(asset.get("balance", 0) or 0)
263+
blocked = float(asset.get("blocked_margin", 0) or 0)
264+
size = balance - blocked # available spot holding
265+
if size <= 0:
266+
continue
267+
# Synthesise a position-like dict matching /v2/positions/margined structure
268+
spot_symbol = f"{symbol}_INR"
269+
positions.append({
270+
"product_id": asset.get("asset_id", ""),
271+
"product_symbol": spot_symbol,
272+
"size": size,
273+
"entry_price": "0", # Wallet doesn't track entry price
274+
"realized_pnl": "0",
275+
"unrealized_pnl": "0",
276+
"_is_spot": True, # Internal flag for downstream mapping
277+
})
278+
except Exception as e:
279+
logger.error(f"[DeltaExchange] Exception fetching spot wallet positions: {e}")
280+
281+
return positions
241282

242283

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

247288

@@ -406,22 +447,22 @@ def place_smartorder_api(data, auth):
406447
symbol = data.get("symbol")
407448
exchange = data.get("exchange")
408449
product = data.get("product")
409-
position_size = int(data.get("position_size", "0"))
450+
position_size = float(data.get("position_size", "0"))
410451

411-
current_position = int(
452+
current_position = float(
412453
get_open_position(symbol, exchange, map_product_type(product), auth)
413454
)
414455
logger.info(
415456
f"[DeltaExchange] SmartOrder: target={position_size} current={current_position}"
416457
)
417458

418-
if position_size == 0 and current_position == 0 and int(data["quantity"]) != 0:
459+
if position_size == 0 and current_position == 0 and float(data["quantity"]) != 0:
419460
return place_order_api(data, auth)
420461

421462
if position_size == current_position:
422463
msg = (
423464
"No OpenPosition Found. Not placing Exit order."
424-
if int(data["quantity"]) == 0
465+
if float(data["quantity"]) == 0
425466
else "No action needed. Position size matches current position"
426467
)
427468
return res, {"status": "success", "message": msg}, None

broker/deltaexchange/database/master_contract_db.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ def _to_canonical_symbol(delta_symbol: str, instrument_type: str, expiry: str) -
253253
"futures": "FUT",
254254
"call_options": "CE",
255255
"put_options": "PE",
256+
"spot": "SPOT",
257+
"move_options": "MOVE",
256258
"interest_rate_swaps": "IRS",
257259
"spreads": "SPREAD",
258260
"options_combos": "COMBO",
@@ -386,7 +388,7 @@ def process_delta_products(products):
386388
expiry ← settlement_time (None → "" for perpetuals;
387389
ISO string → "DD-MON-YY" for futures/options)
388390
strike ← 0.0 (strike is encoded in the symbol for options)
389-
lotsize ← 1 (1 contract; contract_value gives underlying units)
391+
lotsize ← product_specs.min_order_size or 1 (fractional for spot, e.g. 0.0001 BTC)
390392
instrumenttype ← contract_type (mapped via CONTRACT_TYPE_MAP)
391393
tick_size ← tick_size (string → float)
392394
"""
@@ -437,16 +439,30 @@ def process_delta_products(products):
437439
except (ValueError, TypeError):
438440
tick_size = 0.0
439441

440-
# Extract strike for option contracts from symbol (e.g. C-BTC-80000-280225 -> 80000.0)
442+
# Extract strike price — use the API field directly when available,
443+
# fall back to parsing from the symbol (e.g. C-BTC-80000-280225 -> 80000.0)
441444
symbol_str = p.get("symbol", "")
442445
strike_val = 0.0
443446
if instrument_type in ("CE", "PE", "TCE", "TPE", "SYNCE", "SYNPE"):
444-
parts_s = symbol_str.split("-")
445-
if len(parts_s) >= 3:
446-
try:
447-
strike_val = float(parts_s[2])
448-
except (ValueError, TypeError):
449-
strike_val = 0.0
447+
try:
448+
strike_val = float(p.get("strike_price") or 0)
449+
except (ValueError, TypeError):
450+
strike_val = 0.0
451+
# Fallback: parse from symbol if API field missing
452+
if strike_val == 0.0:
453+
parts_s = symbol_str.split("-")
454+
if len(parts_s) >= 3:
455+
try:
456+
strike_val = float(parts_s[2])
457+
except (ValueError, TypeError):
458+
strike_val = 0.0
459+
460+
# Lot size: use min_order_size from product_specs (important for spot
461+
# instruments where fractional quantities are allowed, e.g. 0.0001 BTC)
462+
try:
463+
lotsize = float(product_specs.get("min_order_size") or 1)
464+
except (ValueError, TypeError):
465+
lotsize = 1.0
450466

451467
# Build OpenAlgo canonical symbol (exchange = CRYPTO, broker-agnostic format)
452468
canonical_symbol = _to_canonical_symbol(symbol_str, instrument_type, expiry)
@@ -461,7 +477,7 @@ def process_delta_products(products):
461477
"brexchange": "DELTAIN", # Broker identifier (Delta Exchange India)
462478
"expiry": expiry,
463479
"strike": strike_val,
464-
"lotsize": 1,
480+
"lotsize": lotsize,
465481
"instrumenttype": instrument_type,
466482
"tick_size": tick_size,
467483
"contract_value": float(p.get("contract_value") or 1.0),

broker/deltaexchange/mapping/margin_data.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Mapping OpenAlgo margin positions to Delta Exchange margin_required API format
33
# Delta Exchange endpoint: GET /v2/products/{product_id}/margin_required
44

5+
from broker.deltaexchange.mapping.transform_data import _order_size
56
from database.token_db import get_token
67
from utils.logging import get_logger
78

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

5556
entry = {
5657
"product_id": product_id,
57-
"size": int(pos["quantity"]),
58+
"size": _order_size(pos["quantity"], symbol, exchange),
5859
"side": side,
5960
"order_type": order_type,
6061
}

broker/deltaexchange/mapping/order_data.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22

3-
from database.token_db import get_symbol, get_symbol_info
3+
from database.token_db import get_oa_symbol, get_symbol, get_symbol_info
44
from utils.logging import get_logger
55

66
logger = get_logger(__name__)
@@ -427,16 +427,26 @@ def map_position_data(position_data):
427427

428428
product_id = position.get("product_id", "")
429429
product_symbol = position.get("product_symbol", "")
430+
is_spot = position.get("_is_spot", False)
430431

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

435441
position["exchangeSegment"] = "CRYPTO"
436-
position["productType"] = "NRML"
442+
position["productType"] = "CNC" if is_spot else "NRML"
437443

438444
# Net quantity: positive = long, negative = short
439-
net_qty = int(position.get("size", 0))
445+
# Use float() to support fractional spot sizes (e.g. 0.0001 BTC)
446+
try:
447+
net_qty = float(position.get("size", 0))
448+
except (ValueError, TypeError):
449+
net_qty = 0
440450
position["netQty"] = net_qty
441451

442452
# Average entry price

broker/deltaexchange/mapping/transform_data.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
# Mapping OpenAlgo API Request to Delta Exchange API Parameters
22
# Delta Exchange API docs: https://docs.delta.exchange
33

4-
from database.token_db import get_br_symbol, get_token
4+
from database.token_db import get_br_symbol, get_symbol_info, get_token
55
from utils.logging import get_logger
66

77
logger = get_logger(__name__)
88

99

10+
def _order_size(quantity, symbol, exchange):
11+
"""
12+
Convert quantity to the correct type for the Delta Exchange size parameter.
13+
- Spot instruments: fractional float (e.g. 0.05 SOL)
14+
- Derivatives (futures/options/perps): integer number of contracts
15+
16+
Raises ValueError if a fractional quantity is passed for a non-spot instrument.
17+
"""
18+
qty = float(quantity)
19+
info = get_symbol_info(symbol, exchange)
20+
if info and info.instrumenttype == "SPOT":
21+
return qty
22+
if qty != int(qty):
23+
raise ValueError(
24+
f"Fractional quantity ({qty}) not allowed for derivative contracts. "
25+
f"Use whole numbers for {symbol}."
26+
)
27+
return int(qty)
28+
29+
1030
def transform_data(data, token):
1131
"""
1232
Transforms the OpenAlgo API request structure to Delta Exchange POST /v2/orders payload.
@@ -38,10 +58,12 @@ def transform_data(data, token):
3858
order_type = map_order_type(data["pricetype"])
3959
side = data["action"].lower() # "buy" or "sell"
4060

61+
size = _order_size(data["quantity"], data["symbol"], data["exchange"])
62+
4163
transformed = {
4264
"product_id": int(token),
4365
"product_symbol": symbol,
44-
"size": int(data["quantity"]),
66+
"size": size,
4567
"side": side,
4668
"order_type": order_type,
4769
"time_in_force": "gtc",
@@ -134,10 +156,12 @@ def transform_modify_order_data(data):
134156
else:
135157
limit_price = str(data.get("price", "0"))
136158

159+
size = _order_size(data["quantity"], data["symbol"], data["exchange"])
160+
137161
transformed = {
138162
"id": order_id,
139163
"product_id": product_id,
140-
"size": int(data["quantity"]),
164+
"size": size,
141165
"limit_price": limit_price,
142166
}
143167

0 commit comments

Comments
 (0)