Refactor code structure for improved readability and maintainability
This commit is contained in:
20
app/config.py
Normal file
20
app/config.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from decimal import Decimal
|
||||
|
||||
TARGET_WEIGHTS = {
|
||||
"TPAY": Decimal("0.15"),
|
||||
"RU000A10B7T7": Decimal("0.05"),
|
||||
"SFIN": Decimal("0.02"),
|
||||
"RU000A107AM4": Decimal("0.06"),
|
||||
"T": Decimal("0.04"),
|
||||
"RUB000UTSTOM": Decimal("0.03"),
|
||||
"SBERP": Decimal("0.04"),
|
||||
"SU26212RMFS9": Decimal("0.15"),
|
||||
"SBRB": Decimal("0.017"),
|
||||
"TGLD@": Decimal("0.10"),
|
||||
"TBRU@": Decimal("0.03"),
|
||||
"TATNP": Decimal("0.013"),
|
||||
"TMOS@": Decimal("0.25"),
|
||||
"ROSN": Decimal("0.05"),
|
||||
}
|
||||
|
||||
CORRIDOR = Decimal("0.02") # 3% коридор отклонения от целевой доли
|
||||
32
app/main.py
Normal file
32
app/main.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from t_tech.invest import Client
|
||||
from t_tech.invest.sandbox.client import SandboxClient
|
||||
|
||||
from rebalance import RebalanceBot
|
||||
from config import TARGET_WEIGHTS, CORRIDOR
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main():
|
||||
with SandboxClient(token=os.getenv("TINVEST_TOKEN")) as client: # type: ignore
|
||||
bot = RebalanceBot(
|
||||
client=client,
|
||||
account_id="bb29bb79-8cf1-42ba-843f-47ef76c5b7c0",
|
||||
target_weights=TARGET_WEIGHTS,
|
||||
corridor=CORRIDOR,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
plan = bot.calculate_rebalance(bot.fetch_portfolio())
|
||||
|
||||
sales = [trade for trade in plan if trade["action"] == "SELL"]
|
||||
buys = [trade for trade in plan if trade["action"] == "BUY"]
|
||||
|
||||
bot.execute_orders(sales, "ПРОДАЖИ")
|
||||
bot.execute_orders(buys, "ПОКУПКИ")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
122
app/rebalance.py
Normal file
122
app/rebalance.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from decimal import Decimal
|
||||
from t_tech.invest import OrderDirection, OrderType
|
||||
|
||||
from t_tech.invest.utils import quotation_to_decimal as to_dec
|
||||
import uuid
|
||||
|
||||
|
||||
class RebalanceBot:
|
||||
def __init__(
|
||||
self,
|
||||
client,
|
||||
account_id: str,
|
||||
target_weights: dict,
|
||||
corridor: Decimal,
|
||||
dry_run=True,
|
||||
):
|
||||
|
||||
self.client = client
|
||||
self.account_id = account_id
|
||||
self.target_weights = target_weights
|
||||
self.corridor = corridor
|
||||
self.portfolio_value = Decimal("0")
|
||||
self.instrument_cache = {}
|
||||
self.dry_run = dry_run
|
||||
|
||||
def fetch_portfolio(self):
|
||||
account = self.client.operations.get_portfolio(account_id=self.account_id)
|
||||
self.portfolio_value = to_dec(account.total_amount_portfolio)
|
||||
|
||||
return account.positions
|
||||
|
||||
def get_instrument_data(self, instrument_uid):
|
||||
if instrument_uid not in self.instrument_cache:
|
||||
resp = self.client.instruments.find_instrument(query=instrument_uid)
|
||||
self.instrument_cache[instrument_uid] = resp.instruments[0]
|
||||
|
||||
return self.instrument_cache[instrument_uid]
|
||||
|
||||
def calculate_rebalance(self, positions):
|
||||
plan = []
|
||||
for pos in positions:
|
||||
ticker = pos.ticker
|
||||
uid = pos.instrument_uid
|
||||
asset_type = pos.instrument_type
|
||||
price = to_dec(pos.current_price)
|
||||
qty = to_dec(pos.quantity)
|
||||
|
||||
if asset_type == "currency":
|
||||
continue
|
||||
elif asset_type == "bond":
|
||||
instrument_info = self.get_instrument_data(uid)
|
||||
nkd = to_dec(pos.current_nkd)
|
||||
current_value = (price + nkd) * qty
|
||||
else:
|
||||
instrument_info = self.get_instrument_data(uid)
|
||||
current_value = price * qty
|
||||
|
||||
target_weight = self.target_weights.get(ticker, Decimal("0"))
|
||||
current_weight = current_value / self.portfolio_value
|
||||
delta = target_weight - current_weight
|
||||
money_delta = delta * self.portfolio_value
|
||||
print(
|
||||
f"{ticker}, Тип: {asset_type}, Текущая доля {current_weight:.2%}, Цель {target_weight:.2%}, Дельта: {delta:.2%}, Дельта в рублях: {money_delta:.6}"
|
||||
)
|
||||
|
||||
if abs(delta) > self.corridor:
|
||||
money_to_trade = self.portfolio_value * delta
|
||||
one_lot_price = price * instrument_info.lot
|
||||
lots = int(money_to_trade / one_lot_price)
|
||||
|
||||
if lots != 0:
|
||||
plan.append(
|
||||
{
|
||||
"ticker": ticker,
|
||||
"uid": uid,
|
||||
"action": "BUY" if lots > 0 else "SELL",
|
||||
"lots": abs(lots),
|
||||
"delta_pct": delta * 100,
|
||||
}
|
||||
)
|
||||
|
||||
print(
|
||||
f"{ticker:12} | Доля: {current_weight:6.2%} | Цель: {target_weight:6.2%}"
|
||||
)
|
||||
|
||||
return plan
|
||||
|
||||
def execute_orders(self, trades, group_name):
|
||||
if not trades:
|
||||
return
|
||||
|
||||
print(f"\n--- Исполнение блока: {group_name} ---")
|
||||
|
||||
for trade in trades:
|
||||
action_ru = "КУПИТЬ" if trade["action"] == "BUY" else "ПРОДАТЬ"
|
||||
|
||||
if self.dry_run:
|
||||
print(
|
||||
f"[СИМУЛЯЦИЯ] {action_ru} {trade['ticker']}: {trade['lots']} лотов"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
direction = (
|
||||
OrderDirection.ORDER_DIRECTION_BUY
|
||||
if trade["action"] == "BUY"
|
||||
else OrderDirection.ORDER_DIRECTION_SELL
|
||||
)
|
||||
|
||||
response = self.client.orders.post_order(
|
||||
figi=trade["figi"],
|
||||
quantity=int(trade["lots"]),
|
||||
direction=direction,
|
||||
account_id=self.account_id,
|
||||
order_type=OrderType.ORDER_TYPE_MARKET,
|
||||
order_id=str(uuid.uuid4()),
|
||||
)
|
||||
print(
|
||||
f"[ИСПОЛНЕНО]\n{trade['ticker']} на {trade['lots']} лотов. ID: {response.order_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[ОШИБКА]\nПри сделке с {trade['ticker']}: {e}")
|
||||
Reference in New Issue
Block a user