Path: blob/master/ invest-robot-contest_investRobot-master/robotlib/stats.py
5929 views
from __future__ import annotations12import datetime3import logging4import pickle5import uuid67from abc import ABC, abstractmethod8from dataclasses import asdict910import pandas as pd1112from tinkoff.invest import OrderState, Instrument, OrderDirection, Quotation, MoneyValue, OrderExecutionReportStatus, \13OrderType1415from robotlib.money import Money161718class TradeStatisticsAnalyzer:19PENDING_ORDER_STATUSES = [20OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_NEW,21OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_PARTIALLYFILL22]2324trades: dict[str, OrderState]25positions: int26money: float27instrument_info: Instrument28logger: logging.Logger2930def __init__(self, positions: int, money: float, instrument_info: Instrument, logger: logging.Logger):31self.trades = {}32self.positions = positions33self.money = money34self.instrument_info = instrument_info35self.logger = logger3637def add_trade(self, trade: OrderState) -> None:38self.logger.debug(f'Updating balance. Current state: [positions={self.positions} money={self.money}]. '39f'trade: {trade}')4041if trade.order_id in self.trades:42trade.direction = self.trades[trade.order_id].direction43sign = 1 if trade.direction == OrderDirection.ORDER_DIRECTION_BUY else -144self.positions += (trade.lots_executed - self.trades[trade.order_id].lots_executed) * sign45self.money -= (self.convert_from_quotation(trade.total_order_amount)46- self.convert_from_quotation(self.trades[trade.order_id].total_order_amount)) * sign47else:48sign = 1 if trade.direction == OrderDirection.ORDER_DIRECTION_BUY else -149self.positions += trade.lots_executed * sign50self.money -= self.convert_from_quotation(trade.total_order_amount) * sign5152self.trades[trade.order_id] = trade53self.logger.debug(f'Updating balance. New state: [positions={self.positions} money={self.money}]')5455def cancel_order(self, order_id: str):56self.trades.pop(order_id)5758def get_positions(self) -> int:59return self.positions6061def get_money(self) -> float:62return self.money6364def get_pending_orders(self) -> list[OrderState]:65return [trade for trade in self.trades.values() if trade.execution_report_status in self.PENDING_ORDER_STATUSES]6667def save_to_file(self, filename: str) -> None:68with open(filename, 'wb') as file:69pickle.dump(obj=self, file=file, protocol=pickle.HIGHEST_PROTOCOL)7071@staticmethod72def load_from_file(filename: str) -> TradeStatisticsAnalyzer:73with open(filename, 'rb') as file:74return pickle.load(file)7576@staticmethod77def convert_from_quotation(amount: Quotation | MoneyValue) -> float | None:78if amount is None:79return None80return amount.units + amount.nano / (10 ** 9)8182def add_backtest_trade(self, quantity: int, price: Quotation, direction: OrderDirection):83if quantity == 0:84return85price_money = MoneyValue('RUB', price.units, price.nano)86zero_money = MoneyValue('RUB', 0, 0)87self.add_trade(OrderState(88order_id=str(uuid.uuid4()),89execution_report_status=OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_FILL,90lots_requested=quantity,91lots_executed=quantity,92initial_order_price=price_money,93executed_order_price=price_money,94total_order_amount=(Money(price) * quantity).to_money_value('RUB'),95average_position_price=price_money,96initial_commission=zero_money,97executed_commission=zero_money,98figi=self.instrument_info.figi,99direction=direction,100initial_security_price=price_money,101stages=[],102service_commission=zero_money,103currency=price_money.currency,104order_type=OrderType.ORDER_TYPE_MARKET,105order_date=datetime.datetime.now()106))107108def get_report(self, processors: list[TradeStatisticsProcessorBase] = None,109calculators: list[TradeStatisticsCalculatorBase] = None)\110-> tuple[dict[str, any], pd.DataFrame]:111df = pd.DataFrame(map(asdict, self.trades.values())) # pylint:disable=invalid-name112df['average_position_price'] = df['average_position_price'].apply(lambda x: x['units'] + x['nano'] / (10 ** 9))113df['total_order_amount'] = df['total_order_amount'].apply(lambda x: x['units'] + x['nano'] / (10 ** 9))114df['sign'] = 3 - df['direction'] * 2115116for processor in processors or []:117df = processor.process(df) # pylint:disable=invalid-name118119stats = {}120for calculator in calculators or []:121stats |= calculator.calculate(df)122123return stats, df124125126class TradeStatisticsProcessorBase(ABC): # pylint:disable=too-few-public-methods127@abstractmethod128def process(self, df: pd.DataFrame) -> pd.DataFrame: # pylint:disable=invalid-name129raise NotImplementedError()130131132class TradeStatisticsCalculatorBase(ABC): # pylint:disable=too-few-public-methods133@abstractmethod134def calculate(self, df: pd.DataFrame) -> dict[str, any]: # pylint:disable=invalid-name135raise NotImplementedError()136137138class BalanceProcessor(TradeStatisticsProcessorBase): # pylint:disable=too-few-public-methods139def process(self, df: pd.DataFrame) -> pd.DataFrame:140df['balance'] = -(df['total_order_amount'] * df['sign']).cumsum()141df['instrument_balance'] = (df['lots_executed'] * df['sign']).cumsum()142return df143144145class BalanceCalculator(TradeStatisticsCalculatorBase): # pylint:disable=too-few-public-methods146def calculate(self, df: pd.DataFrame) -> dict[str, any]:147final_balance = df['balance'][len(df) - 1]148final_instrument_balance = df['instrument_balance'][len(df) - 1]149final_price = df['average_position_price'][len(df) - 1]150return {151'final_balance': final_balance,152'max_loss': -df['balance'].min(),153'final_instrument_balance': final_instrument_balance,154'income': final_balance + final_instrument_balance * final_price155}156157158