Skip to content

Commit 60be9f1

Browse files
Fix simulation mode issues: convert simulated_orders to dictionary, generate unique order IDs, and improve order execution
1 parent a08eb13 commit 60be9f1

2 files changed

Lines changed: 226 additions & 105 deletions

File tree

directa_api/trading.py

Lines changed: 183 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import time
44
import re
55
import datetime
6+
import random
67
from typing import Optional, Dict, List, Union, Tuple, Any
78

89
from directa_api.parsers import (
@@ -61,7 +62,7 @@ def __init__(self, host: str = "127.0.0.1", port: int = 10002, buffer_size: int
6162

6263
# Simulation data (used only in simulation mode)
6364
self.simulated_portfolio = []
64-
self.simulated_orders = []
65+
self.simulated_orders = {} # Dictionary with order_id as key
6566
self.simulated_account = {
6667
"account_code": "SIM1234",
6768
"liquidity": 10000.0,
@@ -678,8 +679,8 @@ def place_order(self, symbol: str, side: str, quantity: int,
678679
else:
679680
raise ValueError("Side must be either 'BUY' or 'SELL'")
680681

681-
# Generate a unique order ID
682-
order_id = f"ORD{int(time.time())}"
682+
# Generate a unique order ID with timestamp and a random component
683+
order_id = f"ORD{int(time.time())}_{random.randint(1000, 9999)}"
683684

684685
if order_type == "LIMIT":
685686
command = f"{cmd_prefix} {order_id},{symbol},{quantity},{price}"
@@ -702,14 +703,15 @@ def place_order(self, symbol: str, side: str, quantity: int,
702703
"time": current_time
703704
}
704705

705-
# Add to simulated orders list
706-
self.simulated_orders.append(new_order)
706+
# Add to simulated orders dictionary with order_id as key
707+
self.simulated_orders[order_id] = new_order
707708

708709
# Create simulated response
709710
response = f"TRADOK;{symbol};{order_id};SENT;{side};{quantity};{price};0;0;{quantity};SIMREF001;{command}"
710711

711712
if not parse:
712713
return response
714+
713715
return parse_order_response(response)
714716

715717
# Real mode - use actual API
@@ -748,14 +750,14 @@ def cancel_order(self, order_id: str, parse: bool = True) -> Union[Dict[str, Any
748750
if self.simulation_mode:
749751
# In simulation mode, find and update the order in our simulated list
750752
order_found = False
751-
for order in self.simulated_orders:
753+
for order in self.simulated_orders.values():
752754
if order["order_id"] == order_id:
753755
order["status"] = "CANCELLED"
754756
order_found = True
755757
break
756758

757759
if order_found:
758-
symbol = next((o["symbol"] for o in self.simulated_orders if o["order_id"] == order_id), "UNKNOWN")
760+
symbol = next((o["symbol"] for o in self.simulated_orders.values() if o["order_id"] == order_id), "UNKNOWN")
759761
response = f"TRADOK;{symbol};{order_id};CANCELLED;CANCEL;0;0;0;0;0;SIMREF002;REVORD {order_id}"
760762
else:
761763
response = "ERR;N/A;1020" # Order not found
@@ -830,7 +832,7 @@ def get_orders(self, parse: bool = True) -> Union[Dict[str, Any], str]:
830832
# Format order entries
831833
order_lines = []
832834

833-
for order in self.simulated_orders:
835+
for order in self.simulated_orders.values():
834836
line = f"ORDER;{order['symbol']};{order['time']};{order['order_id']};{order['side']};{order['price']};0;{order['quantity']};{order['status']}"
835837
order_lines.append(line)
836838

@@ -931,39 +933,52 @@ def get_connection_metrics(self) -> Dict[str, Any]:
931933
}
932934

933935
# Simulation helper methods
934-
def add_simulated_position(self, symbol: str, quantity: int, avg_price: float = 0, gain: float = 0) -> None:
936+
def add_simulated_position(self, symbol: str, quantity: int, price: float):
935937
"""
936-
Add a position to the simulated portfolio (simulation mode only)
938+
Add a position to the simulated portfolio.
939+
If the position already exists, update its quantity and average price.
937940
938941
Args:
939-
symbol: Symbol/ticker of the position
940-
quantity: Number of shares
941-
avg_price: Average purchase price
942-
gain: Unrealized gain/loss
942+
symbol: The symbol of the position
943+
quantity: The quantity to add (can be negative for selling)
944+
price: The price of the new position
943945
"""
944946
if not self.simulation_mode:
945947
self.logger.warning("add_simulated_position called but simulation mode is not active")
946948
return
947-
948-
# Check if position already exists
949+
950+
# Find if we already have this position
949951
for position in self.simulated_portfolio:
950952
if position["symbol"] == symbol:
951-
# Update existing position
952-
position["quantity"] += quantity
953-
if quantity > 0: # Only recalculate avg price on buys
954-
position["avg_price"] = avg_price
955-
position["gain"] = gain
956-
self.logger.info(f"Updated simulated position: {symbol}, quantity: {position['quantity']}")
957-
return
953+
# Calculate new average price based on quantities
954+
new_qty = position["quantity"] + quantity
955+
if new_qty <= 0:
956+
# Remove this position if quantity goes to 0 or negative
957+
self.simulated_portfolio = [p for p in self.simulated_portfolio if p["symbol"] != symbol]
958+
return
958959

959-
# Add new position
960-
self.simulated_portfolio.append({
960+
position["avg_price"] = ((position["quantity"] * position["avg_price"]) +
961+
(quantity * price)) / new_qty
962+
position["quantity"] = new_qty
963+
self.logger.debug(f"Updated simulated position: {position}")
964+
# Update total balance after position change
965+
self.update_simulated_total_balance()
966+
return
967+
968+
# New position
969+
if quantity <= 0:
970+
self.logger.warning(f"Tried to add a new position with quantity <= 0: {symbol}, {quantity}")
971+
return
972+
973+
new_position = {
961974
"symbol": symbol,
962975
"quantity": quantity,
963-
"avg_price": avg_price,
964-
"gain": gain
965-
})
966-
self.logger.info(f"Added simulated position: {symbol}, quantity: {quantity}")
976+
"avg_price": price
977+
}
978+
self.simulated_portfolio.append(new_position)
979+
self.logger.debug(f"Added new simulated position: {new_position}")
980+
# Update total balance after new position
981+
self.update_simulated_total_balance()
967982

968983
def remove_simulated_position(self, symbol: str) -> bool:
969984
"""
@@ -1009,69 +1024,152 @@ def update_simulated_account(self, liquidity: float = None, equity: float = None
10091024

10101025
self.logger.info(f"Updated simulated account: liquidity={self.simulated_account['liquidity']}, equity={self.simulated_account['equity']}")
10111026

1012-
def simulate_order_execution(self, order_id: str, executed_price: float = None,
1013-
executed_quantity: int = None) -> bool:
1027+
def fix_test(self):
1028+
"""Reset simulated account and portfolio for testing."""
1029+
if not self.simulation_mode:
1030+
logging.warning("fix_test called but simulation mode is not active")
1031+
return
1032+
1033+
# Reset to initial state
1034+
self.simulated_account = {
1035+
"account_code": "SIM1234", # Important for get_account_info
1036+
"broker_id": "DEMO",
1037+
"account_name": "Simulated Account",
1038+
"currency": "EUR",
1039+
"liquidity": 10000.0,
1040+
"equity": 10000.0,
1041+
"mtd": 0.0,
1042+
"ytd": 0.0,
1043+
"pl_daily": 0.0,
1044+
"pl_ytd": 0.0,
1045+
"total_balance": 10000.0,
1046+
}
1047+
self.simulated_portfolio = []
1048+
self.simulated_orders = {}
1049+
logging.info("Simulation state reset for testing")
1050+
return {"success": True, "data": "Simulation reset"}
1051+
1052+
def simulate_order_execution(self, order_req: Union[str, dict], fill_price: Optional[float] = None, executed_price: Optional[float] = None) -> dict:
10141053
"""
1015-
Simulate execution of an order (simulation mode only)
1054+
Simulates the execution of an order by updating the account balances and portfolio.
10161055
10171056
Args:
1018-
order_id: ID of the order to execute
1019-
executed_price: Execution price (if None, uses order price)
1020-
executed_quantity: Quantity executed (if None, uses full order quantity)
1021-
1057+
order_req: Either the order ID (string) or the order details (dictionary)
1058+
fill_price: Price at which the order is filled (if None, use the price from order_req)
1059+
executed_price: Alternative name for fill_price (for backward compatibility)
1060+
10221061
Returns:
1023-
True if order was found and executed, False otherwise
1062+
A dictionary containing the simulated response with execution details
10241063
"""
10251064
if not self.simulation_mode:
10261065
self.logger.warning("simulate_order_execution called but simulation mode is not active")
1027-
return False
1066+
return {"success": False, "error": "Simulation mode not active"}
1067+
1068+
# Use executed_price if provided (for backward compatibility)
1069+
if executed_price is not None:
1070+
fill_price = executed_price
10281071

1029-
for order in self.simulated_orders:
1030-
if order["order_id"] == order_id:
1031-
# Set execution details
1032-
exec_price = executed_price if executed_price is not None else order["price"]
1033-
exec_qty = executed_quantity if executed_quantity is not None else order["quantity"]
1034-
1035-
# Update order status
1036-
order["status"] = "EXECUTED"
1037-
order["executed_price"] = exec_price
1038-
order["executed_quantity"] = exec_qty
1039-
1040-
# Update portfolio
1041-
symbol = order["symbol"]
1042-
side = order["side"]
1043-
1044-
if side == "BUY":
1045-
# Add to portfolio
1046-
self.add_simulated_position(symbol, exec_qty, exec_price)
1072+
# If order_req is a string (order ID), find the order in simulated_orders
1073+
if isinstance(order_req, str):
1074+
order_id = order_req
1075+
found_order = None
1076+
for order in self.simulated_orders.values():
1077+
if order.get("order_id") == order_id:
1078+
found_order = order
1079+
order_req = order
1080+
break
1081+
else:
1082+
return {"success": False, "error": f"Order ID {order_id} not found"}
1083+
1084+
# Extract order details
1085+
symbol = order_req.get("symbol", "")
1086+
side = order_req.get("side", "").upper()
1087+
quantity = int(order_req.get("quantity", 0))
1088+
price = fill_price if fill_price is not None else float(order_req.get("price", 0))
1089+
1090+
# Check for required fields
1091+
if not all([symbol, side, quantity, price]):
1092+
return {"success": False, "error": "Missing required order fields"}
1093+
1094+
# Calculate order value
1095+
order_value = quantity * price
1096+
1097+
# Update account based on order type
1098+
if side == "BUY":
1099+
# Check if we have enough liquidity
1100+
if order_value > self.simulated_account.get("liquidity", 0):
1101+
return {"success": False, "error": "Insufficient funds for buy order"}
1102+
1103+
# Reduce liquidity
1104+
self.simulated_account["liquidity"] -= order_value
1105+
1106+
# Add position
1107+
self.add_simulated_position(symbol, quantity, price)
1108+
1109+
elif side == "SELL":
1110+
# Find the position
1111+
position_found = False
1112+
for position in self.simulated_portfolio:
1113+
if position["symbol"] == symbol:
1114+
if position["quantity"] < quantity:
1115+
return {"success": False, "error": "Insufficient shares for sell order"}
10471116

1048-
# Update account liquidity
1049-
new_liquidity = self.simulated_account["liquidity"] - (exec_price * exec_qty)
1050-
self.update_simulated_account(liquidity=new_liquidity)
1051-
elif side == "SELL":
1052-
# Find position
1053-
position_found = False
1054-
for position in self.simulated_portfolio:
1055-
if position["symbol"] == symbol:
1056-
position_found = True
1057-
# Reduce position
1058-
if position["quantity"] >= exec_qty:
1059-
position["quantity"] -= exec_qty
1060-
if position["quantity"] <= 0:
1061-
self.remove_simulated_position(symbol)
1062-
else:
1063-
self.logger.warning(f"Selling more shares ({exec_qty}) than in portfolio ({position['quantity']})")
1064-
break
1117+
# Increase liquidity (we sold shares)
1118+
self.simulated_account["liquidity"] += order_value
10651119

1066-
if not position_found:
1067-
self.logger.warning(f"Selling shares of {symbol} not in portfolio")
1068-
1069-
# Update account liquidity
1070-
new_liquidity = self.simulated_account["liquidity"] + (exec_price * exec_qty)
1071-
self.update_simulated_account(liquidity=new_liquidity)
1072-
1073-
self.logger.info(f"Simulated execution of order {order_id}: {exec_qty} shares at {exec_price}")
1074-
return True
1075-
1076-
self.logger.warning(f"Attempted to execute non-existent order: {order_id}")
1077-
return False
1120+
# Reduce position quantity
1121+
new_qty = position["quantity"] - quantity
1122+
if new_qty == 0:
1123+
# Remove position completely
1124+
self.simulated_portfolio = [p for p in self.simulated_portfolio if p["symbol"] != symbol]
1125+
else:
1126+
# Update quantity
1127+
position["quantity"] = new_qty
1128+
1129+
position_found = True
1130+
break
1131+
1132+
if not position_found:
1133+
return {"success": False, "error": f"Position {symbol} not found in portfolio"}
1134+
else:
1135+
return {"success": False, "error": f"Unsupported order side: {side}"}
1136+
1137+
# Update total balance after the operation
1138+
self.update_simulated_total_balance()
1139+
1140+
# Create a simulated execution response
1141+
execution_resp = {
1142+
"success": True,
1143+
"data": {
1144+
"order_id": f"sim-{int(time.time() * 1000)}",
1145+
"symbol": symbol,
1146+
"side": side,
1147+
"quantity": quantity,
1148+
"price": price,
1149+
"value": order_value,
1150+
"execution_time": datetime.datetime.now().isoformat(),
1151+
"account_status": {
1152+
"liquidity": self.simulated_account.get("liquidity", 0),
1153+
"portfolio_count": len(self.simulated_portfolio),
1154+
"total_balance": self.simulated_account.get("total_balance", 0)
1155+
}
1156+
}
1157+
}
1158+
1159+
self.logger.info(f"Simulated order execution: {side} {quantity} {symbol} @ {price}")
1160+
return execution_resp
1161+
1162+
def update_simulated_total_balance(self):
1163+
"""
1164+
Update the total balance of the simulated account based on cash liquidity and portfolio value.
1165+
"""
1166+
if not self.simulation_mode:
1167+
self.logger.warning("update_simulated_total_balance called but simulation mode is not active")
1168+
return
1169+
1170+
portfolio_value = 0.0
1171+
for position in self.simulated_portfolio:
1172+
portfolio_value += position["quantity"] * position.get("avg_price", 0)
1173+
1174+
self.simulated_account["total_balance"] = self.simulated_account["liquidity"] + portfolio_value
1175+
self.logger.info(f"Updated simulated total balance: {self.simulated_account['total_balance']}")

0 commit comments

Comments
 (0)