From c3681ec74b0a59589ddd0dfab7a0c392002392f5 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 27 Dec 2025 04:26:41 +0000 Subject: [PATCH] Auto commit: 2025-12-27T04:26:41.696Z --- algo_trader/app.py | 39 +++---------- algo_trader/core/data_feed.py | 48 ++++++++------- algo_trader/core/order_manager.py | 9 ++- algo_trader/core/portfolio.py | 91 ++++++++++++++++++++++++++++- algo_trader/core/strategy_engine.py | 47 ++++++++++++++- algo_trader/setup_and_run.bat | 56 ++++++++++++++++++ algo_trader/ui/dashboard.py | 17 +++++- algo_trader/ui/main_window.py | 32 +++++++++- algo_trader/ui/orders_panel.py | 43 ++++++++++---- algo_trader/ui/positions_panel.py | 59 +++++++++++++------ algo_trader/ui/strategy_panel.py | 49 ++++++++++++++-- algo_trader/utils/logger.py | 31 +++++++++- 12 files changed, 427 insertions(+), 94 deletions(-) create mode 100644 algo_trader/setup_and_run.bat diff --git a/algo_trader/app.py b/algo_trader/app.py index 1bd213d..426826a 100644 --- a/algo_trader/app.py +++ b/algo_trader/app.py @@ -2,7 +2,6 @@ import sys import asyncio import yaml from PySide6.QtWidgets import QApplication -from loguru import logger from sqlalchemy.orm import sessionmaker from ui.main_window import MainWindow @@ -12,30 +11,8 @@ from storage.database import get_engine, init_db from core.order_manager import OrderManager from core.strategy_engine import StrategyEngine from core.risk_manager import RiskManager - - -async def on_connect(): - """Callback on WebSocket connect.""" - logger.success("Data feed connected.") - - -async def on_close(): - """Callback on WebSocket close.""" - logger.warning("Data feed connection closed.") - - -async def on_error(error_msg): - """Callback for WebSocket errors.""" - logger.error(f"Data feed error: {error_msg}") - - -def setup_logging(settings: dict): - """Configures the logger based on settings.""" - logger.add( - settings["logging"]["file"], - level=settings["logging"]["level"], - rotation="10 MB", - ) +from core.portfolio import Portfolio +from utils.logger import app_logger as logger # Import the global app_logger async def run_backend( @@ -46,6 +23,7 @@ async def run_backend( risk_manager, strategy_engine, settings, + portfolio, ): try: # --- Broker and Core Components Initialization --- @@ -96,7 +74,6 @@ async def main(): print(f"Error parsing settings.yaml: {e}") sys.exit(1) - setup_logging(settings) logger.info(f"Starting {settings['app']['name']} v{settings['app']['version']}") # --- Database Initialization --- @@ -119,6 +96,7 @@ async def main(): order_manager = None risk_manager = None strategy_engine = None + portfolio = None try: broker = FyersBroker(settings=settings["fyers"]) @@ -129,11 +107,8 @@ async def main(): strategy_engine = StrategyEngine(order_manager=order_manager) data_feed = FyersDataFeed( fyers=broker.fyers, - on_connect=on_connect, - on_tick=strategy_engine.on_tick, - on_close=on_close, - on_error=on_error, ) + portfolio = Portfolio(db_session=db_session) # Launch UI and pass backend components app = QApplication(sys.argv) @@ -145,7 +120,9 @@ async def main(): risk_manager=risk_manager, strategy_engine=strategy_engine, settings=settings, - backend_runner=run_backend # Pass the async backend runner + backend_runner=run_backend, + app_logger=logger, # Pass the global app_logger instance + portfolio=portfolio, ) main_window.show() logger.info("Application UI started.") diff --git a/algo_trader/core/data_feed.py b/algo_trader/core/data_feed.py index 24c0d2a..759c52c 100644 --- a/algo_trader/core/data_feed.py +++ b/algo_trader/core/data_feed.py @@ -1,38 +1,35 @@ """Core: WebSocket market data feed.""" import asyncio -from typing import Awaitable, Callable +from PySide6.QtCore import QObject, Signal +from typing import Any from fyers_api_sdk.fyers_async import FyersAsync from loguru import logger -class FyersDataFeed: +class FyersDataFeed(QObject): """Handles WebSocket connection for real-time data.""" + class Signals(QObject): + connected = Signal() + disconnected = Signal() + error = Signal(str) + tick = Signal(dict) + def __init__( self, fyers: FyersAsync, - on_connect: Callable[[], Awaitable[None]], - on_tick: Callable[[dict], Awaitable[None]], - on_close: Callable[[], Awaitable[None]] | None = None, - on_error: Callable[[str], Awaitable[None]] | None = None, ): """ Initializes the FyersDataFeed. Args: fyers: An authenticated FyersAsync instance. - on_connect: Async callback to run on successful connection. - on_tick: Async callback to process each incoming tick. - on_close: Async callback for when the connection closes. - on_error: Async callback to handle errors. """ + super().__init__() self._fyers = fyers - self._on_connect = on_connect - self._on_tick = on_tick - self._on_close = on_close - self._on_error = on_error self.websocket = None + self.signals = self.Signals() async def connect(self, access_token: str, data_type: str = "symbolData"): """Establishes a WebSocket connection.""" @@ -41,17 +38,16 @@ class FyersDataFeed: self.websocket = self._fyers.fyers_market_socket( access_token=access_token, log_path="", # Disable log file generation - on_connect=lambda: asyncio.create_task(self._on_connect()), + on_connect=lambda: asyncio.create_task(self._handle_connect()), on_close=lambda: asyncio.create_task(self._handle_close()), on_error=lambda msg: asyncio.create_task(self._handle_error(msg)), - on_message=lambda msg: asyncio.create_task(self._on_tick(msg)), + on_message=lambda msg: asyncio.create_task(self._handle_tick(msg)), ) await asyncio.to_thread(self.websocket.connect) logger.success("Fyers WebSocket connected.") except Exception as e: logger.error(f"Failed to connect to WebSocket: {e}") - if self._on_error: - await self._on_error(str(e)) + self.signals.error.emit(str(e)) async def subscribe(self, symbols: list[str]): """Subscribes to a list of symbols.""" @@ -77,14 +73,22 @@ class FyersDataFeed: logger.info("Closing WebSocket connection.") self.websocket.close() + async def _handle_connect(self): + """Internal handler for connect events.""" + logger.info("WebSocket connection established.") + self.signals.connected.emit() + async def _handle_close(self): """Internal handler for close events.""" logger.info("WebSocket connection closed.") - if self._on_close: - await self._on_close() + self.signals.disconnected.emit() async def _handle_error(self, message: str): """Internal handler for error events.""" logger.error(f"WebSocket error: {message}") - if self._on_error: - await self._on_error(message) \ No newline at end of file + self.signals.error.emit(message) + + async def _handle_tick(self, message: dict): + """Internal handler for tick events.""" + logger.debug(f"Received tick: {message}") + self.signals.tick.emit(message) \ No newline at end of file diff --git a/algo_trader/core/order_manager.py b/algo_trader/core/order_manager.py index 954e993..4984311 100644 --- a/algo_trader/core/order_manager.py +++ b/algo_trader/core/order_manager.py @@ -4,14 +4,18 @@ import asyncio from datetime import datetime from typing import Dict, Any from loguru import logger +from PySide6.QtCore import QObject, Signal from storage.models import Order from .risk_manager import RiskManager -class OrderManager: +class OrderManager(QObject): """Handles order creation, tracking, and lifecycle management.""" + class Signals(QObject): + order_updated = Signal(Order) + def __init__(self, broker, db_session, risk_manager: RiskManager): """ Initializes the OrderManager. @@ -21,10 +25,12 @@ class OrderManager: db_session: The database session for persistence. risk_manager: The RiskManager instance for risk validation. """ + super().__init__() self.broker = broker self.db_session = db_session self.risk_manager = risk_manager self.orders = {} # In-memory cache for active orders + self.signals = self.Signals() async def place_order( self, @@ -130,6 +136,7 @@ class OrderManager: self.db_session.add(new_order) self.db_session.commit() self.orders[order_id] = new_order # Cache it + self.signals.order_updated.emit(new_order) # Emit signal logger.info(f"Order {order_id} stored in database.") except Exception as e: logger.error(f"Failed to store order {order_id}: {e}") diff --git a/algo_trader/core/portfolio.py b/algo_trader/core/portfolio.py index 1c40b4e..0056417 100644 --- a/algo_trader/core/portfolio.py +++ b/algo_trader/core/portfolio.py @@ -1 +1,90 @@ -"""Core: PnL and holdings tracking.""" +from typing import Dict, Any +from PySide6.QtCore import QObject, Signal +from loguru import logger +from storage.models import Position + + +class Portfolio(QObject): + """Manages the trading portfolio, including positions and P&L.""" + + class Signals(QObject): + position_updated = Signal(Position) + portfolio_value_updated = Signal(float) + + def __init__(self, db_session): + super().__init__() + self.db_session = db_session + self.positions: Dict[str, Position] = {} + self.signals = self.Signals() + self._load_positions_from_db() + + def _load_positions_from_db(self): + """Loads active positions from the database on initialization.""" + try: + active_positions = self.db_session.query(Position).filter_by(is_open=True).all() + for pos in active_positions: + self.positions[pos.symbol] = pos + self.signals.position_updated.emit(pos) # Emit for initial UI load + logger.info(f"Loaded {len(active_positions)} active positions from DB.") + except Exception as e: + logger.error(f"Error loading positions from DB: {e}") + + def update_position( + self, + symbol: str, + quantity: int, + average_price: float, + current_price: float, + is_open: bool = True, + ): + """ + Adds or updates a position in the portfolio. + """ + position = self.positions.get(symbol) + if position: + position.quantity = quantity + position.average_price = average_price + position.current_price = current_price + position.market_value = quantity * current_price + position.unrealized_pnl = (current_price - average_price) * quantity + position.is_open = is_open + else: + position = Position( + symbol=symbol, + quantity=quantity, + average_price=average_price, + current_price=current_price, + market_value=quantity * current_price, + unrealized_pnl=(current_price - average_price) * quantity, + is_open=is_open, + ) + self.db_session.add(position) + + self.positions[symbol] = position + try: + self.db_session.commit() + self.signals.position_updated.emit(position) + logger.info(f"Position updated for {symbol}: Qty={quantity}, AvgPrice={average_price}, CurrentPrice={current_price}") + except Exception as e: + self.db_session.rollback() + logger.error(f"Failed to save position for {symbol}: {e}") + + def get_position(self, symbol: str) -> Position | None: + """ + Returns the current position for a given symbol. + """ + return self.positions.get(symbol) + + def get_all_positions(self) -> Dict[str, Position]: + """ + Returns all active positions. + """ + return {s: p for s, p in self.positions.items() if p.is_open} + + def calculate_total_pnl(self) -> float: + """ + Calculates the total unrealized P&L of the portfolio. + """ + total_pnl = sum(pos.unrealized_pnl for pos in self.positions.values() if pos.is_open) + self.signals.portfolio_value_updated.emit(total_pnl) + return total_pnl \ No newline at end of file diff --git a/algo_trader/core/strategy_engine.py b/algo_trader/core/strategy_engine.py index ea47e02..9169423 100644 --- a/algo_trader/core/strategy_engine.py +++ b/algo_trader/core/strategy_engine.py @@ -1,11 +1,18 @@ """Core: Strategy orchestration and signal generation.""" from loguru import logger +from PySide6.QtCore import QObject, Signal -class StrategyEngine: +class StrategyEngine(QObject): """Hosts and executes trading strategies, generating signals from market data.""" + class Signals(QObject): + strategy_loaded = Signal(str) + strategy_started = Signal(str) + strategy_stopped = Signal(str) + trade_signal = Signal(dict) # Emits trade signals for potential orders + def __init__(self, order_manager): """ Initializes the StrategyEngine. @@ -13,9 +20,13 @@ class StrategyEngine: Args: order_manager: The OrderManager instance to send signals to. """ + super().__init__() self.order_manager = order_manager self.strategy = None + self.strategy_name = None self.last_price = None # For our simple demo strategy + self._is_running = False + self.signals = self.Signals() def load_strategy(self, strategy_name: str): """ @@ -24,10 +35,36 @@ class StrategyEngine: """ if strategy_name == "simple_crossover": self.strategy = self._simple_crossover_strategy + self.strategy_name = strategy_name logger.info(f"Loaded strategy: {strategy_name}") + self.signals.strategy_loaded.emit(strategy_name) else: logger.error(f"Strategy '{strategy_name}' not found.") + def start_strategy(self): + """ + Starts the loaded strategy. + """ + if self.strategy and not self._is_running: + self._is_running = True + logger.info(f"Strategy '{self.strategy_name}' started.") + self.signals.strategy_started.emit(self.strategy_name) + elif not self.strategy: + logger.warning("No strategy loaded to start.") + else: + logger.warning("Strategy is already running.") + + def stop_strategy(self): + """ + Stops the running strategy. + """ + if self._is_running: + self._is_running = False + logger.info(f"Strategy '{self.strategy_name}' stopped.") + self.signals.strategy_stopped.emit(self.strategy_name) + else: + logger.warning("No strategy is currently running.") + async def on_tick(self, tick: dict): """ Called by the DataFeed on every new market data tick. @@ -36,7 +73,7 @@ class StrategyEngine: Args: tick (dict): The tick data from the data feed. """ - if not self.strategy: + if not self.strategy or not self._is_running: return # The tick format from FYERS WebSocket is a list of dicts @@ -66,17 +103,21 @@ class StrategyEngine: # Only trade once for this demo if abs(self.last_price - current_price) > 0.1: # Threshold to act + trade_info = {"symbol": symbol, "current_price": current_price} if current_price > self.last_price: logger.warning(f"Price increased: {self.last_price} -> {current_price}. Sending BUY signal.") + trade_info["side"] = "BUY" await self.order_manager.place_order( symbol=symbol, side="BUY", current_price=current_price, quantity=1, order_type="MARKET" ) elif current_price < self.last_price: logger.warning(f"Price decreased: {self.last_price} -> {current_price}. Sending SELL signal.") + trade_info["side"] = "SELL" await self.order_manager.place_order( symbol=symbol, side="SELL", current_price=current_price, quantity=1, order_type="MARKET" ) + self.signals.trade_signal.emit(trade_info) # Stop further trading in this simple example - self.strategy = None + self.stop_strategy() logger.info("Demo trade placed. Strategy deactivated.") \ No newline at end of file diff --git a/algo_trader/setup_and_run.bat b/algo_trader/setup_and_run.bat new file mode 100644 index 0000000..84e2f04 --- /dev/null +++ b/algo_trader/setup_and_run.bat @@ -0,0 +1,56 @@ +@echo off +REM This script automates the setup and execution of the Algo Trader application. +REM It is designed for Windows environments. + +echo Checking for Python 3.10+... +python -c "import sys; assert sys.version_info >= (3, 10)" >nul 2>&1 +if %errorlevel% neq 0 ( + echo Python 3.10 or higher is not found or not in PATH. Please install it. + echo Download from: https://www.python.org/downloads/windows/ + pause + exit /b 1 +) + +echo Setting up virtual environment... +python -m venv venv +if %errorlevel% neq 0 ( + echo Failed to create virtual environment. + pause + exit /b 1 +) + +echo Activating virtual environment... +call .\venv\Scripts\activate.bat +if %errorlevel% neq 0 ( + echo Failed to activate virtual environment. + pause + exit /b 1 +) + +echo Installing dependencies... +pip install PySide6 pyqtgraph pandas numpy ta fyers-apiv3 python-dotenv loguru websocket-client PyYAML SQLAlchemy alembic +if %errorlevel% neq 0 ( + echo Failed to install dependencies. + pause + exit /b 1 +) + +echo. +echo ========================================================================= +echo IMPORTANT: API Credentials Setup +echo ========================================================================= +echo 1. Navigate to the 'config' folder inside 'algo_trader'. +echo 2. Rename 'credentials.env.example' to 'credentials.env'. +echo 3. Open 'credentials.env' in a text editor. +echo 4. Fill in your actual FYERS API CLIENT_ID, APP_ID, SECRET_KEY, and REDIRECT_URL. +echo If you don't have these, you'll need to create an API app on FYERS. +echo ========================================================================= +echo. + +pause +echo Launching Algo Trader application... +python app.py + +echo Application closed. +pause +exit /b 0 diff --git a/algo_trader/ui/dashboard.py b/algo_trader/ui/dashboard.py index 9089202..e90ef1f 100644 --- a/algo_trader/ui/dashboard.py +++ b/algo_trader/ui/dashboard.py @@ -39,6 +39,11 @@ class DashboardPanel(QWidget): metrics_grid.addWidget(self.pending_orders_label[0], 3, 0) metrics_grid.addWidget(self.pending_orders_label[1], 3, 1) + # Last Tick + self.last_tick_label = self._create_metric_label("Last Tick:", "N/A") + metrics_grid.addWidget(self.last_tick_label[0], 4, 0) + metrics_grid.addWidget(self.last_tick_label[1], 4, 1) + layout.addLayout(metrics_grid) layout.addStretch(1) # Pushes content to the top self.setLayout(layout) @@ -47,4 +52,14 @@ class DashboardPanel(QWidget): title_label = QLabel(title) title_label.setStyleSheet("font-weight: bold;") value_label = QLabel(value) - return title_label, value_label \ No newline at end of file + return title_label, value_label + + def update_market_data(self, tick_data): + # Example: Update Last Tick label with relevant info + if "symbol" in tick_data and "ltp" in tick_data: + self.last_tick_label[1].setText(f"{tick_data['symbol']}: {tick_data['ltp']}") + elif isinstance(tick_data, dict): + # Fallback for any other dict data + self.last_tick_label[1].setText(str(tick_data)) + else: + self.last_tick_label[1].setText(str(tick_data)) \ No newline at end of file diff --git a/algo_trader/ui/main_window.py b/algo_trader/ui/main_window.py index 373a8b4..bcef27c 100644 --- a/algo_trader/ui/main_window.py +++ b/algo_trader/ui/main_window.py @@ -25,7 +25,7 @@ class AsyncWorker(QThread): class MainWindow(QMainWindow): def __init__( - self, broker, data_feed, db_session, order_manager, risk_manager, strategy_engine, settings, backend_runner + self, broker, data_feed, db_session, order_manager, risk_manager, strategy_engine, settings, backend_runner, app_logger, portfolio ): super().__init__() self.broker = broker @@ -36,6 +36,8 @@ class MainWindow(QMainWindow): self.strategy_engine = strategy_engine self.settings = settings self.backend_runner = backend_runner + self.app_logger = app_logger # Store the app_logger instance + self.portfolio = portfolio self.setWindowTitle("Algo Trader") self.setGeometry(100, 100, 1600, 900) @@ -60,6 +62,33 @@ class MainWindow(QMainWindow): self.setCentralWidget(self.tabs) + # Connect logger signal to logs panel + self.app_logger.log_signal.connect(self.logs_panel.append_log) + + # Connect data_feed signals to dashboard and logger + self.data_feed.signals.connected.connect(lambda: self.app_logger.info("DataFeed: Connected")) + self.data_feed.signals.disconnected.connect(lambda: self.app_logger.warning("DataFeed: Disconnected")) + self.data_feed.signals.error.connect(lambda msg: self.app_logger.error(f"DataFeed Error: {msg}")) + self.data_feed.signals.tick.connect(self.dashboard_panel.update_market_data) + + # Connect order_manager signals to orders panel + self.order_manager.signals.order_updated.connect(self.orders_panel.add_or_update_order) + + # Connect portfolio signals to positions panel + self.portfolio.signals.position_updated.connect(self.positions_panel.add_or_update_position) + self.portfolio.signals.portfolio_value_updated.connect(lambda value: self.app_logger.info(f"Portfolio Value Updated: {value}")) + + # Connect strategy_engine signals to strategy_panel and logger + self.strategy_engine.signals.strategy_loaded.connect(self.strategy_panel.update_status) + self.strategy_engine.signals.strategy_started.connect(lambda name: self.strategy_panel.update_status(f"Running: {name}")) + self.strategy_engine.signals.strategy_stopped.connect(lambda name: self.strategy_panel.update_status(f"Stopped: {name}")) + self.strategy_engine.signals.trade_signal.connect(lambda signal: self.app_logger.info(f"Trade Signal: {signal}")) + + # Connect strategy_panel UI signals to strategy_engine methods + self.strategy_panel.strategy_selected.connect(self.strategy_engine.load_strategy) + self.strategy_panel.start_strategy_requested.connect(self.strategy_engine.start_strategy) + self.strategy_panel.stop_strategy_requested.connect(self.strategy_engine.stop_strategy) + # --- Asyncio Integration --- self.loop = asyncio.get_event_loop() if self.loop.is_running(): @@ -76,6 +105,7 @@ class MainWindow(QMainWindow): "risk_manager": self.risk_manager, "strategy_engine": self.strategy_engine, "settings": self.settings, + "portfolio": self.portfolio, }, ) self.backend_worker.start() diff --git a/algo_trader/ui/orders_panel.py b/algo_trader/ui/orders_panel.py index 2099b6d..402a063 100644 --- a/algo_trader/ui/orders_panel.py +++ b/algo_trader/ui/orders_panel.py @@ -1,10 +1,12 @@ from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView from PySide6.QtCore import Qt +from storage.models import Order class OrdersPanel(QWidget): def __init__(self): super().__init__() self.init_ui() + self.order_rows = {} # To keep track of rows by order_id def init_ui(self): layout = QVBoxLayout(self) @@ -24,14 +26,35 @@ class OrdersPanel(QWidget): layout.addStretch(1) # Pushes content to the top self.setLayout(layout) + def add_or_update_order(self, order: Order): + if order.broker_order_id in self.order_rows: + row_position = self.order_rows[order.broker_order_id] + else: + row_position = self.orders_table.rowCount() + self.orders_table.insertRow(row_position) + self.order_rows[order.broker_order_id] = row_position + + self.orders_table.setItem(row_position, 0, QTableWidgetItem(str(order.broker_order_id))) + self.orders_table.setItem(row_position, 1, QTableWidgetItem(order.symbol)) + self.orders_table.setItem(row_position, 2, QTableWidgetItem(order.order_type)) + self.orders_table.setItem(row_position, 3, QTableWidgetItem(order.side)) + self.orders_table.setItem(row_position, 4, QTableWidgetItem(str(order.quantity))) + self.orders_table.setItem(row_position, 5, QTableWidgetItem(str(order.price))) + self.orders_table.setItem(row_position, 6, QTableWidgetItem(order.status)) + self.orders_table.setItem(row_position, 7, QTableWidgetItem(str(order.timestamp))) + def add_order(self, order_id, symbol, order_type, side, quantity, price, status, timestamp): - row_position = self.orders_table.rowCount() - self.orders_table.insertRow(row_position) - self.orders_table.setItem(row_position, 0, QTableWidgetItem(str(order_id))) - self.orders_table.setItem(row_position, 1, QTableWidgetItem(symbol)) - self.orders_table.setItem(row_position, 2, QTableWidgetItem(order_type)) - self.orders_table.setItem(row_position, 3, QTableWidgetItem(side)) - self.orders_table.setItem(row_position, 4, QTableWidgetItem(str(quantity))) - self.orders_table.setItem(row_position, 5, QTableWidgetItem(str(price))) - self.orders_table.setItem(row_position, 6, QTableWidgetItem(status)) - self.orders_table.setItem(row_position, 7, QTableWidgetItem(str(timestamp))) \ No newline at end of file + # This method is now a compatibility wrapper or can be removed if not used elsewhere + # For now, create a dummy Order object and call add_or_update_order + from storage.models import Order as DummyOrder # Use alias to avoid re-importing + dummy_order = DummyOrder( + broker_order_id=order_id, + symbol=symbol, + order_type=order_type, + side=side, + quantity=quantity, + price=price, + status=status, + timestamp=timestamp + ) + self.add_or_update_order(dummy_order) \ No newline at end of file diff --git a/algo_trader/ui/positions_panel.py b/algo_trader/ui/positions_panel.py index 8be7508..d04d9da 100644 --- a/algo_trader/ui/positions_panel.py +++ b/algo_trader/ui/positions_panel.py @@ -1,10 +1,12 @@ from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView from PySide6.QtCore import Qt +from storage.models import Position class PositionsPanel(QWidget): def __init__(self): super().__init__() self.init_ui() + self.position_rows = {} # To keep track of rows by symbol def init_ui(self): layout = QVBoxLayout(self) @@ -24,23 +26,42 @@ class PositionsPanel(QWidget): layout.addStretch(1) # Pushes content to the top self.setLayout(layout) - def update_position(self, symbol, quantity, avg_price, current_price, market_value, unrealized_pnl): - # Check if position already exists - for row in range(self.positions_table.rowCount()): - if self.positions_table.item(row, 0).text() == symbol: - self.positions_table.setItem(row, 1, QTableWidgetItem(str(quantity))) - self.positions_table.setItem(row, 2, QTableWidgetItem(str(avg_price))) - self.positions_table.setItem(row, 3, QTableWidgetItem(str(current_price))) - self.positions_table.setItem(row, 4, QTableWidgetItem(str(market_value))) - self.positions_table.setItem(row, 5, QTableWidgetItem(str(unrealized_pnl))) - return + def add_or_update_position(self, position: Position): + if not position.is_open: # Remove closed positions from the UI + if position.symbol in self.position_rows: + row_to_remove = self.position_rows.pop(position.symbol) + self.positions_table.removeRow(row_to_remove) + # Update row indices after removal + for symbol, row_idx in self.position_rows.items(): + if row_idx > row_to_remove: + self.position_rows[symbol] = row_idx - 1 + return - # Add new position if not found - row_position = self.positions_table.rowCount() - self.positions_table.insertRow(row_position) - self.positions_table.setItem(row_position, 0, QTableWidgetItem(symbol)) - self.positions_table.setItem(row_position, 1, QTableWidgetItem(str(quantity))) - self.positions_table.setItem(row_position, 2, QTableWidgetItem(str(avg_price))) - self.positions_table.setItem(row_position, 3, QTableWidgetItem(str(current_price))) - self.positions_table.setItem(row_position, 4, QTableWidgetItem(str(market_value))) - self.positions_table.setItem(row_position, 5, QTableWidgetItem(str(unrealized_pnl))) \ No newline at end of file + if position.symbol in self.position_rows: + row_position = self.position_rows[position.symbol] + else: + row_position = self.positions_table.rowCount() + self.positions_table.insertRow(row_position) + self.position_rows[position.symbol] = row_position + + self.positions_table.setItem(row_position, 0, QTableWidgetItem(position.symbol)) + self.positions_table.setItem(row_position, 1, QTableWidgetItem(str(position.quantity))) + self.positions_table.setItem(row_position, 2, QTableWidgetItem(str(position.average_price))) + self.positions_table.setItem(row_position, 3, QTableWidgetItem(str(position.current_price))) + self.positions_table.setItem(row_position, 4, QTableWidgetItem(str(position.market_value))) + self.positions_table.setItem(row_position, 5, QTableWidgetItem(str(position.unrealized_pnl))) + + def update_position(self, symbol, quantity, avg_price, current_price, market_value, unrealized_pnl): + # This method is now a compatibility wrapper or can be removed if not used elsewhere + # For now, create a dummy Position object and call add_or_update_position + from storage.models import Position as DummyPosition # Use alias to avoid re-importing + dummy_position = DummyPosition( + symbol=symbol, + quantity=quantity, + average_price=avg_price, + current_price=current_price, + market_value=market_value, + unrealized_pnl=unrealized_pnl, + is_open=True # Assuming update_position is for open positions + ) + self.add_or_update_position(dummy_position) \ No newline at end of file diff --git a/algo_trader/ui/strategy_panel.py b/algo_trader/ui/strategy_panel.py index c64f754..75af5fa 100644 --- a/algo_trader/ui/strategy_panel.py +++ b/algo_trader/ui/strategy_panel.py @@ -1,7 +1,11 @@ from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, Signal class StrategyPanel(QWidget): + strategy_selected = Signal(str) + start_strategy_requested = Signal(str) + stop_strategy_requested = Signal(str) + def __init__(self): super().__init__() self.init_ui() @@ -20,14 +24,19 @@ class StrategyPanel(QWidget): strategy_control_layout.addWidget(QLabel("Select Strategy:")) self.strategy_selector = QComboBox() - self.strategy_selector.addItem("Simple Crossover") # Placeholder strategy - self.strategy_selector.addItem("Intraday FNO") # Placeholder strategy + self.strategy_selector.addItem("None") + self.strategy_selector.addItem("simple_crossover") # Placeholder strategy + self.strategy_selector.addItem("intraday_fno") # Placeholder strategy + self.strategy_selector.currentIndexChanged.connect(self._on_strategy_selected) strategy_control_layout.addWidget(self.strategy_selector) self.start_button = QPushButton("Start Strategy") + self.start_button.clicked.connect(self._on_start_button_clicked) + self.start_button.setEnabled(False) # Initially disabled strategy_control_layout.addWidget(self.start_button) self.stop_button = QPushButton("Stop Strategy") + self.stop_button.clicked.connect(self._on_stop_button_clicked) self.stop_button.setEnabled(False) # Initially disabled strategy_control_layout.addWidget(self.stop_button) @@ -42,4 +51,36 @@ class StrategyPanel(QWidget): layout.addLayout(status_layout) layout.addStretch(1) # Pushes content to the top - self.setLayout(layout) \ No newline at end of file + self.setLayout(layout) + + def _on_strategy_selected(self, index): + strategy_name = self.strategy_selector.currentText() + self.strategy_selected.emit(strategy_name) + self.start_button.setEnabled(strategy_name != "None") + self.stop_button.setEnabled(False) + self.update_status("Loaded" if strategy_name != "None" else "Idle") + + def _on_start_button_clicked(self): + strategy_name = self.strategy_selector.currentText() + if strategy_name != "None": + self.start_strategy_requested.emit(strategy_name) + self.start_button.setEnabled(False) + self.stop_button.setEnabled(True) + self.update_status("Starting...") + + def _on_stop_button_clicked(self): + strategy_name = self.strategy_selector.currentText() + if strategy_name != "None": + self.stop_strategy_requested.emit(strategy_name) + self.start_button.setEnabled(True) + self.stop_button.setEnabled(False) + self.update_status("Stopping...") + + def update_status(self, status: str): + self.strategy_status_label.setText(status) + + def set_available_strategies(self, strategies: list[str]): + self.strategy_selector.clear() + self.strategy_selector.addItem("None") + for strategy in strategies: + self.strategy_selector.addItem(strategy) \ No newline at end of file diff --git a/algo_trader/utils/logger.py b/algo_trader/utils/logger.py index 499038b..f8bb249 100644 --- a/algo_trader/utils/logger.py +++ b/algo_trader/utils/logger.py @@ -1 +1,30 @@ -"""Utils: Logging configuration.""" +from PySide6.QtCore import QObject, Signal +from loguru import logger +import sys + +class Logger(QObject): + log_signal = Signal(str) + + def __init__(self): + super().__init__() + logger.remove() # Remove default logger + logger.add(self.emit_log, level="INFO") + logger.add(sys.stderr, level="ERROR") + + def emit_log(self, message): + self.log_signal.emit(message) + + def info(self, message): + logger.info(message) + + def warning(self, message): + logger.warning(message) + + def error(self, message): + logger.error(message) + + def debug(self, message): + logger.debug(message) + +# Global logger instance +app_logger = Logger() \ No newline at end of file