Auto commit: 2025-12-27T04:26:41.696Z
This commit is contained in:
parent
5836c16118
commit
c3681ec74b
@ -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.")
|
||||||
|
|||||||
@ -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)
|
||||||
@ -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}")
|
||||||
|
|||||||
@ -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
|
||||||
@ -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.")
|
||||||
56
algo_trader/setup_and_run.bat
Normal file
56
algo_trader/setup_and_run.bat
Normal 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
|
||||||
@ -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)
|
||||||
@ -48,3 +53,13 @@ class DashboardPanel(QWidget):
|
|||||||
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))
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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_order(self, order_id, symbol, order_type, side, quantity, price, status, timestamp):
|
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()
|
row_position = self.orders_table.rowCount()
|
||||||
self.orders_table.insertRow(row_position)
|
self.orders_table.insertRow(row_position)
|
||||||
self.orders_table.setItem(row_position, 0, QTableWidgetItem(str(order_id)))
|
self.order_rows[order.broker_order_id] = row_position
|
||||||
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, 0, QTableWidgetItem(str(order.broker_order_id)))
|
||||||
self.orders_table.setItem(row_position, 3, QTableWidgetItem(side))
|
self.orders_table.setItem(row_position, 1, QTableWidgetItem(order.symbol))
|
||||||
self.orders_table.setItem(row_position, 4, QTableWidgetItem(str(quantity)))
|
self.orders_table.setItem(row_position, 2, QTableWidgetItem(order.order_type))
|
||||||
self.orders_table.setItem(row_position, 5, QTableWidgetItem(str(price)))
|
self.orders_table.setItem(row_position, 3, QTableWidgetItem(order.side))
|
||||||
self.orders_table.setItem(row_position, 6, QTableWidgetItem(status))
|
self.orders_table.setItem(row_position, 4, QTableWidgetItem(str(order.quantity)))
|
||||||
self.orders_table.setItem(row_position, 7, QTableWidgetItem(str(timestamp)))
|
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):
|
||||||
|
# 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)
|
||||||
@ -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.position_rows[position.symbol]
|
||||||
|
else:
|
||||||
row_position = self.positions_table.rowCount()
|
row_position = self.positions_table.rowCount()
|
||||||
self.positions_table.insertRow(row_position)
|
self.positions_table.insertRow(row_position)
|
||||||
self.positions_table.setItem(row_position, 0, QTableWidgetItem(symbol))
|
self.position_rows[position.symbol] = row_position
|
||||||
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, 0, QTableWidgetItem(position.symbol))
|
||||||
self.positions_table.setItem(row_position, 3, QTableWidgetItem(str(current_price)))
|
self.positions_table.setItem(row_position, 1, QTableWidgetItem(str(position.quantity)))
|
||||||
self.positions_table.setItem(row_position, 4, QTableWidgetItem(str(market_value)))
|
self.positions_table.setItem(row_position, 2, QTableWidgetItem(str(position.average_price)))
|
||||||
self.positions_table.setItem(row_position, 5, QTableWidgetItem(str(unrealized_pnl)))
|
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)
|
||||||
@ -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)
|
||||||
|
|
||||||
@ -43,3 +52,35 @@ class StrategyPanel(QWidget):
|
|||||||
|
|
||||||
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)
|
||||||
@ -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()
|
||||||
Loading…
x
Reference in New Issue
Block a user