Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
wiseplat
GitHub Repository: wiseplat/python-code
Path: blob/master/ invest-robot-contest_investRobot-master/robotlib/stats.py
5929 views
1
from __future__ import annotations
2
3
import datetime
4
import logging
5
import pickle
6
import uuid
7
8
from abc import ABC, abstractmethod
9
from dataclasses import asdict
10
11
import pandas as pd
12
13
from tinkoff.invest import OrderState, Instrument, OrderDirection, Quotation, MoneyValue, OrderExecutionReportStatus, \
14
OrderType
15
16
from robotlib.money import Money
17
18
19
class TradeStatisticsAnalyzer:
20
PENDING_ORDER_STATUSES = [
21
OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_NEW,
22
OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_PARTIALLYFILL
23
]
24
25
trades: dict[str, OrderState]
26
positions: int
27
money: float
28
instrument_info: Instrument
29
logger: logging.Logger
30
31
def __init__(self, positions: int, money: float, instrument_info: Instrument, logger: logging.Logger):
32
self.trades = {}
33
self.positions = positions
34
self.money = money
35
self.instrument_info = instrument_info
36
self.logger = logger
37
38
def add_trade(self, trade: OrderState) -> None:
39
self.logger.debug(f'Updating balance. Current state: [positions={self.positions} money={self.money}]. '
40
f'trade: {trade}')
41
42
if trade.order_id in self.trades:
43
trade.direction = self.trades[trade.order_id].direction
44
sign = 1 if trade.direction == OrderDirection.ORDER_DIRECTION_BUY else -1
45
self.positions += (trade.lots_executed - self.trades[trade.order_id].lots_executed) * sign
46
self.money -= (self.convert_from_quotation(trade.total_order_amount)
47
- self.convert_from_quotation(self.trades[trade.order_id].total_order_amount)) * sign
48
else:
49
sign = 1 if trade.direction == OrderDirection.ORDER_DIRECTION_BUY else -1
50
self.positions += trade.lots_executed * sign
51
self.money -= self.convert_from_quotation(trade.total_order_amount) * sign
52
53
self.trades[trade.order_id] = trade
54
self.logger.debug(f'Updating balance. New state: [positions={self.positions} money={self.money}]')
55
56
def cancel_order(self, order_id: str):
57
self.trades.pop(order_id)
58
59
def get_positions(self) -> int:
60
return self.positions
61
62
def get_money(self) -> float:
63
return self.money
64
65
def get_pending_orders(self) -> list[OrderState]:
66
return [trade for trade in self.trades.values() if trade.execution_report_status in self.PENDING_ORDER_STATUSES]
67
68
def save_to_file(self, filename: str) -> None:
69
with open(filename, 'wb') as file:
70
pickle.dump(obj=self, file=file, protocol=pickle.HIGHEST_PROTOCOL)
71
72
@staticmethod
73
def load_from_file(filename: str) -> TradeStatisticsAnalyzer:
74
with open(filename, 'rb') as file:
75
return pickle.load(file)
76
77
@staticmethod
78
def convert_from_quotation(amount: Quotation | MoneyValue) -> float | None:
79
if amount is None:
80
return None
81
return amount.units + amount.nano / (10 ** 9)
82
83
def add_backtest_trade(self, quantity: int, price: Quotation, direction: OrderDirection):
84
if quantity == 0:
85
return
86
price_money = MoneyValue('RUB', price.units, price.nano)
87
zero_money = MoneyValue('RUB', 0, 0)
88
self.add_trade(OrderState(
89
order_id=str(uuid.uuid4()),
90
execution_report_status=OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_FILL,
91
lots_requested=quantity,
92
lots_executed=quantity,
93
initial_order_price=price_money,
94
executed_order_price=price_money,
95
total_order_amount=(Money(price) * quantity).to_money_value('RUB'),
96
average_position_price=price_money,
97
initial_commission=zero_money,
98
executed_commission=zero_money,
99
figi=self.instrument_info.figi,
100
direction=direction,
101
initial_security_price=price_money,
102
stages=[],
103
service_commission=zero_money,
104
currency=price_money.currency,
105
order_type=OrderType.ORDER_TYPE_MARKET,
106
order_date=datetime.datetime.now()
107
))
108
109
def get_report(self, processors: list[TradeStatisticsProcessorBase] = None,
110
calculators: list[TradeStatisticsCalculatorBase] = None)\
111
-> tuple[dict[str, any], pd.DataFrame]:
112
df = pd.DataFrame(map(asdict, self.trades.values())) # pylint:disable=invalid-name
113
df['average_position_price'] = df['average_position_price'].apply(lambda x: x['units'] + x['nano'] / (10 ** 9))
114
df['total_order_amount'] = df['total_order_amount'].apply(lambda x: x['units'] + x['nano'] / (10 ** 9))
115
df['sign'] = 3 - df['direction'] * 2
116
117
for processor in processors or []:
118
df = processor.process(df) # pylint:disable=invalid-name
119
120
stats = {}
121
for calculator in calculators or []:
122
stats |= calculator.calculate(df)
123
124
return stats, df
125
126
127
class TradeStatisticsProcessorBase(ABC): # pylint:disable=too-few-public-methods
128
@abstractmethod
129
def process(self, df: pd.DataFrame) -> pd.DataFrame: # pylint:disable=invalid-name
130
raise NotImplementedError()
131
132
133
class TradeStatisticsCalculatorBase(ABC): # pylint:disable=too-few-public-methods
134
@abstractmethod
135
def calculate(self, df: pd.DataFrame) -> dict[str, any]: # pylint:disable=invalid-name
136
raise NotImplementedError()
137
138
139
class BalanceProcessor(TradeStatisticsProcessorBase): # pylint:disable=too-few-public-methods
140
def process(self, df: pd.DataFrame) -> pd.DataFrame:
141
df['balance'] = -(df['total_order_amount'] * df['sign']).cumsum()
142
df['instrument_balance'] = (df['lots_executed'] * df['sign']).cumsum()
143
return df
144
145
146
class BalanceCalculator(TradeStatisticsCalculatorBase): # pylint:disable=too-few-public-methods
147
def calculate(self, df: pd.DataFrame) -> dict[str, any]:
148
final_balance = df['balance'][len(df) - 1]
149
final_instrument_balance = df['instrument_balance'][len(df) - 1]
150
final_price = df['average_position_price'][len(df) - 1]
151
return {
152
'final_balance': final_balance,
153
'max_loss': -df['balance'].min(),
154
'final_instrument_balance': final_instrument_balance,
155
'income': final_balance + final_instrument_balance * final_price
156
}
157
158