33import time
44import re
55import datetime
6+ import random
67from typing import Optional , Dict , List , Union , Tuple , Any
78
89from 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