Auto commit: 2025-12-27T04:26:41.696Z

This commit is contained in:
Flatlogic Bot 2025-12-27 04:26:41 +00:00
parent 5836c16118
commit c3681ec74b
12 changed files with 427 additions and 94 deletions

View File

@ -2,7 +2,6 @@ import sys
import asyncio import asyncio
import yaml import yaml
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from loguru import logger
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from ui.main_window import MainWindow 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.order_manager import OrderManager
from core.strategy_engine import StrategyEngine from core.strategy_engine import StrategyEngine
from core.risk_manager import RiskManager from core.risk_manager import RiskManager
from core.portfolio import Portfolio
from utils.logger import app_logger as logger # Import the global app_logger
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",
)
async def run_backend( async def run_backend(
@ -46,6 +23,7 @@ async def run_backend(
risk_manager, risk_manager,
strategy_engine, strategy_engine,
settings, settings,
portfolio,
): ):
try: try:
# --- Broker and Core Components Initialization --- # --- Broker and Core Components Initialization ---
@ -96,7 +74,6 @@ async def main():
print(f"Error parsing settings.yaml: {e}") print(f"Error parsing settings.yaml: {e}")
sys.exit(1) sys.exit(1)
setup_logging(settings)
logger.info(f"Starting {settings['app']['name']} v{settings['app']['version']}") logger.info(f"Starting {settings['app']['name']} v{settings['app']['version']}")
# --- Database Initialization --- # --- Database Initialization ---
@ -119,6 +96,7 @@ async def main():
order_manager = None order_manager = None
risk_manager = None risk_manager = None
strategy_engine = None strategy_engine = None
portfolio = None
try: try:
broker = FyersBroker(settings=settings["fyers"]) broker = FyersBroker(settings=settings["fyers"])
@ -129,11 +107,8 @@ async def main():
strategy_engine = StrategyEngine(order_manager=order_manager) strategy_engine = StrategyEngine(order_manager=order_manager)
data_feed = FyersDataFeed( data_feed = FyersDataFeed(
fyers=broker.fyers, 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 # Launch UI and pass backend components
app = QApplication(sys.argv) app = QApplication(sys.argv)
@ -145,7 +120,9 @@ async def main():
risk_manager=risk_manager, risk_manager=risk_manager,
strategy_engine=strategy_engine, strategy_engine=strategy_engine,
settings=settings, 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() main_window.show()
logger.info("Application UI started.") logger.info("Application UI started.")

View File

@ -1,38 +1,35 @@
"""Core: WebSocket market data feed.""" """Core: WebSocket market data feed."""
import asyncio 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 fyers_api_sdk.fyers_async import FyersAsync
from loguru import logger from loguru import logger
class FyersDataFeed: class FyersDataFeed(QObject):
"""Handles WebSocket connection for real-time data.""" """Handles WebSocket connection for real-time data."""
class Signals(QObject):
connected = Signal()
disconnected = Signal()
error = Signal(str)
tick = Signal(dict)
def __init__( def __init__(
self, self,
fyers: FyersAsync, 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. Initializes the FyersDataFeed.
Args: Args:
fyers: An authenticated FyersAsync instance. 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._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.websocket = None
self.signals = self.Signals()
async def connect(self, access_token: str, data_type: str = "symbolData"): async def connect(self, access_token: str, data_type: str = "symbolData"):
"""Establishes a WebSocket connection.""" """Establishes a WebSocket connection."""
@ -41,17 +38,16 @@ class FyersDataFeed:
self.websocket = self._fyers.fyers_market_socket( self.websocket = self._fyers.fyers_market_socket(
access_token=access_token, access_token=access_token,
log_path="", # Disable log file generation 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_close=lambda: asyncio.create_task(self._handle_close()),
on_error=lambda msg: asyncio.create_task(self._handle_error(msg)), 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) await asyncio.to_thread(self.websocket.connect)
logger.success("Fyers WebSocket connected.") logger.success("Fyers WebSocket connected.")
except Exception as e: except Exception as e:
logger.error(f"Failed to connect to WebSocket: {e}") logger.error(f"Failed to connect to WebSocket: {e}")
if self._on_error: self.signals.error.emit(str(e))
await self._on_error(str(e))
async def subscribe(self, symbols: list[str]): async def subscribe(self, symbols: list[str]):
"""Subscribes to a list of symbols.""" """Subscribes to a list of symbols."""
@ -77,14 +73,22 @@ class FyersDataFeed:
logger.info("Closing WebSocket connection.") logger.info("Closing WebSocket connection.")
self.websocket.close() 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): async def _handle_close(self):
"""Internal handler for close events.""" """Internal handler for close events."""
logger.info("WebSocket connection closed.") logger.info("WebSocket connection closed.")
if self._on_close: self.signals.disconnected.emit()
await self._on_close()
async def _handle_error(self, message: str): async def _handle_error(self, message: str):
"""Internal handler for error events.""" """Internal handler for error events."""
logger.error(f"WebSocket error: {message}") logger.error(f"WebSocket error: {message}")
if self._on_error: self.signals.error.emit(message)
await self._on_error(message)
async def _handle_tick(self, message: dict):
"""Internal handler for tick events."""
logger.debug(f"Received tick: {message}")
self.signals.tick.emit(message)

View File

@ -4,14 +4,18 @@ import asyncio
from datetime import datetime from datetime import datetime
from typing import Dict, Any from typing import Dict, Any
from loguru import logger from loguru import logger
from PySide6.QtCore import QObject, Signal
from storage.models import Order from storage.models import Order
from .risk_manager import RiskManager from .risk_manager import RiskManager
class OrderManager: class OrderManager(QObject):
"""Handles order creation, tracking, and lifecycle management.""" """Handles order creation, tracking, and lifecycle management."""
class Signals(QObject):
order_updated = Signal(Order)
def __init__(self, broker, db_session, risk_manager: RiskManager): def __init__(self, broker, db_session, risk_manager: RiskManager):
""" """
Initializes the OrderManager. Initializes the OrderManager.
@ -21,10 +25,12 @@ class OrderManager:
db_session: The database session for persistence. db_session: The database session for persistence.
risk_manager: The RiskManager instance for risk validation. risk_manager: The RiskManager instance for risk validation.
""" """
super().__init__()
self.broker = broker self.broker = broker
self.db_session = db_session self.db_session = db_session
self.risk_manager = risk_manager self.risk_manager = risk_manager
self.orders = {} # In-memory cache for active orders self.orders = {} # In-memory cache for active orders
self.signals = self.Signals()
async def place_order( async def place_order(
self, self,
@ -130,6 +136,7 @@ class OrderManager:
self.db_session.add(new_order) self.db_session.add(new_order)
self.db_session.commit() self.db_session.commit()
self.orders[order_id] = new_order # Cache it 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.") logger.info(f"Order {order_id} stored in database.")
except Exception as e: except Exception as e:
logger.error(f"Failed to store order {order_id}: {e}") logger.error(f"Failed to store order {order_id}: {e}")

View File

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

View File

@ -1,11 +1,18 @@
"""Core: Strategy orchestration and signal generation.""" """Core: Strategy orchestration and signal generation."""
from loguru import logger 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.""" """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): def __init__(self, order_manager):
""" """
Initializes the StrategyEngine. Initializes the StrategyEngine.
@ -13,9 +20,13 @@ class StrategyEngine:
Args: Args:
order_manager: The OrderManager instance to send signals to. order_manager: The OrderManager instance to send signals to.
""" """
super().__init__()
self.order_manager = order_manager self.order_manager = order_manager
self.strategy = None self.strategy = None
self.strategy_name = None
self.last_price = None # For our simple demo strategy self.last_price = None # For our simple demo strategy
self._is_running = False
self.signals = self.Signals()
def load_strategy(self, strategy_name: str): def load_strategy(self, strategy_name: str):
""" """
@ -24,10 +35,36 @@ class StrategyEngine:
""" """
if strategy_name == "simple_crossover": if strategy_name == "simple_crossover":
self.strategy = self._simple_crossover_strategy self.strategy = self._simple_crossover_strategy
self.strategy_name = strategy_name
logger.info(f"Loaded strategy: {strategy_name}") logger.info(f"Loaded strategy: {strategy_name}")
self.signals.strategy_loaded.emit(strategy_name)
else: else:
logger.error(f"Strategy '{strategy_name}' not found.") 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): async def on_tick(self, tick: dict):
""" """
Called by the DataFeed on every new market data tick. Called by the DataFeed on every new market data tick.
@ -36,7 +73,7 @@ class StrategyEngine:
Args: Args:
tick (dict): The tick data from the data feed. tick (dict): The tick data from the data feed.
""" """
if not self.strategy: if not self.strategy or not self._is_running:
return return
# The tick format from FYERS WebSocket is a list of dicts # The tick format from FYERS WebSocket is a list of dicts
@ -66,17 +103,21 @@ class StrategyEngine:
# Only trade once for this demo # Only trade once for this demo
if abs(self.last_price - current_price) > 0.1: # Threshold to act 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: if current_price > self.last_price:
logger.warning(f"Price increased: {self.last_price} -> {current_price}. Sending BUY signal.") logger.warning(f"Price increased: {self.last_price} -> {current_price}. Sending BUY signal.")
trade_info["side"] = "BUY"
await self.order_manager.place_order( await self.order_manager.place_order(
symbol=symbol, side="BUY", current_price=current_price, quantity=1, order_type="MARKET" symbol=symbol, side="BUY", current_price=current_price, quantity=1, order_type="MARKET"
) )
elif current_price < self.last_price: elif current_price < self.last_price:
logger.warning(f"Price decreased: {self.last_price} -> {current_price}. Sending SELL signal.") logger.warning(f"Price decreased: {self.last_price} -> {current_price}. Sending SELL signal.")
trade_info["side"] = "SELL"
await self.order_manager.place_order( await self.order_manager.place_order(
symbol=symbol, side="SELL", current_price=current_price, quantity=1, order_type="MARKET" 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 # Stop further trading in this simple example
self.strategy = None self.stop_strategy()
logger.info("Demo trade placed. Strategy deactivated.") logger.info("Demo trade placed. Strategy deactivated.")

View File

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

View File

@ -39,6 +39,11 @@ class DashboardPanel(QWidget):
metrics_grid.addWidget(self.pending_orders_label[0], 3, 0) metrics_grid.addWidget(self.pending_orders_label[0], 3, 0)
metrics_grid.addWidget(self.pending_orders_label[1], 3, 1) 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.addLayout(metrics_grid)
layout.addStretch(1) # Pushes content to the top layout.addStretch(1) # Pushes content to the top
self.setLayout(layout) self.setLayout(layout)
@ -47,4 +52,14 @@ class DashboardPanel(QWidget):
title_label = QLabel(title) title_label = QLabel(title)
title_label.setStyleSheet("font-weight: bold;") title_label.setStyleSheet("font-weight: bold;")
value_label = QLabel(value) value_label = QLabel(value)
return title_label, value_label 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))

View File

@ -25,7 +25,7 @@ class AsyncWorker(QThread):
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__( 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__() super().__init__()
self.broker = broker self.broker = broker
@ -36,6 +36,8 @@ class MainWindow(QMainWindow):
self.strategy_engine = strategy_engine self.strategy_engine = strategy_engine
self.settings = settings self.settings = settings
self.backend_runner = backend_runner self.backend_runner = backend_runner
self.app_logger = app_logger # Store the app_logger instance
self.portfolio = portfolio
self.setWindowTitle("Algo Trader") self.setWindowTitle("Algo Trader")
self.setGeometry(100, 100, 1600, 900) self.setGeometry(100, 100, 1600, 900)
@ -60,6 +62,33 @@ class MainWindow(QMainWindow):
self.setCentralWidget(self.tabs) 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 --- # --- Asyncio Integration ---
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
if self.loop.is_running(): if self.loop.is_running():
@ -76,6 +105,7 @@ class MainWindow(QMainWindow):
"risk_manager": self.risk_manager, "risk_manager": self.risk_manager,
"strategy_engine": self.strategy_engine, "strategy_engine": self.strategy_engine,
"settings": self.settings, "settings": self.settings,
"portfolio": self.portfolio,
}, },
) )
self.backend_worker.start() self.backend_worker.start()

View File

@ -1,10 +1,12 @@
from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from storage.models import Order
class OrdersPanel(QWidget): class OrdersPanel(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.init_ui() self.init_ui()
self.order_rows = {} # To keep track of rows by order_id
def init_ui(self): def init_ui(self):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@ -24,14 +26,35 @@ class OrdersPanel(QWidget):
layout.addStretch(1) # Pushes content to the top layout.addStretch(1) # Pushes content to the top
self.setLayout(layout) 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): def add_order(self, order_id, symbol, order_type, side, quantity, price, status, timestamp):
row_position = self.orders_table.rowCount() # This method is now a compatibility wrapper or can be removed if not used elsewhere
self.orders_table.insertRow(row_position) # For now, create a dummy Order object and call add_or_update_order
self.orders_table.setItem(row_position, 0, QTableWidgetItem(str(order_id))) from storage.models import Order as DummyOrder # Use alias to avoid re-importing
self.orders_table.setItem(row_position, 1, QTableWidgetItem(symbol)) dummy_order = DummyOrder(
self.orders_table.setItem(row_position, 2, QTableWidgetItem(order_type)) broker_order_id=order_id,
self.orders_table.setItem(row_position, 3, QTableWidgetItem(side)) symbol=symbol,
self.orders_table.setItem(row_position, 4, QTableWidgetItem(str(quantity))) order_type=order_type,
self.orders_table.setItem(row_position, 5, QTableWidgetItem(str(price))) side=side,
self.orders_table.setItem(row_position, 6, QTableWidgetItem(status)) quantity=quantity,
self.orders_table.setItem(row_position, 7, QTableWidgetItem(str(timestamp))) price=price,
status=status,
timestamp=timestamp
)
self.add_or_update_order(dummy_order)

View File

@ -1,10 +1,12 @@
from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from storage.models import Position
class PositionsPanel(QWidget): class PositionsPanel(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.init_ui() self.init_ui()
self.position_rows = {} # To keep track of rows by symbol
def init_ui(self): def init_ui(self):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@ -24,23 +26,42 @@ class PositionsPanel(QWidget):
layout.addStretch(1) # Pushes content to the top layout.addStretch(1) # Pushes content to the top
self.setLayout(layout) self.setLayout(layout)
def update_position(self, symbol, quantity, avg_price, current_price, market_value, unrealized_pnl): def add_or_update_position(self, position: Position):
# Check if position already exists if not position.is_open: # Remove closed positions from the UI
for row in range(self.positions_table.rowCount()): if position.symbol in self.position_rows:
if self.positions_table.item(row, 0).text() == symbol: row_to_remove = self.position_rows.pop(position.symbol)
self.positions_table.setItem(row, 1, QTableWidgetItem(str(quantity))) self.positions_table.removeRow(row_to_remove)
self.positions_table.setItem(row, 2, QTableWidgetItem(str(avg_price))) # Update row indices after removal
self.positions_table.setItem(row, 3, QTableWidgetItem(str(current_price))) for symbol, row_idx in self.position_rows.items():
self.positions_table.setItem(row, 4, QTableWidgetItem(str(market_value))) if row_idx > row_to_remove:
self.positions_table.setItem(row, 5, QTableWidgetItem(str(unrealized_pnl))) self.position_rows[symbol] = row_idx - 1
return return
# Add new position if not found if position.symbol in self.position_rows:
row_position = self.positions_table.rowCount() row_position = self.position_rows[position.symbol]
self.positions_table.insertRow(row_position) else:
self.positions_table.setItem(row_position, 0, QTableWidgetItem(symbol)) row_position = self.positions_table.rowCount()
self.positions_table.setItem(row_position, 1, QTableWidgetItem(str(quantity))) self.positions_table.insertRow(row_position)
self.positions_table.setItem(row_position, 2, QTableWidgetItem(str(avg_price))) self.position_rows[position.symbol] = row_position
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, 0, QTableWidgetItem(position.symbol))
self.positions_table.setItem(row_position, 5, QTableWidgetItem(str(unrealized_pnl))) 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)

View File

@ -1,7 +1,11 @@
from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox
from PySide6.QtCore import Qt from PySide6.QtCore import Qt, Signal
class StrategyPanel(QWidget): class StrategyPanel(QWidget):
strategy_selected = Signal(str)
start_strategy_requested = Signal(str)
stop_strategy_requested = Signal(str)
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.init_ui() self.init_ui()
@ -20,14 +24,19 @@ class StrategyPanel(QWidget):
strategy_control_layout.addWidget(QLabel("Select Strategy:")) strategy_control_layout.addWidget(QLabel("Select Strategy:"))
self.strategy_selector = QComboBox() self.strategy_selector = QComboBox()
self.strategy_selector.addItem("Simple Crossover") # Placeholder strategy self.strategy_selector.addItem("None")
self.strategy_selector.addItem("Intraday FNO") # Placeholder strategy 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) strategy_control_layout.addWidget(self.strategy_selector)
self.start_button = QPushButton("Start Strategy") 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) strategy_control_layout.addWidget(self.start_button)
self.stop_button = QPushButton("Stop Strategy") self.stop_button = QPushButton("Stop Strategy")
self.stop_button.clicked.connect(self._on_stop_button_clicked)
self.stop_button.setEnabled(False) # Initially disabled self.stop_button.setEnabled(False) # Initially disabled
strategy_control_layout.addWidget(self.stop_button) strategy_control_layout.addWidget(self.stop_button)
@ -42,4 +51,36 @@ class StrategyPanel(QWidget):
layout.addLayout(status_layout) layout.addLayout(status_layout)
layout.addStretch(1) # Pushes content to the top layout.addStretch(1) # Pushes content to the top
self.setLayout(layout) 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)

View File

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