Реализация AI-робоадвайзера для инвестиций
Робоадвайзер — автоматизированная система управления инвестиционным портфелем на основе риск-профиля клиента. Betterment, Wealthfront, Тинькофф Робоинвестирование — все они используют современную теорию портфеля (MPT) + ML для ребалансировки и персонализации. Регуляторные требования делают задачу сложнее: нужны explainable recommendations.
Профилирование инвестора
from anthropic import Anthropic
import numpy as np
import pandas as pd
from scipy.optimize import minimize
class InvestorProfiler:
def __init__(self):
self.llm = Anthropic()
def assess_risk_profile(self, questionnaire_answers: dict) -> dict:
"""Определение риск-профиля через анкету"""
# Скоринг ответов
risk_score = 0
max_score = 0
scoring_rules = {
'age': lambda x: max(0, (65 - x) / 45 * 20), # Молодой = выше риск
'investment_horizon': {'<1y': 5, '1-3y': 10, '3-5y': 15, '>5y': 20},
'risk_tolerance': {'conservative': 5, 'moderate': 12, 'aggressive': 20},
'income_stability': {'unstable': 0, 'stable': 5, 'very_stable': 10},
'loss_reaction': {'sell_all': 0, 'sell_some': 5, 'hold': 10, 'buy_more': 15},
}
for question, rules in scoring_rules.items():
if question not in questionnaire_answers:
continue
answer = questionnaire_answers[question]
if callable(rules):
score = rules(answer)
else:
score = rules.get(answer, 0)
risk_score += score
max_score += 20
normalized = risk_score / max_score
# Категории риска
if normalized < 0.3:
risk_category = 'conservative'
equity_allocation = 20
elif normalized < 0.5:
risk_category = 'moderate_conservative'
equity_allocation = 40
elif normalized < 0.7:
risk_category = 'moderate'
equity_allocation = 60
elif normalized < 0.85:
risk_category = 'moderate_aggressive'
equity_allocation = 75
else:
risk_category = 'aggressive'
equity_allocation = 90
profile = {
'risk_score': normalized,
'risk_category': risk_category,
'equity_allocation': equity_allocation,
'bond_allocation': 100 - equity_allocation - 5,
'cash_allocation': 5
}
# LLM объяснение профиля
profile['explanation'] = self._explain_profile(profile, questionnaire_answers)
return profile
def _explain_profile(self, profile: dict, answers: dict) -> str:
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=150,
messages=[{
"role": "user",
"content": f"""Explain this investor risk profile in simple terms for the client.
Profile: {profile['risk_category']}, equity: {profile['equity_allocation']}%
Key factors from questionnaire: {answers}
2-3 sentences. No jargon. Explain why this allocation suits them."""
}]
)
return response.content[0].text
class PortfolioOptimizer:
"""Оптимизация портфеля по Марковицу с ML-предсказанием доходностей"""
def optimize(self, expected_returns: np.ndarray,
covariance_matrix: np.ndarray,
target_return: float = None,
max_volatility: float = None,
asset_names: list = None,
constraints: dict = None) -> dict:
"""Оптимизация Марковица"""
n_assets = len(expected_returns)
def portfolio_variance(weights):
return weights @ covariance_matrix @ weights
def portfolio_return(weights):
return weights @ expected_returns
def neg_sharpe(weights, risk_free_rate=0.05):
ret = portfolio_return(weights)
vol = np.sqrt(portfolio_variance(weights))
return -(ret - risk_free_rate / 252) / vol
# Ограничения
scipy_constraints = [
{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}
]
if target_return:
scipy_constraints.append({
'type': 'eq',
'fun': lambda w: portfolio_return(w) - target_return
})
# Границы весов
min_weight = constraints.get('min_weight', 0.02) if constraints else 0.02
max_weight = constraints.get('max_weight', 0.40) if constraints else 0.40
bounds = [(min_weight, max_weight)] * n_assets
# Оптимизация
result = minimize(
neg_sharpe if not target_return else portfolio_variance,
x0=np.ones(n_assets) / n_assets,
method='SLSQP',
bounds=bounds,
constraints=scipy_constraints,
options={'ftol': 1e-9, 'maxiter': 1000}
)
weights = result.x
ret = portfolio_return(weights)
vol = np.sqrt(portfolio_variance(weights))
sharpe = (ret - 0.05/252) / vol * np.sqrt(252)
return {
'weights': {(asset_names[i] if asset_names else f'asset_{i}'): float(w)
for i, w in enumerate(weights)},
'expected_annual_return': float(ret * 252),
'annual_volatility': float(vol * np.sqrt(252)),
'sharpe_ratio': float(sharpe)
}
class RebalancingEngine:
"""Автоматическая ребалансировка с учётом транзакционных издержек"""
def check_rebalancing_needed(self, current_weights: dict,
target_weights: dict,
threshold: float = 0.05) -> bool:
"""Нужна ли ребалансировка"""
for asset, target_w in target_weights.items():
current_w = current_weights.get(asset, 0)
if abs(current_w - target_w) > threshold:
return True
return False
def generate_rebalancing_orders(self, portfolio_value: float,
current_weights: dict,
target_weights: dict,
min_trade_size: float = 10) -> list[dict]:
"""Генерация ордеров ребалансировки"""
orders = []
for asset, target_w in target_weights.items():
current_w = current_weights.get(asset, 0)
delta_w = target_w - current_w
trade_value = abs(delta_w * portfolio_value)
if trade_value >= min_trade_size:
orders.append({
'asset': asset,
'action': 'BUY' if delta_w > 0 else 'SELL',
'value': trade_value,
'weight_delta': delta_w
})
return orders
Оптимизированный портфель с правильным риск-профилем при горизонте 10+ лет: ожидаемая доходность на 1.5-2.5% выше субоптимального. Средняя стоимость AuM для робоадвайзера: 0.25-0.50% в год (против 1-2% у ручного управления). Регуляторные требования в РФ: ИИС, НДФЛ-1, Д2, соответствие риск-профиля при сделках.







