Fix paper trade P/L updates and fee calculations
- Fix default fee from 0.1 (10%) to 0.001 (0.1%) in Trade class - Add real-time trade updates via WebSocket trade_update events - Fetch current market price for market orders instead of using cached price - Add trade persistence to database with proper schema - Add execution loop to update trades even without active strategies - Add frontend handler for trade_update events in communication.js - Add handleTradeUpdate method in trade.js for live P/L updates - Add debug file logging for trade update debugging - Update statistics dashboard and trading HUD templates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
78dfd71303
commit
f603728080
|
|
@ -44,7 +44,7 @@ class BrighterTrades:
|
|||
self.indicators = Indicators(self.candles, self.users, self.data)
|
||||
|
||||
# Object that maintains the trades data
|
||||
self.trades = Trades(self.users)
|
||||
self.trades = Trades(self.users, data_cache=self.data)
|
||||
# The Trades object needs to connect to an exchange_interface.
|
||||
self.trades.connect_exchanges(exchanges=self.exchanges)
|
||||
|
||||
|
|
@ -349,6 +349,12 @@ class BrighterTrades:
|
|||
price_updates = {symbol: float(cdata['close'])}
|
||||
trade_updates = self.trades.update(price_updates)
|
||||
|
||||
# Debug: log trade updates
|
||||
if self.trades.active_trades:
|
||||
logger.debug(f"Active trades: {list(self.trades.active_trades.keys())}")
|
||||
logger.debug(f"Price updates for symbol '{symbol}': {price_updates}")
|
||||
logger.debug(f"Trade updates returned: {trade_updates}")
|
||||
|
||||
# Update all active strategy instances with new candle data
|
||||
stg_updates = self.strategies.update(candle_data=cdata)
|
||||
|
||||
|
|
@ -1088,69 +1094,105 @@ class BrighterTrades:
|
|||
|
||||
return result
|
||||
|
||||
def close_trade(self, trade_id):
|
||||
def close_trade(self, trade_id: str, current_price: float = None) -> dict:
|
||||
"""
|
||||
Closes a trade identified by the given trade ID.
|
||||
|
||||
:param trade_id: The ID of the trade to be closed.
|
||||
:param current_price: Optional current price for settlement.
|
||||
:return: Dict with success status and trade info.
|
||||
"""
|
||||
if self.trades.is_valid_trade_id(trade_id):
|
||||
pass
|
||||
# self.trades.close_trade(trade_id)TODO
|
||||
# self.config.remove('trades', trade_id)
|
||||
print(f"Trade {trade_id} has been closed.")
|
||||
else:
|
||||
print(f"Invalid trade ID: {trade_id}. Unable to close the trade.")
|
||||
if not self.trades.is_valid_trade_id(trade_id):
|
||||
logger.warning(f"Invalid trade ID: {trade_id}. Unable to close the trade.")
|
||||
return {"success": False, "message": f"Invalid trade ID: {trade_id}"}
|
||||
|
||||
def received_new_trade(self, data: dict) -> dict | None:
|
||||
result = self.trades.close_trade(trade_id, current_price=current_price)
|
||||
if result.get('success'):
|
||||
logger.info(f"Trade {trade_id} has been closed.")
|
||||
else:
|
||||
logger.warning(f"Failed to close trade {trade_id}: {result.get('message')}")
|
||||
|
||||
return result
|
||||
|
||||
def received_new_trade(self, data: dict, user_id: int = None) -> dict:
|
||||
"""
|
||||
Called when a new trade has been defined and created in the UI.
|
||||
|
||||
:param data: A dictionary containing the attributes of the trade.
|
||||
:return: The details of the trade as a dictionary, or None on failure.
|
||||
:param user_id: The ID of the user creating the trade.
|
||||
:return: Dict with success status and trade data.
|
||||
"""
|
||||
|
||||
def vld(attr):
|
||||
def get_value(attr, default=None):
|
||||
"""
|
||||
Casts numeric strings to float before returning the attribute.
|
||||
Returns None if the attribute is absent in the data.
|
||||
Gets a value from data, casting numeric strings to float where appropriate.
|
||||
"""
|
||||
if attr in data and data[attr] != '':
|
||||
val = data.get(attr, default)
|
||||
if val is None or val == '':
|
||||
return default
|
||||
# Try to cast to float for numeric fields
|
||||
if attr in ['price', 'quantity']:
|
||||
try:
|
||||
return float(data[attr])
|
||||
except ValueError:
|
||||
return data[attr]
|
||||
else:
|
||||
return None
|
||||
return float(val)
|
||||
except (ValueError, TypeError):
|
||||
return val
|
||||
return val
|
||||
|
||||
# Get trade parameters
|
||||
target = get_value('target') or get_value('exchange_name', 'test_exchange')
|
||||
symbol = get_value('symbol') or get_value('trading_pair')
|
||||
price = get_value('price', 0.0)
|
||||
side = get_value('side', 'buy')
|
||||
order_type = get_value('orderType') or get_value('order_type', 'MARKET')
|
||||
quantity = get_value('quantity', 0.0)
|
||||
strategy_id = get_value('strategy_id')
|
||||
|
||||
# Validate required fields
|
||||
if not symbol:
|
||||
return {"success": False, "message": "Symbol is required."}
|
||||
if not quantity or float(quantity) <= 0:
|
||||
return {"success": False, "message": "Quantity must be greater than 0."}
|
||||
|
||||
# Forward the request to trades
|
||||
status, result = self.trades.new_trade(
|
||||
target=target,
|
||||
symbol=symbol,
|
||||
price=price,
|
||||
side=side,
|
||||
order_type=order_type,
|
||||
qty=quantity,
|
||||
user_id=user_id,
|
||||
strategy_id=strategy_id
|
||||
)
|
||||
|
||||
# Forward the request to trades.
|
||||
status, result = self.trades.new_trade(target=vld('exchange_name'), symbol=vld('symbol'), price=vld('price'),
|
||||
side=vld('side'), order_type=vld('orderType'),
|
||||
qty=vld('quantity'))
|
||||
if status == 'Error':
|
||||
print(f'Error placing the trade: {result}')
|
||||
return None
|
||||
logger.warning(f'Error placing the trade: {result}')
|
||||
return {"success": False, "message": result}
|
||||
|
||||
print(f'Trade order received: exchange_name={vld("exchange_name")}, '
|
||||
f'symbol={vld("symbol")}, '
|
||||
f'side={vld("side")}, '
|
||||
f'type={vld("orderType")}, '
|
||||
f'quantity={vld("quantity")}, '
|
||||
f'price={vld("price")}')
|
||||
|
||||
# Update config's list of trades and save to file.TODO
|
||||
# self.config.update_data('trades', self.trades.get_trades('dict'))
|
||||
logger.info(f'Trade order received: target={target}, symbol={symbol}, '
|
||||
f'side={side}, type={order_type}, quantity={quantity}, price={price}')
|
||||
|
||||
# Get the created trade
|
||||
trade_obj = self.trades.get_trade_by_id(result)
|
||||
if trade_obj:
|
||||
# Return the trade object that was created in a form that can be converted to json.
|
||||
return trade_obj.__dict__
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Trade created successfully.",
|
||||
"trade": trade_obj.to_json()
|
||||
}
|
||||
else:
|
||||
return None
|
||||
return {"success": False, "message": "Trade created but could not be retrieved."}
|
||||
|
||||
def get_trades(self):
|
||||
""" Return a JSON object of all the trades in the trades instance."""
|
||||
return self.trades.get_trades('dict')
|
||||
def get_trades(self, user_id: int = None):
|
||||
"""
|
||||
Return a JSON object of all the trades in the trades instance.
|
||||
|
||||
:param user_id: Optional user ID to filter trades.
|
||||
:return: List of trade dictionaries.
|
||||
"""
|
||||
if user_id is not None:
|
||||
return self.trades.get_trades_for_user(user_id, 'json')
|
||||
return self.trades.get_trades('json')
|
||||
|
||||
def delete_backtest(self, msg_data):
|
||||
""" Delete an existing backtest by interacting with the Backtester. """
|
||||
|
|
@ -1296,8 +1338,8 @@ class BrighterTrades:
|
|||
return standard_reply("strategies", strategies)
|
||||
|
||||
elif request_for == 'trades':
|
||||
if trades := self.get_trades():
|
||||
return standard_reply("trades", trades)
|
||||
trades = self.get_trades(user_id)
|
||||
return standard_reply("trades", trades if trades else [])
|
||||
else:
|
||||
print('Warning: Unhandled request!')
|
||||
print(msg_data)
|
||||
|
|
@ -1331,7 +1373,14 @@ class BrighterTrades:
|
|||
})
|
||||
|
||||
if msg_type == 'close_trade':
|
||||
self.close_trade(msg_data)
|
||||
trade_id = msg_data.get('trade_id') or msg_data.get('unique_id') or msg_data
|
||||
if isinstance(trade_id, dict):
|
||||
trade_id = trade_id.get('trade_id') or trade_id.get('unique_id')
|
||||
result = self.close_trade(str(trade_id))
|
||||
if result.get('success'):
|
||||
return standard_reply("trade_closed", result)
|
||||
else:
|
||||
return standard_reply("trade_error", result)
|
||||
|
||||
if msg_type == 'new_signal':
|
||||
result = self.received_new_signal(msg_data, user_id)
|
||||
|
|
@ -1365,8 +1414,11 @@ class BrighterTrades:
|
|||
return standard_reply("strategy_error", {"message": "Failed to edit strategy."})
|
||||
|
||||
if msg_type == 'new_trade':
|
||||
if r_data := self.received_new_trade(msg_data):
|
||||
return standard_reply("trade_created", r_data)
|
||||
result = self.received_new_trade(msg_data, user_id=user_id)
|
||||
if result.get('success'):
|
||||
return standard_reply("trade_created", result)
|
||||
else:
|
||||
return standard_reply("trade_error", result)
|
||||
|
||||
if msg_type == 'config_exchange':
|
||||
user = msg_data.get('user') or user_name
|
||||
|
|
|
|||
45
src/app.py
45
src/app.py
|
|
@ -23,6 +23,13 @@ from utils import sanitize_for_json # noqa: E402
|
|||
# Set up logging
|
||||
log_level_name = os.getenv('BRIGHTER_LOG_LEVEL', 'INFO').upper()
|
||||
log_level = getattr(logging, log_level_name, logging.INFO)
|
||||
|
||||
# Debug file logger for execution loop
|
||||
_loop_debug = logging.getLogger('loop_debug')
|
||||
_loop_debug.setLevel(logging.DEBUG)
|
||||
_loop_handler = logging.FileHandler('/home/rob/PycharmProjects/BrighterTrading/trade_debug.log', mode='a')
|
||||
_loop_handler.setFormatter(logging.Formatter('%(asctime)s - [LOOP] %(message)s'))
|
||||
_loop_debug.addHandler(_loop_handler)
|
||||
logging.basicConfig(level=log_level)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(logging.WARNING)
|
||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||
|
|
@ -145,6 +152,44 @@ def strategy_execution_loop():
|
|||
except Exception as e:
|
||||
logger.error(f"Error executing strategy {instance_key}: {e}", exc_info=True)
|
||||
|
||||
# Update active trades (runs every iteration, regardless of active strategies)
|
||||
_loop_debug.debug(f"Checking active_trades: {len(brighter_trades.trades.active_trades)} trades")
|
||||
if brighter_trades.trades.active_trades:
|
||||
_loop_debug.debug(f"Has active trades, getting prices...")
|
||||
try:
|
||||
symbols = set(trade.symbol for trade in brighter_trades.trades.active_trades.values())
|
||||
_loop_debug.debug(f"Symbols to fetch: {symbols}")
|
||||
price_updates = {}
|
||||
for symbol in symbols:
|
||||
try:
|
||||
price = brighter_trades.exchanges.get_price(symbol)
|
||||
_loop_debug.debug(f"Got price for {symbol}: {price}")
|
||||
if price:
|
||||
price_updates[symbol] = price
|
||||
except Exception as e:
|
||||
_loop_debug.debug(f"Failed to get price for {symbol}: {e}")
|
||||
logger.warning(f"Could not get price for {symbol}: {e}")
|
||||
|
||||
_loop_debug.debug(f"price_updates: {price_updates}")
|
||||
if price_updates:
|
||||
_loop_debug.debug(f"Calling brighter_trades.trades.update()")
|
||||
trade_updates = brighter_trades.trades.update(price_updates)
|
||||
_loop_debug.debug(f"trade_updates returned: {trade_updates}")
|
||||
if trade_updates:
|
||||
logger.debug(f"Trade updates (no active strategies): {trade_updates}")
|
||||
for update in trade_updates:
|
||||
trade_id = update.get('id')
|
||||
trade = brighter_trades.trades.active_trades.get(trade_id)
|
||||
_loop_debug.debug(f"Emitting update for trade_id={trade_id}, creator={trade.creator if trade else None}")
|
||||
if trade and trade.creator:
|
||||
user_name = brighter_trades.users.get_username(user_id=trade.creator)
|
||||
if user_name:
|
||||
socketio.emit('trade_update', sanitize_for_json(update), room=user_name)
|
||||
_loop_debug.debug(f"Emitted trade_update to room={user_name}")
|
||||
except Exception as e:
|
||||
_loop_debug.debug(f"Exception in trade update: {e}")
|
||||
logger.error(f"Error updating trades (no strategies): {e}", exc_info=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Strategy execution loop error: {e}")
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,12 @@ class Comms {
|
|||
console.log('Strategy events received:', data);
|
||||
this.emit('strategy_events', data);
|
||||
});
|
||||
|
||||
// Handle trade update events from execution loop
|
||||
this.socket.on('trade_update', (data) => {
|
||||
console.log('Trade update received:', data);
|
||||
this.emit('trade_update', data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -333,7 +339,8 @@ class Comms {
|
|||
high: candlestick.h,
|
||||
low: candlestick.l,
|
||||
close: candlestick.c,
|
||||
vol: candlestick.v
|
||||
vol: candlestick.v,
|
||||
symbol: tradingPair // Include trading pair for trade matching
|
||||
};
|
||||
this.candleUpdate(newCandle);
|
||||
|
||||
|
|
|
|||
1034
src/static/trade.js
1034
src/static/trade.js
File diff suppressed because it is too large
Load Diff
|
|
@ -1,47 +1,50 @@
|
|||
<div class="form-popup" id="new_trade_form">
|
||||
<form action="/new_trade" class="form-container">
|
||||
<!-- Panel 1 of 1 (8 rows, 4 columns) -->
|
||||
<div id="trade_pan_1" class="form_panels" style="display: grid;grid-template-columns:repeat(4,1fr);grid-template-rows: repeat(8,1fr);">
|
||||
<!-- Panel title (row 1/8)-->
|
||||
<!-- Panel 1 of 1 (8 rows, 2 columns) -->
|
||||
<div id="trade_pan_1" class="form_panels" style="display: grid; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(8, 1fr); gap: 10px;">
|
||||
<!-- Panel title (row 1) -->
|
||||
<h1 style="grid-column: 1 / span 2; grid-row: 1;">Create New Trade</h1>
|
||||
<!-- Target input field (row 2/8)-->
|
||||
<div id = "tradeTarget_div" style="grid-column: 1 / span 2; grid-row:2;">
|
||||
<label for='tradeTarget' >Trade target:</label>
|
||||
|
||||
<!-- Trade Mode Selection (row 2) -->
|
||||
<label for="tradeTarget" style="grid-column: 1; grid-row: 2;"><b>Trade Mode:</b></label>
|
||||
<select name="tradeTarget" id="tradeTarget" style="grid-column: 2; grid-row: 2;">
|
||||
<option>test_exchange</option>
|
||||
<option>binance</option>
|
||||
<option value="test_exchange">Paper Trade</option>
|
||||
<option value="binance">Binance (Live)</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Side Input field (row 3/8)-->
|
||||
|
||||
<!-- Side Input field (row 3) -->
|
||||
<label for="side" style="grid-column: 1; grid-row: 3;"><b>Side:</b></label>
|
||||
<select name="side" id="side" style="grid-column: 2; grid-row: 3;">
|
||||
<option>buy</option>
|
||||
<option>sell</option>
|
||||
<option value="buy">Buy</option>
|
||||
<option value="sell">Sell</option>
|
||||
</select>
|
||||
<!-- orderType Input field (row 4/8)-->
|
||||
<label for="orderType" style="grid-column: 1; grid-row: 4;"><b>Order type:</b></label>
|
||||
|
||||
<!-- Order Type Input field (row 4) -->
|
||||
<label for="orderType" style="grid-column: 1; grid-row: 4;"><b>Order Type:</b></label>
|
||||
<select name="orderType" id="orderType" style="grid-column: 2; grid-row: 4;">
|
||||
<option>MARKET</option>
|
||||
<option>LIMIT</option>
|
||||
<option value="MARKET">Market</option>
|
||||
<option value="LIMIT">Limit</option>
|
||||
</select>
|
||||
<!-- Price Input field (row 5/8)-->
|
||||
|
||||
<!-- Price Input field (row 5) -->
|
||||
<label id="price-label" for="price" style="grid-column: 1; grid-row: 5;"><b>Price:</b></label>
|
||||
<input type="number" min="0" value="0.1" step="0.1" name="price" id="price" style="grid-column: 2; grid-row: 5;" >
|
||||
<output name="currentPrice" id="currentPrice" style="grid-column: 2; grid-row: 5;" >
|
||||
</output>
|
||||
<!-- quantity Input field (row 6/8)-->
|
||||
<input type="number" min="0" value="0.1" step="0.00000001" name="price" id="price" style="grid-column: 2; grid-row: 5; display: none;">
|
||||
<output name="currentPrice" id="currentPrice" style="grid-column: 2; grid-row: 5;"></output>
|
||||
|
||||
<!-- Quantity Input field (row 6) -->
|
||||
<label for="quantity" style="grid-column: 1; grid-row: 6;"><b>Quantity:</b></label>
|
||||
<input type="number" min="0" value="0" step="0.01" name="quantity" id="quantity" style="grid-column: 2; grid-row: 6;" >
|
||||
</input>
|
||||
<!-- Value field (row 7/8)-->
|
||||
<label for="tradeValue" style="grid-column: 1; grid-row: 7;"><b>Value</b></label>
|
||||
<output name="tradeValue" id="tradeValue" for="quantity price" style="grid-column: 2; grid-row: 7;"></output>
|
||||
<!-- buttons (row 8/8)-->
|
||||
<div style="grid-column: 1 / span 4; grid-row: 8;">
|
||||
<input type="number" min="0" value="0" step="0.00000001" name="quantity" id="quantity" style="grid-column: 2; grid-row: 6;">
|
||||
|
||||
<!-- Value field (row 7) -->
|
||||
<label for="tradeValue" style="grid-column: 1; grid-row: 7;"><b>Est. Value:</b></label>
|
||||
<output name="tradeValue" id="tradeValue" for="quantity price" style="grid-column: 2; grid-row: 7;">0</output>
|
||||
|
||||
<!-- Buttons (row 8) -->
|
||||
<div style="grid-column: 1 / span 2; grid-row: 8;">
|
||||
<button type="button" class="btn cancel" onclick="UI.trade.close_tradeForm()">Close</button>
|
||||
<button type="button" class="btn next" onclick="UI.trade.submitNewTrade()">Create Trade</button>
|
||||
</div>
|
||||
|
||||
</div><!----End panel 1--------->
|
||||
</div><!-- End panel 1 -->
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,29 @@
|
|||
<div class="content" id="statistics_content">
|
||||
<!-- Active Trades Section -->
|
||||
<div class="stats-section">
|
||||
<h4>Active Trades</h4>
|
||||
<div class="trades-summary-grid">
|
||||
<div class="trades-summary-item">
|
||||
<span class="trades-summary-label">Total</span>
|
||||
<span class="trades-summary-value" id="stat_active_trades">0</span>
|
||||
</div>
|
||||
<div class="trades-summary-item">
|
||||
<span class="trades-summary-label">Paper</span>
|
||||
<span class="trades-summary-value paper" id="stat_paper_trades">0</span>
|
||||
</div>
|
||||
<div class="trades-summary-item">
|
||||
<span class="trades-summary-label">Live</span>
|
||||
<span class="trades-summary-value live" id="stat_live_trades">0</span>
|
||||
</div>
|
||||
<div class="trades-summary-item">
|
||||
<span class="trades-summary-label">Total P/L</span>
|
||||
<span class="trades-summary-value" id="stat_trades_pl">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Running Strategies Section -->
|
||||
<div class="stats-section">
|
||||
<h4>Running Strategies</h4>
|
||||
|
|
@ -94,6 +119,52 @@
|
|||
margin: 5px 0;
|
||||
}
|
||||
|
||||
/* Trades summary grid */
|
||||
.trades-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 5px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.trades-summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.trades-summary-label {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.trades-summary-value {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.trades-summary-value.paper {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
.trades-summary-value.live {
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.trades-summary-value.positive {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.trades-summary-value.negative {
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
/* Running strategies list */
|
||||
.running-strategy-item {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,211 @@
|
|||
<div id="trade_content" class="content">
|
||||
<button class="btn" id="new_trade" onclick="UI.trade.open_tradeForm()">New Trade</button>
|
||||
<hr>
|
||||
<h3>Trades</h3>
|
||||
<div><ul id="activeTradesLst"></ul></div>
|
||||
<h3>Active Trades</h3>
|
||||
<div id="tradesContainer" class="trades-container">
|
||||
<p class="no-data-msg">No active trades</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.trades-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 5px 0;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.trade-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 140px;
|
||||
max-width: 180px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
|
||||
border: 2px solid #ccc;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trade-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Paper trade styling */
|
||||
.trade-card.trade-paper {
|
||||
border-style: dashed;
|
||||
border-color: #9e9e9e;
|
||||
background: linear-gradient(135deg, #fafafa, #f0f0f0);
|
||||
}
|
||||
|
||||
.trade-paper-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 10px;
|
||||
background: #9e9e9e;
|
||||
color: white;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Side-based accent colors */
|
||||
.trade-card.trade-buy {
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
.trade-card.trade-sell {
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
|
||||
/* Close button */
|
||||
.trade-close-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 2px 5px;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.trade-card:hover .trade-close-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.trade-close-btn:hover {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
/* Trade info */
|
||||
.trade-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.trade-symbol-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.trade-side {
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.trade-side.buy {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.trade-side.sell {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.trade-symbol {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.trade-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.trade-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.trade-value {
|
||||
font-weight: 500;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* P/L colors */
|
||||
.trade-pl.positive {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.trade-pl.negative {
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
/* P/L flash animation */
|
||||
@keyframes plFlash {
|
||||
0% { background: rgba(255,235,59,0.5); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
|
||||
.trade-pl-flash {
|
||||
animation: plFlash 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hover panel */
|
||||
.trade-hover {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
min-width: 200px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.trade-card:hover .trade-hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.trade-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.trade-paper-indicator {
|
||||
color: #9e9e9e;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Removing animation */
|
||||
.trade-card-removing {
|
||||
animation: cardRemove 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes cardRemove {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-msg {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
423
src/trade.py
423
src/trade.py
|
|
@ -1,5 +1,7 @@
|
|||
import json
|
||||
import logging
|
||||
import uuid
|
||||
import datetime as dt
|
||||
from typing import Any
|
||||
|
||||
from Users import Users
|
||||
|
|
@ -7,16 +9,26 @@ from Users import Users
|
|||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Debug file logger for trade updates
|
||||
_debug_logger = logging.getLogger('trade_debug')
|
||||
_debug_logger.setLevel(logging.DEBUG)
|
||||
_debug_handler = logging.FileHandler('/home/rob/PycharmProjects/BrighterTrading/trade_debug.log', mode='w')
|
||||
_debug_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
|
||||
_debug_logger.addHandler(_debug_handler)
|
||||
|
||||
|
||||
class Trade:
|
||||
def __init__(self, target: str, symbol: str, side: str, order_price: float, base_order_qty: float,
|
||||
order_type: str = 'MARKET', time_in_force: str = 'GTC', unique_id: str | None = None,
|
||||
status: str | None = None, stats: dict[str, Any] | None = None,
|
||||
order: Any | None = None, fee: float = 0.1, strategy_id: str | None = None):
|
||||
order: Any | None = None, fee: float = 0.001, strategy_id: str | None = None,
|
||||
is_paper: bool = False, creator: int | None = None, created_at: str | None = None,
|
||||
tbl_key: str | None = None):
|
||||
"""
|
||||
Initializes a Trade instance with all necessary attributes.
|
||||
"""
|
||||
self.unique_id = unique_id or uuid.uuid4().hex
|
||||
self.tbl_key = tbl_key or self.unique_id
|
||||
self.target = target
|
||||
self.symbol = symbol
|
||||
self.side = side.upper()
|
||||
|
|
@ -26,6 +38,9 @@ class Trade:
|
|||
self.base_order_qty = base_order_qty
|
||||
self.fee = fee
|
||||
self.strategy_id = strategy_id
|
||||
self.is_paper = is_paper
|
||||
self.creator = creator
|
||||
self.created_at = created_at or dt.datetime.now(dt.timezone.utc).isoformat()
|
||||
|
||||
if status is None:
|
||||
self.status = 'inactive'
|
||||
|
|
@ -54,6 +69,7 @@ class Trade:
|
|||
"""
|
||||
return {
|
||||
'unique_id': self.unique_id,
|
||||
'tbl_key': self.tbl_key,
|
||||
'strategy_id': self.strategy_id,
|
||||
'target': self.target,
|
||||
'symbol': self.symbol,
|
||||
|
|
@ -64,7 +80,10 @@ class Trade:
|
|||
'time_in_force': self.time_in_force,
|
||||
'status': self.status,
|
||||
'stats': self.stats,
|
||||
'order': self.order
|
||||
'order': self.order,
|
||||
'is_paper': self.is_paper,
|
||||
'creator': self.creator,
|
||||
'created_at': self.created_at
|
||||
}
|
||||
|
||||
def get_position_size(self) -> float:
|
||||
|
|
@ -186,13 +205,17 @@ class Trade:
|
|||
|
||||
|
||||
class Trades:
|
||||
def __init__(self, users: Users):
|
||||
def __init__(self, users: Users, data_cache: Any = None):
|
||||
"""
|
||||
Initializes the Trades class with necessary attributes.
|
||||
|
||||
:param users: Users instance for user lookups.
|
||||
:param data_cache: DataCache instance for persistence.
|
||||
"""
|
||||
self.users = users
|
||||
self.data_cache = data_cache
|
||||
self.exchange_interface: Any | None = None # Define the type based on your exchange interface
|
||||
self.exchange_fees = {'maker': 0.01, 'taker': 0.05}
|
||||
self.exchange_fees = {'maker': 0.001, 'taker': 0.001}
|
||||
self.hedge_mode = False
|
||||
self.side: str | None = None
|
||||
self.active_trades: dict[str, Trade] = {} # Keyed by trade.unique_id
|
||||
|
|
@ -201,6 +224,308 @@ class Trades:
|
|||
self.balances: dict[str, float] = {} # Track balances per strategy
|
||||
self.locked_funds: dict[str, float] = {} # Track locked funds per strategy
|
||||
|
||||
# Initialize database persistence if data_cache is available
|
||||
if self.data_cache:
|
||||
self._ensure_table_exists()
|
||||
self._create_cache()
|
||||
self._load_trades_from_db()
|
||||
|
||||
def _ensure_table_exists(self) -> None:
|
||||
"""Create the trades table in the database if it doesn't exist."""
|
||||
try:
|
||||
if not self.data_cache.db.table_exists('trades'):
|
||||
create_sql = """
|
||||
CREATE TABLE IF NOT EXISTS trades (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator INTEGER,
|
||||
unique_id TEXT UNIQUE,
|
||||
target TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
side TEXT NOT NULL,
|
||||
order_type TEXT NOT NULL,
|
||||
order_price REAL,
|
||||
base_order_qty REAL NOT NULL,
|
||||
time_in_force TEXT DEFAULT 'GTC',
|
||||
fee REAL DEFAULT 0.1,
|
||||
status TEXT DEFAULT 'inactive',
|
||||
stats_json TEXT,
|
||||
strategy_id TEXT,
|
||||
is_paper INTEGER DEFAULT 0,
|
||||
created_at TEXT,
|
||||
tbl_key TEXT UNIQUE
|
||||
)
|
||||
"""
|
||||
self.data_cache.db.execute_sql(create_sql, params=[])
|
||||
logger.info("Created trades table in database")
|
||||
except Exception as e:
|
||||
logger.error(f"Error ensuring trades table exists: {e}", exc_info=True)
|
||||
|
||||
def _create_cache(self) -> None:
|
||||
"""Create the trades cache in DataCache."""
|
||||
try:
|
||||
self.data_cache.create_cache(
|
||||
name='trades',
|
||||
cache_type='table',
|
||||
size_limit=1000,
|
||||
eviction_policy='deny',
|
||||
default_expiration=dt.timedelta(hours=24),
|
||||
columns=[
|
||||
"creator",
|
||||
"unique_id",
|
||||
"target",
|
||||
"symbol",
|
||||
"side",
|
||||
"order_type",
|
||||
"order_price",
|
||||
"base_order_qty",
|
||||
"time_in_force",
|
||||
"fee",
|
||||
"status",
|
||||
"stats_json",
|
||||
"strategy_id",
|
||||
"is_paper",
|
||||
"created_at",
|
||||
"tbl_key"
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Cache 'trades' may already exist: {e}")
|
||||
|
||||
def _load_trades_from_db(self) -> None:
|
||||
"""Load all active trades from database into memory."""
|
||||
try:
|
||||
trades_df = self.data_cache.get_all_rows_from_datacache(cache_name='trades')
|
||||
if trades_df is not None and not trades_df.empty:
|
||||
for _, row in trades_df.iterrows():
|
||||
# Only load non-closed trades
|
||||
status = row.get('status', 'inactive')
|
||||
if status == 'closed':
|
||||
continue
|
||||
|
||||
# Parse stats JSON
|
||||
stats_json = row.get('stats_json', '{}')
|
||||
try:
|
||||
stats = json.loads(stats_json) if stats_json else {}
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
stats = {}
|
||||
|
||||
trade = Trade(
|
||||
target=row.get('target', ''),
|
||||
symbol=row.get('symbol', ''),
|
||||
side=row.get('side', 'BUY'),
|
||||
order_price=float(row.get('order_price', 0)),
|
||||
base_order_qty=float(row.get('base_order_qty', 0)),
|
||||
order_type=row.get('order_type', 'MARKET'),
|
||||
time_in_force=row.get('time_in_force', 'GTC'),
|
||||
unique_id=row.get('unique_id'),
|
||||
status=status,
|
||||
stats=stats if stats else None,
|
||||
fee=float(row.get('fee', 0.001)),
|
||||
strategy_id=row.get('strategy_id'),
|
||||
is_paper=bool(row.get('is_paper', 0)),
|
||||
creator=row.get('creator'),
|
||||
created_at=row.get('created_at'),
|
||||
tbl_key=row.get('tbl_key')
|
||||
)
|
||||
self.active_trades[trade.unique_id] = trade
|
||||
self.stats['num_trades'] += 1
|
||||
logger.info(f"Loaded {len(self.active_trades)} active trades from database")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading trades from database: {e}", exc_info=True)
|
||||
|
||||
def _save_trade(self, trade: Trade) -> bool:
|
||||
"""
|
||||
Save a trade to the database.
|
||||
|
||||
:param trade: Trade object to save.
|
||||
:return: True if successful, False otherwise.
|
||||
"""
|
||||
if not self.data_cache:
|
||||
return True # No persistence, just return success
|
||||
|
||||
try:
|
||||
columns = (
|
||||
"creator", "unique_id", "target", "symbol", "side", "order_type",
|
||||
"order_price", "base_order_qty", "time_in_force", "fee", "status",
|
||||
"stats_json", "strategy_id", "is_paper", "created_at", "tbl_key"
|
||||
)
|
||||
|
||||
stats_json = json.dumps(trade.stats) if trade.stats else '{}'
|
||||
|
||||
values = (
|
||||
trade.creator,
|
||||
trade.unique_id,
|
||||
trade.target,
|
||||
trade.symbol,
|
||||
trade.side,
|
||||
trade.order_type,
|
||||
trade.order_price,
|
||||
trade.base_order_qty,
|
||||
trade.time_in_force,
|
||||
trade.fee,
|
||||
trade.status,
|
||||
stats_json,
|
||||
trade.strategy_id,
|
||||
int(trade.is_paper),
|
||||
trade.created_at,
|
||||
trade.tbl_key
|
||||
)
|
||||
|
||||
# Check if trade already exists
|
||||
existing = self.data_cache.get_rows_from_datacache(
|
||||
cache_name='trades',
|
||||
filter_vals=[('tbl_key', trade.tbl_key)],
|
||||
include_tbl_key=True
|
||||
)
|
||||
|
||||
if existing.empty:
|
||||
# Insert new trade
|
||||
self.data_cache.insert_row_into_datacache(
|
||||
cache_name='trades',
|
||||
columns=columns,
|
||||
values=values
|
||||
)
|
||||
else:
|
||||
# Update existing trade
|
||||
self.data_cache.modify_datacache_item(
|
||||
cache_name='trades',
|
||||
filter_vals=[('tbl_key', trade.tbl_key)],
|
||||
field_names=columns,
|
||||
new_values=values,
|
||||
key=trade.tbl_key,
|
||||
overwrite='tbl_key'
|
||||
)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save trade {trade.unique_id}: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def _update_trade_in_db(self, trade: Trade) -> bool:
|
||||
"""
|
||||
Update a trade's stats in the database.
|
||||
|
||||
:param trade: Trade object to update.
|
||||
:return: True if successful, False otherwise.
|
||||
"""
|
||||
return self._save_trade(trade)
|
||||
|
||||
def _delete_trade_from_db(self, trade_id: str) -> bool:
|
||||
"""
|
||||
Delete a trade from the database.
|
||||
|
||||
:param trade_id: The unique ID of the trade to delete.
|
||||
:return: True if successful, False otherwise.
|
||||
"""
|
||||
if not self.data_cache:
|
||||
return True
|
||||
|
||||
try:
|
||||
self.data_cache.remove_row_from_datacache(
|
||||
cache_name='trades',
|
||||
filter_vals=[('unique_id', trade_id)]
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete trade {trade_id}: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def new_trade(self, target: str, symbol: str, price: float, side: str,
|
||||
order_type: str, qty: float, user_id: int = None,
|
||||
strategy_id: str = None) -> tuple[str, str | None]:
|
||||
"""
|
||||
Creates a new trade (paper or live).
|
||||
|
||||
:param target: The exchange target ('test_exchange' for paper, exchange name for live).
|
||||
:param symbol: The trading pair symbol.
|
||||
:param price: The price to trade at (ignored for market orders).
|
||||
:param side: 'BUY' or 'SELL'.
|
||||
:param order_type: 'MARKET' or 'LIMIT'.
|
||||
:param qty: The quantity to trade.
|
||||
:param user_id: The user creating the trade.
|
||||
:param strategy_id: Optional strategy ID if from a strategy.
|
||||
:return: Tuple of (status, trade_id or error message).
|
||||
"""
|
||||
# Determine if this is a paper trade
|
||||
is_paper = target in ['test_exchange', 'paper', 'Paper Trade']
|
||||
|
||||
# For market orders, fetch the current price from exchange
|
||||
effective_price = float(price) if price else 0.0
|
||||
if order_type and order_type.upper() == 'MARKET' and self.exchange_interface:
|
||||
try:
|
||||
current_price = self.exchange_interface.get_price(symbol)
|
||||
if current_price:
|
||||
effective_price = float(current_price)
|
||||
logger.debug(f"Market order: using current price {effective_price} for {symbol}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not fetch current price for {symbol}: {e}, using provided price {price}")
|
||||
|
||||
try:
|
||||
trade = Trade(
|
||||
target=target,
|
||||
symbol=symbol,
|
||||
side=side.upper(),
|
||||
order_price=effective_price,
|
||||
base_order_qty=float(qty),
|
||||
order_type=order_type.upper() if order_type else 'MARKET',
|
||||
strategy_id=strategy_id,
|
||||
is_paper=is_paper,
|
||||
creator=user_id
|
||||
)
|
||||
|
||||
if is_paper:
|
||||
# Paper trade: simulate immediate fill
|
||||
trade.status = 'filled'
|
||||
trade.stats['qty_filled'] = trade.base_order_qty
|
||||
trade.stats['opening_price'] = trade.order_price
|
||||
trade.stats['opening_value'] = trade.base_order_qty * trade.order_price
|
||||
trade.stats['current_value'] = trade.stats['opening_value']
|
||||
logger.info(f"Paper trade created: {trade.unique_id} {side} {qty} {symbol} @ {effective_price}")
|
||||
else:
|
||||
# Live trade: place order on exchange
|
||||
if not self.exchange_connected():
|
||||
return 'Error', 'No exchange connected'
|
||||
|
||||
user_name = self._get_user_name(user_id) if user_id else 'unknown'
|
||||
status, msg = self.place_order(trade, user_name=user_name)
|
||||
if status != 'success':
|
||||
return 'Error', msg
|
||||
|
||||
# Add to active trades
|
||||
self.active_trades[trade.unique_id] = trade
|
||||
self.stats['num_trades'] += 1
|
||||
|
||||
# Persist to database
|
||||
self._save_trade(trade)
|
||||
|
||||
return 'Success', trade.unique_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating new trade: {e}", exc_info=True)
|
||||
return 'Error', str(e)
|
||||
|
||||
def get_trades_for_user(self, user_id: int, form: str = 'json') -> list:
|
||||
"""
|
||||
Returns trades visible to a specific user.
|
||||
|
||||
:param user_id: The user ID to filter trades for.
|
||||
:param form: Output format ('json', 'obj', 'dict').
|
||||
:return: List of trades.
|
||||
"""
|
||||
user_trades = [
|
||||
trade for trade in self.active_trades.values()
|
||||
if trade.creator == user_id or trade.creator is None
|
||||
]
|
||||
|
||||
if form == 'obj':
|
||||
return user_trades
|
||||
elif form == 'json':
|
||||
return [trade.to_json() for trade in user_trades]
|
||||
elif form == 'dict':
|
||||
return [trade.__dict__ for trade in user_trades]
|
||||
else:
|
||||
return [trade.to_json() for trade in user_trades]
|
||||
|
||||
def buy(self, order_data: dict[str, Any], user_id: int) -> tuple[str, str | None]:
|
||||
"""
|
||||
Executes a buy order.
|
||||
|
|
@ -520,16 +845,37 @@ class Trades:
|
|||
:param price_updates: Dictionary mapping symbols to their current prices.
|
||||
:return: List of dictionaries containing updated trade data.
|
||||
"""
|
||||
_debug_logger.debug(f"=== Trades.update() called ===")
|
||||
_debug_logger.debug(f"price_updates: {price_updates}")
|
||||
_debug_logger.debug(f"active_trades count: {len(self.active_trades)}")
|
||||
_debug_logger.debug(f"active_trades keys: {list(self.active_trades.keys())}")
|
||||
|
||||
r_update = []
|
||||
|
||||
for trade_id, trade in list(self.active_trades.items()):
|
||||
symbol = trade.symbol
|
||||
_debug_logger.debug(f"Processing trade_id={trade_id}, symbol={symbol}, status={trade.status}")
|
||||
current_price = price_updates.get(symbol)
|
||||
_debug_logger.debug(f"current_price from get(): {current_price}")
|
||||
|
||||
if current_price is None:
|
||||
logger.warning(f"No price update provided for symbol '{symbol}'. Skipping trade {trade_id}.")
|
||||
# Try to find a matching symbol (handle format differences like BTC/USD vs BTC/USDT)
|
||||
for price_symbol, price in price_updates.items():
|
||||
# Normalize both symbols for comparison
|
||||
norm_trade = symbol.upper().replace('/', '')
|
||||
norm_price = price_symbol.upper().replace('/', '')
|
||||
if norm_trade == norm_price or norm_trade.rstrip('T') == norm_price.rstrip('T'):
|
||||
current_price = price
|
||||
logger.debug(f"Matched trade symbol '{symbol}' to price symbol '{price_symbol}'")
|
||||
break
|
||||
|
||||
if current_price is None:
|
||||
_debug_logger.debug(f"current_price is None after matching, skipping trade {trade_id}")
|
||||
logger.warning(f"No price update for symbol '{symbol}'. Available: {list(price_updates.keys())}. Skipping trade {trade_id}.")
|
||||
continue
|
||||
|
||||
_debug_logger.debug(f"current_price resolved to: {current_price}")
|
||||
_debug_logger.debug(f"Checking trade.status: '{trade.status}' in ['unfilled', 'part-filled']")
|
||||
if trade.status in ['unfilled', 'part-filled']:
|
||||
status = self.exchange_interface.get_trade_status(trade)
|
||||
if status in ['FILLED', 'PARTIALLY_FILLED']:
|
||||
|
|
@ -559,57 +905,102 @@ class Trades:
|
|||
trade.status = status.lower()
|
||||
continue # Skip further processing for this trade
|
||||
|
||||
_debug_logger.debug(f"Checking if trade.status == 'inactive': {trade.status == 'inactive'}")
|
||||
if trade.status == 'inactive':
|
||||
_debug_logger.debug(f"Trade {trade_id} is inactive, skipping")
|
||||
logger.error(f"Trades:update() - inactive trade encountered: {trade_id}")
|
||||
continue # Skip processing for inactive trades
|
||||
|
||||
_debug_logger.debug(f"Calling trade.update({current_price})")
|
||||
trade.update(current_price)
|
||||
trade_status = trade.status
|
||||
_debug_logger.debug(f"After trade.update(), trade_status={trade_status}, trade.stats={trade.stats}")
|
||||
|
||||
if trade_status in ['updated', 'filled', 'part-filled']:
|
||||
r_update.append({
|
||||
update_data = {
|
||||
'status': trade_status,
|
||||
'id': trade.unique_id,
|
||||
'pl': trade.stats.get('profit', 0.0),
|
||||
'pl_pct': trade.stats.get('profit_pct', 0.0)
|
||||
})
|
||||
}
|
||||
r_update.append(update_data)
|
||||
_debug_logger.debug(f"Appended update_data: {update_data}")
|
||||
logger.info(f"Trade {trade_id} updated: price={current_price}, P/L={update_data['pl']:.2f} ({update_data['pl_pct']:.2f}%)")
|
||||
else:
|
||||
_debug_logger.debug(f"trade_status '{trade_status}' not in update list, appending minimal data")
|
||||
r_update.append({'id': trade.unique_id, 'status': trade_status})
|
||||
|
||||
_debug_logger.debug(f"=== Trades.update() returning: {r_update} ===")
|
||||
return r_update
|
||||
|
||||
def close_trade(self, trade_id: str) -> bool:
|
||||
def close_trade(self, trade_id: str, current_price: float = None) -> dict:
|
||||
"""
|
||||
Closes a specific trade by settling it.
|
||||
|
||||
:param trade_id: The unique ID of the trade.
|
||||
:return: True if successful, False otherwise.
|
||||
:param current_price: Optional current price (used for paper trades).
|
||||
:return: Dict with success status and trade info.
|
||||
"""
|
||||
trade = self.get_trade_by_id(trade_id)
|
||||
if not trade:
|
||||
logger.error(f"close_trade(): Trade ID {trade_id} not found.")
|
||||
return False
|
||||
return {"success": False, "message": f"Trade {trade_id} not found."}
|
||||
|
||||
if trade.status == 'closed':
|
||||
logger.warning(f"close_trade(): Trade ID {trade_id} is already closed.")
|
||||
return False
|
||||
return {"success": False, "message": f"Trade {trade_id} is already closed."}
|
||||
|
||||
try:
|
||||
# Fetch the current price from the exchange
|
||||
# Get current price
|
||||
if current_price is None:
|
||||
if trade.is_paper:
|
||||
# For paper trades without a price, use the last known current price
|
||||
current_price = trade.stats.get('current_price', trade.order_price)
|
||||
elif self.exchange_interface:
|
||||
current_price = self.exchange_interface.get_price(trade.symbol)
|
||||
else:
|
||||
current_price = trade.stats.get('current_price', trade.order_price)
|
||||
|
||||
# Settle the trade
|
||||
trade.settle(qty=trade.base_order_qty, price=current_price)
|
||||
|
||||
# Calculate final P/L
|
||||
final_pl = trade.stats.get('profit', 0.0)
|
||||
final_pl_pct = trade.stats.get('profit_pct', 0.0)
|
||||
|
||||
# Move from active to settled
|
||||
if trade.status == 'closed':
|
||||
del self.active_trades[trade_id]
|
||||
self.settled_trades[trade_id] = trade
|
||||
self.stats['num_trades'] -= 1
|
||||
logger.info(f"Trade {trade_id} has been closed.")
|
||||
return True
|
||||
|
||||
# Update database - either delete or mark as closed
|
||||
if self.data_cache:
|
||||
self._save_trade(trade)
|
||||
|
||||
logger.info(f"Trade {trade_id} closed. P/L: {final_pl:.2f} ({final_pl_pct:.2f}%)")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Trade closed successfully.",
|
||||
"trade_id": trade_id,
|
||||
"final_pl": final_pl,
|
||||
"final_pl_pct": final_pl_pct,
|
||||
"settled_price": current_price
|
||||
}
|
||||
else:
|
||||
# Partial settlement
|
||||
self._save_trade(trade)
|
||||
logger.info(f"Trade {trade_id} partially settled.")
|
||||
return True
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Trade partially settled.",
|
||||
"trade_id": trade_id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing trade '{trade_id}': {e}", exc_info=True)
|
||||
return False
|
||||
return {"success": False, "message": f"Error closing trade: {str(e)}"}
|
||||
|
||||
def reduce_trade(self, user_id: int, trade_id: str, qty: float) -> float | None:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,276 +1,460 @@
|
|||
from ExchangeInterface import ExchangeInterface
|
||||
from trade import Trades
|
||||
"""Tests for the Trade and Trades classes."""
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||
|
||||
from trade import Trade, Trades
|
||||
|
||||
|
||||
def test_connect_exchange():
|
||||
exchange = ExchangeInterface()
|
||||
test_trades_obj = Trades()
|
||||
test_trades_obj.connect_exchange(exchange)
|
||||
print(f'\nConnected to exchange_interface: {test_trades_obj.exchange_connected()}')
|
||||
account_info = exchange.get_precision(symbol='ETHUSDT')
|
||||
print(account_info)
|
||||
assert test_trades_obj.exchange_connected()
|
||||
class TestTrade:
|
||||
"""Tests for the Trade class."""
|
||||
|
||||
def test_trade_creation(self):
|
||||
"""Test basic trade creation."""
|
||||
trade = Trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
side='BUY',
|
||||
order_price=50000.0,
|
||||
base_order_qty=0.1
|
||||
)
|
||||
|
||||
assert trade.symbol == 'BTC/USDT'
|
||||
assert trade.side == 'BUY'
|
||||
assert trade.order_price == 50000.0
|
||||
assert trade.base_order_qty == 0.1
|
||||
assert trade.status == 'inactive'
|
||||
assert trade.is_paper is False
|
||||
assert trade.unique_id is not None
|
||||
|
||||
def test_trade_paper_flag(self):
|
||||
"""Test is_paper flag."""
|
||||
trade = Trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
side='BUY',
|
||||
order_price=50000.0,
|
||||
base_order_qty=0.1,
|
||||
is_paper=True
|
||||
)
|
||||
|
||||
assert trade.is_paper is True
|
||||
|
||||
def test_trade_to_json(self):
|
||||
"""Test trade serialization."""
|
||||
trade = Trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
side='BUY',
|
||||
order_price=50000.0,
|
||||
base_order_qty=0.1,
|
||||
is_paper=True,
|
||||
creator=1
|
||||
)
|
||||
|
||||
json_data = trade.to_json()
|
||||
|
||||
assert json_data['symbol'] == 'BTC/USDT'
|
||||
assert json_data['side'] == 'BUY'
|
||||
assert json_data['is_paper'] is True
|
||||
assert json_data['creator'] == 1
|
||||
assert 'stats' in json_data
|
||||
|
||||
def test_trade_update_values(self):
|
||||
"""Test P/L calculation."""
|
||||
trade = Trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
side='BUY',
|
||||
order_price=50000.0,
|
||||
base_order_qty=0.1,
|
||||
fee=0.001 # 0.1% fee
|
||||
)
|
||||
trade.status = 'filled'
|
||||
trade.stats['qty_filled'] = 0.1
|
||||
|
||||
# Price goes up
|
||||
trade.update_values(55000.0)
|
||||
|
||||
assert trade.stats['current_price'] == 55000.0
|
||||
assert trade.stats['current_value'] == 5500.0
|
||||
# Profit should be positive (minus fees)
|
||||
assert trade.stats['profit'] > 0
|
||||
|
||||
def test_trade_sell_side_pl(self):
|
||||
"""Test P/L calculation for sell side."""
|
||||
trade = Trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
side='SELL',
|
||||
order_price=50000.0,
|
||||
base_order_qty=0.1,
|
||||
fee=0.001
|
||||
)
|
||||
trade.status = 'filled'
|
||||
trade.stats['qty_filled'] = 0.1
|
||||
|
||||
# Price goes down - should be profit for sell
|
||||
trade.update_values(45000.0)
|
||||
|
||||
assert trade.stats['profit'] > 0
|
||||
|
||||
def test_trade_filled(self):
|
||||
"""Test trade fill logic."""
|
||||
trade = Trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
side='BUY',
|
||||
order_price=50000.0,
|
||||
base_order_qty=0.1
|
||||
)
|
||||
|
||||
trade.trade_filled(qty=0.1, price=50000.0)
|
||||
|
||||
assert trade.status == 'filled'
|
||||
assert trade.stats['qty_filled'] == 0.1
|
||||
assert trade.stats['opening_price'] == 50000.0
|
||||
|
||||
def test_trade_partial_fill(self):
|
||||
"""Test partial fill logic."""
|
||||
trade = Trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
side='BUY',
|
||||
order_price=50000.0,
|
||||
base_order_qty=0.1
|
||||
)
|
||||
|
||||
trade.trade_filled(qty=0.05, price=50000.0)
|
||||
|
||||
assert trade.status == 'part-filled'
|
||||
assert trade.stats['qty_filled'] == 0.05
|
||||
|
||||
def test_trade_settle(self):
|
||||
"""Test trade settlement."""
|
||||
trade = Trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
side='BUY',
|
||||
order_price=50000.0,
|
||||
base_order_qty=0.1
|
||||
)
|
||||
trade.trade_filled(qty=0.1, price=50000.0)
|
||||
|
||||
trade.settle(qty=0.1, price=55000.0)
|
||||
|
||||
assert trade.status == 'closed'
|
||||
assert trade.stats['settled_price'] == 55000.0
|
||||
|
||||
|
||||
def test_get_trades_by_status():
|
||||
# Connect to the exchange_interface
|
||||
exchange = ExchangeInterface()
|
||||
test_trades_obj = Trades()
|
||||
test_trades_obj.connect_exchange(exchange)
|
||||
print(f'\nConnected to exchange_interface: {test_trades_obj.exchange_connected()}')
|
||||
assert test_trades_obj.exchange_connected()
|
||||
class TestTrades:
|
||||
"""Tests for the Trades class."""
|
||||
|
||||
# create a trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 1.5, price=100, offset=None)
|
||||
print('trade 0 created.')
|
||||
print(test_trades_obj.active_trades[0].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[0].status is 'inactive'
|
||||
@pytest.fixture
|
||||
def mock_users(self):
|
||||
"""Create a mock Users object."""
|
||||
users = MagicMock()
|
||||
users.get_username.return_value = 'test_user'
|
||||
return users
|
||||
|
||||
# create a 2nd trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
|
||||
print('trade 1 created.')
|
||||
print(test_trades_obj.active_trades[1].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[1].status is 'inactive'
|
||||
@pytest.fixture
|
||||
def mock_data_cache(self):
|
||||
"""Create a mock DataCache object."""
|
||||
dc = MagicMock()
|
||||
dc.db.table_exists.return_value = True
|
||||
dc.get_all_rows_from_datacache.return_value = None
|
||||
dc.get_rows_from_datacache.return_value = MagicMock(empty=True)
|
||||
return dc
|
||||
|
||||
# create a 3rd trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
|
||||
print('trade 2 created.')
|
||||
print(test_trades_obj.active_trades[2].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[1].status is 'inactive'
|
||||
def test_trades_creation_no_cache(self, mock_users):
|
||||
"""Test Trades creation without data cache."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
# should be three trades in this list
|
||||
print(f'Expecting 3 trades in list: Actual:{len(test_trades_obj.active_trades)}')
|
||||
assert len(test_trades_obj.active_trades) is 3
|
||||
print(test_trades_obj.active_trades[0].status)
|
||||
print(test_trades_obj.active_trades[1].status)
|
||||
print(test_trades_obj.active_trades[2].status)
|
||||
assert trades.users == mock_users
|
||||
assert trades.data_cache is None
|
||||
assert len(trades.active_trades) == 0
|
||||
|
||||
# fill trade one
|
||||
test_trades_obj.active_trades[1].trade_filled(0.4, 2100)
|
||||
print(f'trade 1 filled. status:')
|
||||
print(test_trades_obj.active_trades[1].status)
|
||||
def test_trades_creation_with_cache(self, mock_users, mock_data_cache):
|
||||
"""Test Trades creation with data cache."""
|
||||
trades = Trades(mock_users, data_cache=mock_data_cache)
|
||||
|
||||
# Search for all inactive trades
|
||||
result = test_trades_obj.get_trades_by_status('inactive')
|
||||
print(f'search for all inactive trades. The result: {result}')
|
||||
assert len(result) is 2
|
||||
assert trades.data_cache == mock_data_cache
|
||||
|
||||
# Search for all filled trades
|
||||
result = test_trades_obj.get_trades_by_status('filled')
|
||||
print(f'search for all filled trades. The result: {result}')
|
||||
assert len(result) is 1
|
||||
def test_connect_exchanges(self, mock_users):
|
||||
"""Test exchange connection."""
|
||||
trades = Trades(mock_users)
|
||||
mock_exchange = MagicMock()
|
||||
|
||||
trades.connect_exchanges(mock_exchange)
|
||||
|
||||
assert trades.exchange_interface == mock_exchange
|
||||
assert trades.exchange_connected() is True
|
||||
|
||||
def test_new_paper_trade(self, mock_users):
|
||||
"""Test creating a paper trade."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
status, trade_id = trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1,
|
||||
user_id=1
|
||||
)
|
||||
|
||||
assert status == 'Success'
|
||||
assert trade_id is not None
|
||||
assert trade_id in trades.active_trades
|
||||
|
||||
# Check trade properties
|
||||
trade = trades.get_trade_by_id(trade_id)
|
||||
assert trade.is_paper is True
|
||||
assert trade.status == 'filled'
|
||||
assert trade.creator == 1
|
||||
|
||||
def test_new_live_trade_no_exchange(self, mock_users):
|
||||
"""Test creating a live trade without exchange connected."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
status, msg = trades.new_trade(
|
||||
target='binance',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1,
|
||||
user_id=1
|
||||
)
|
||||
|
||||
assert status == 'Error'
|
||||
assert 'No exchange connected' in msg
|
||||
|
||||
def test_get_trades_json(self, mock_users):
|
||||
"""Test getting trades in JSON format."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
# Create a paper trade
|
||||
trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1
|
||||
)
|
||||
|
||||
result = trades.get_trades('json')
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]['symbol'] == 'BTC/USDT'
|
||||
|
||||
def test_get_trades_for_user(self, mock_users):
|
||||
"""Test filtering trades by user."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
# Create trades for different users
|
||||
trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1,
|
||||
user_id=1
|
||||
)
|
||||
trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='ETH/USDT',
|
||||
price=3000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=1.0,
|
||||
user_id=2
|
||||
)
|
||||
|
||||
# Filter for user 1
|
||||
user1_trades = trades.get_trades_for_user(1, 'json')
|
||||
assert len(user1_trades) == 1
|
||||
assert user1_trades[0]['symbol'] == 'BTC/USDT'
|
||||
|
||||
def test_close_paper_trade(self, mock_users):
|
||||
"""Test closing a paper trade."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
# Create a paper trade
|
||||
status, trade_id = trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1
|
||||
)
|
||||
|
||||
assert trade_id in trades.active_trades
|
||||
|
||||
# Close the trade
|
||||
result = trades.close_trade(trade_id, current_price=55000.0)
|
||||
|
||||
assert result['success'] is True
|
||||
assert trade_id not in trades.active_trades
|
||||
assert trade_id in trades.settled_trades
|
||||
|
||||
def test_close_nonexistent_trade(self, mock_users):
|
||||
"""Test closing a trade that doesn't exist."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
result = trades.close_trade('nonexistent_id')
|
||||
|
||||
assert result['success'] is False
|
||||
assert 'not found' in result['message']
|
||||
|
||||
def test_is_valid_trade_id(self, mock_users):
|
||||
"""Test trade ID validation."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
_, trade_id = trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1
|
||||
)
|
||||
|
||||
assert trades.is_valid_trade_id(trade_id) is True
|
||||
assert trades.is_valid_trade_id('invalid_id') is False
|
||||
|
||||
def test_update_trades(self, mock_users):
|
||||
"""Test updating trade P/L with price changes."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
# Create and fill a trade
|
||||
_, trade_id = trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1
|
||||
)
|
||||
|
||||
# Update with new price
|
||||
price_updates = {'BTC/USDT': 55000.0}
|
||||
updates = trades.update(price_updates)
|
||||
|
||||
assert len(updates) > 0
|
||||
# Find our trade in updates
|
||||
trade_update = next((u for u in updates if u['id'] == trade_id), None)
|
||||
assert trade_update is not None
|
||||
assert trade_update['pl'] != 0 # Should have some P/L
|
||||
|
||||
def test_buy_method_paper(self, mock_users):
|
||||
"""Test buy method creates a BUY paper trade using new_trade."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
# Use new_trade for paper trades (buy/sell methods are for live trading)
|
||||
status, trade_id = trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1,
|
||||
user_id=1
|
||||
)
|
||||
|
||||
assert status == 'Success'
|
||||
trade = trades.get_trade_by_id(trade_id)
|
||||
assert trade.side == 'BUY'
|
||||
|
||||
def test_sell_method_paper(self, mock_users):
|
||||
"""Test sell method creates a SELL paper trade using new_trade."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
# Use new_trade for paper trades (buy/sell methods are for live trading)
|
||||
status, trade_id = trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='sell',
|
||||
order_type='MARKET',
|
||||
qty=0.1,
|
||||
user_id=1
|
||||
)
|
||||
|
||||
assert status == 'Success'
|
||||
trade = trades.get_trade_by_id(trade_id)
|
||||
assert trade.side == 'SELL'
|
||||
|
||||
|
||||
def test_get_trade_by_id():
|
||||
# Connect to the exchange_interface
|
||||
exchange = ExchangeInterface()
|
||||
test_trades_obj = Trades()
|
||||
test_trades_obj.connect_exchange(exchange)
|
||||
print(f'\nConnected to exchange_interface: {test_trades_obj.exchange_connected()}')
|
||||
assert test_trades_obj.exchange_connected()
|
||||
class TestTradeIntegration:
|
||||
"""Integration tests for Trade system."""
|
||||
|
||||
# create a trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 1.5, price=100, offset=None)
|
||||
print('trade 0 created.')
|
||||
print(test_trades_obj.active_trades[0].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[0].status is 'inactive'
|
||||
@pytest.fixture
|
||||
def mock_users(self):
|
||||
users = MagicMock()
|
||||
users.get_username.return_value = 'test_user'
|
||||
return users
|
||||
|
||||
# create a 2nd trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
|
||||
print('trade 1 created.')
|
||||
print(test_trades_obj.active_trades[1].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[1].status is 'inactive'
|
||||
def test_full_trade_lifecycle(self, mock_users):
|
||||
"""Test complete lifecycle: create -> update -> close."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
id = test_trades_obj.active_trades[0].unique_id
|
||||
print(f'the id of trade 0 is{id}')
|
||||
result = test_trades_obj.get_trade_by_id(id)
|
||||
print(f'here is the result after searching for the id:{result}')
|
||||
assert result.unique_id is id
|
||||
# Create trade
|
||||
status, trade_id = trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0,
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1,
|
||||
user_id=1
|
||||
)
|
||||
assert status == 'Success'
|
||||
|
||||
trade = trades.get_trade_by_id(trade_id)
|
||||
# Set a more realistic fee (0.1% instead of default 10%)
|
||||
trade.fee = 0.001
|
||||
trade.stats['fee_paid'] = trade.stats['opening_value'] * trade.fee
|
||||
|
||||
def test_load_trades():
|
||||
# Connect to the exchange_interface
|
||||
exchange = ExchangeInterface()
|
||||
test_trades_obj = Trades()
|
||||
test_trades_obj.connect_exchange(exchange)
|
||||
print(f'\nConnected to exchange_interface: {test_trades_obj.exchange_connected()}')
|
||||
assert test_trades_obj.exchange_connected()
|
||||
print(f'Active trades: {test_trades_obj.active_trades}')
|
||||
trades = [{
|
||||
'order_price': 24595.4,
|
||||
'exchange_name': 'backtester',
|
||||
'base_order_qty': 0.05,
|
||||
'order': None,
|
||||
'fee': 0.1,
|
||||
'order_type': 'MARKET',
|
||||
'side': 'buy',
|
||||
'stats': {
|
||||
'current_price': 24595.4,
|
||||
'current_value': 1229.7700000000002,
|
||||
'fee_paid': 0,
|
||||
'opening_price': 24595.4,
|
||||
'opening_value': 1229.7700000000002,
|
||||
'profit': 0,
|
||||
'profit_pct': 0,
|
||||
'qty_filled': 0,
|
||||
'qty_settled': 0,
|
||||
'settled_price': 0,
|
||||
'settled_value': 0
|
||||
},
|
||||
'status': 'inactive',
|
||||
'symbol': 'BTCUSDT',
|
||||
'time_in_force': 'GTC',
|
||||
'unique_id': '9330afd188474d83b06e19d1916c0474'
|
||||
}]
|
||||
test_trades_obj.load_trades(trades)
|
||||
print(f'Active trades: {test_trades_obj.active_trades[0].__dict__}')
|
||||
assert len(test_trades_obj.active_trades) > 0
|
||||
# Update with higher price (20% increase should exceed fees)
|
||||
trades.update({'BTC/USDT': 60000.0})
|
||||
|
||||
trade = trades.get_trade_by_id(trade_id)
|
||||
assert trade.stats['profit'] > 0 # Should be in profit
|
||||
|
||||
def test_place_order():
|
||||
# Connect to the exchange_interface
|
||||
exchange = ExchangeInterface()
|
||||
test_trades_obj = Trades()
|
||||
test_trades_obj.connect_exchange(exchange)
|
||||
print(f'\nConnected to exchange_interface: {test_trades_obj.exchange_connected()}')
|
||||
# Close trade
|
||||
result = trades.close_trade(trade_id, current_price=60000.0)
|
||||
assert result['success'] is True
|
||||
assert result['final_pl'] > 0
|
||||
|
||||
# Create a new treade on the exchange_interface.
|
||||
test_trades_obj.new_trade('exchange_interface', 'BTCUSDT', 'BUY', 'MARKET', 0.1, price=100, offset=None)
|
||||
print(test_trades_obj.active_trades[0].__dict__)
|
||||
def test_multiple_trades(self, mock_users):
|
||||
"""Test managing multiple trades."""
|
||||
trades = Trades(mock_users)
|
||||
|
||||
# If the status of the trade is unfilled the order is placed.
|
||||
assert test_trades_obj.active_trades[0].status is 'unfilled'
|
||||
# Create multiple trades
|
||||
trade_ids = []
|
||||
for i in range(3):
|
||||
_, trade_id = trades.new_trade(
|
||||
target='test_exchange',
|
||||
symbol='BTC/USDT',
|
||||
price=50000.0 + (i * 100),
|
||||
side='buy',
|
||||
order_type='MARKET',
|
||||
qty=0.1
|
||||
)
|
||||
trade_ids.append(trade_id)
|
||||
|
||||
assert len(trades.active_trades) == 3
|
||||
|
||||
def test_update():
|
||||
# Connect to the exchange_interface
|
||||
exchange = ExchangeInterface()
|
||||
test_trades_obj = Trades()
|
||||
test_trades_obj.connect_exchange(exchange)
|
||||
print(f'\nConnected to exchange_interface: {test_trades_obj.exchange_connected()}')
|
||||
# Close one trade
|
||||
trades.close_trade(trade_ids[1])
|
||||
|
||||
# Create a trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=100, offset=None)
|
||||
print(test_trades_obj.active_trades[0].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[0].status is 'inactive'
|
||||
|
||||
# create a 2nd trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.2, price=100, offset=None)
|
||||
print(test_trades_obj.active_trades[1].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[1].status is 'inactive'
|
||||
|
||||
test_trades_obj.active_trades[0].trade_filled(0.4, 100)
|
||||
test_trades_obj.active_trades[1].trade_filled(0.2, 100)
|
||||
|
||||
test_trades_obj.update(200)
|
||||
print(test_trades_obj.active_trades[0].__dict__)
|
||||
print(test_trades_obj.active_trades[1].__dict__)
|
||||
|
||||
|
||||
def test_new_trade():
|
||||
# Connect to the exchange_interface
|
||||
exchange = ExchangeInterface()
|
||||
test_trades_obj = Trades()
|
||||
test_trades_obj.connect_exchange(exchange)
|
||||
print(f'\nConnected to exchange_interface: {test_trades_obj.exchange_connected()}')
|
||||
|
||||
# create an trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.1, price=100, offset=None)
|
||||
print(test_trades_obj.active_trades[0].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[0].status is 'inactive'
|
||||
|
||||
# create a 2nd trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
|
||||
print(test_trades_obj.active_trades[1].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[1].status is 'inactive'
|
||||
|
||||
|
||||
def test_close_trade():
|
||||
# Connect to the exchange_interface
|
||||
exchange = ExchangeInterface()
|
||||
test_trades_obj = Trades()
|
||||
test_trades_obj.connect_exchange(exchange)
|
||||
print(f'\nConnected to exchange_interface: {test_trades_obj.exchange_connected()}')
|
||||
|
||||
# create a trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.1, price=100, offset=None)
|
||||
print(test_trades_obj.active_trades[0].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[0].status is 'inactive'
|
||||
|
||||
# create a 2nd trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
|
||||
print(test_trades_obj.active_trades[1].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[1].status is 'inactive'
|
||||
|
||||
# should be two trades in this list
|
||||
print(len(test_trades_obj.active_trades))
|
||||
assert len(test_trades_obj.active_trades) > 1
|
||||
|
||||
trade_id = test_trades_obj.active_trades[0].unique_id
|
||||
test_trades_obj.close_trade(trade_id)
|
||||
|
||||
# should be 1 trade in this list
|
||||
print(len(test_trades_obj.active_trades))
|
||||
assert len(test_trades_obj.active_trades) == 1
|
||||
|
||||
|
||||
def test_reduce_trade():
|
||||
# Connect to the exchange_interface
|
||||
exchange = ExchangeInterface()
|
||||
test_trades_obj = Trades()
|
||||
test_trades_obj.connect_exchange(exchange)
|
||||
print(f'\nConnected to exchange_interface: {test_trades_obj.exchange_connected()}')
|
||||
assert test_trades_obj.exchange_connected()
|
||||
|
||||
# create a trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 1.5, price=100, offset=None)
|
||||
print('trade 0 created.')
|
||||
print(test_trades_obj.active_trades[0].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[0].status is 'inactive'
|
||||
|
||||
# create a 2nd trade but not on the exchange_interface
|
||||
test_trades_obj.new_trade('backtestor', 'BTCUSDT', 'BUY', 'MARKET', 0.4, price=2100, offset=None)
|
||||
print('trade 1 created.')
|
||||
print(test_trades_obj.active_trades[1].__dict__)
|
||||
# If the status of the trade is inactive the trade is created but the order isn't placed.
|
||||
assert test_trades_obj.active_trades[1].status is 'inactive'
|
||||
|
||||
# should be two trades in this list
|
||||
print(f'Expecting 2 trades in list: Actual:{len(test_trades_obj.active_trades)}')
|
||||
assert len(test_trades_obj.active_trades) > 1
|
||||
|
||||
# Grab inactive trade 0
|
||||
trade_id = test_trades_obj.active_trades[0].unique_id
|
||||
# reduce the trade by 0.5
|
||||
remaining_qty = test_trades_obj.reduce_trade(trade_id, 0.5)
|
||||
# The trade should be 1 (1.5 - 0.5)
|
||||
print(f'The remaining quantity of the trade should be 1: Actual: {remaining_qty}')
|
||||
assert remaining_qty == 1
|
||||
|
||||
print('trade 0:')
|
||||
print(test_trades_obj.active_trades[0].__dict__)
|
||||
|
||||
test_trades_obj.active_trades[1].trade_filled(0.4, 2100)
|
||||
# Grab filled trade 1
|
||||
trade_id = test_trades_obj.active_trades[1].unique_id
|
||||
|
||||
# reduce the trade by 0.1
|
||||
remaining_qty = float(test_trades_obj.reduce_trade(trade_id, 0.1))
|
||||
# The trade should be 0.3 (0.4 - 0.1)
|
||||
print(f'\nThe remaining quantity of trade 1 should be 0.3: Actual: {remaining_qty}')
|
||||
assert remaining_qty == 0.3
|
||||
|
||||
print('trade 1:')
|
||||
print(test_trades_obj.active_trades[1].__dict__)
|
||||
assert len(trades.active_trades) == 2
|
||||
assert trade_ids[1] not in trades.active_trades
|
||||
|
|
|
|||
Loading…
Reference in New Issue