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:
rob 2026-03-02 16:05:53 -04:00
parent 78dfd71303
commit f603728080
9 changed files with 2160 additions and 528 deletions

View File

@ -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

View File

@ -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}")

View File

@ -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);

File diff suppressed because it is too large Load Diff

View File

@ -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>
<select name="tradeTarget" id="tradeTarget" style="grid-column: 2; grid-row: 2;" >
<option>test_exchange</option>
<option>binance</option>
</select>
</div>
<!-- Side Input field (row 3/8)-->
<!-- 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 value="test_exchange">Paper Trade</option>
<option value="binance">Binance (Live)</option>
</select>
<!-- 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>
<select name="side" id="side" style="grid-column: 2; grid-row: 3;">
<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>
<select name="orderType" id="orderType" style="grid-column: 2; grid-row: 4;" >
<option>MARKET</option>
<option>LIMIT</option>
<!-- 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 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>

View File

@ -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;

View File

@ -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>

View File

@ -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
current_price = self.exchange_interface.get_price(trade.symbol)
# 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:
"""

View File

@ -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