Инвестиционные стратегии (Investment Strategies Endpoints)¶
Аудитория: разработчики, frontend инженеры Последнее обновление: 2025-11-17 Краткое содержание: Полная документация Strategy endpoints — получение доступных стратегий, детали стратегий, административное управление. Включает 4 реальных backend endpoints (2 user + 2 admin) с comprehensive примерами React интеграции.
Обзор endpoints¶
| Метод | Endpoint | Описание | Требуется Auth | Роль |
|---|---|---|---|---|
| GET | /api/user/strategies |
Список доступных стратегий | ✅ Да | User |
| GET | /api/user/strategies/:id |
Получить детали стратегии | ✅ Да | User |
| GET | /api/admin/strategies |
Список стратегий (admin view) | ✅ Да | Admin |
| PUT | /api/admin/strategies/:id |
Обновить стратегию (MVP: config-based) | ✅ Да | Admin |
GET /api/user/strategies¶
Получить список всех доступных инвестиционных стратегий для пользователя.
Запрос¶
Заголовки:
| Заголовок | Значение | Обязательно | Описание |
|---|---|---|---|
Authorization |
Bearer <token> |
✅ Да | User JWT токен |
Ответ¶
Успех (200 OK):
{
"success": true,
"data": {
"strategies": [
{
"id": "conservative-5",
"name": "Conservative 5% APY Strategy",
"description": "Стратегия с минимальным риском, фокусирующаяся на проверенных протоколах стейблкоинов с минимальной волатильностью",
"riskLevel": "low",
"expectedYield": "5.00",
"minInvestment": "10.00",
"maxInvestment": "1000000.00",
"status": "active",
"isActive": true
},
{
"id": "balanced-10",
"name": "Balanced 10% APY Strategy",
"description": "Оптимальный баланс риск-доходность через диверсифицированные DeFi протоколы",
"riskLevel": "medium",
"expectedYield": "10.00",
"minInvestment": "10.00",
"maxInvestment": "1000000.00",
"status": "active",
"isActive": true
},
{
"id": "aggressive-20",
"name": "Aggressive 20% APY Strategy",
"description": "Максимальная доходность через высокорисковые DeFi протоколы и наложение yield",
"riskLevel": "high",
"expectedYield": "20.00",
"minInvestment": "10.00",
"maxInvestment": "1000000.00",
"status": "active",
"isActive": true
}
],
"debug": {
"strategiesCount": 3,
"configType": "*services.StrategyService",
"endpointCalled": true,
"serverTimestamp": "2025-10-06T12:34:56Z"
}
},
"timestamp": "2025-10-06T12:34:56Z"
}
Поля ответа:
| Поле | Тип | Описание |
|---|---|---|
data.strategies[] |
array | Массив доступных стратегий |
data.strategies[].id |
string | Уникальный идентификатор стратегии |
data.strategies[].name |
string | Название стратегии |
data.strategies[].description |
string | Описание стратегии |
data.strategies[].riskLevel |
string | Уровень риска: low, medium, high |
data.strategies[].expectedYield |
string | Целевая годовая доходность (SafeDecimal) |
data.strategies[].minInvestment |
string | Минимальная сумма инвестиции (SafeDecimal) |
data.strategies[].maxInvestment |
string | Максимальная сумма инвестиции (SafeDecimal) |
data.strategies[].status |
string | Статус стратегии (всегда active в текущей версии) |
data.strategies[].isActive |
boolean | Активна ли стратегия для инвестиций |
data.debug |
object | Отладочная информация (только в development) |
data.debug.strategiesCount |
number | Количество стратегий в ответе |
data.debug.endpointCalled |
boolean | Подтверждение вызова endpoint |
data.debug.serverTimestamp |
string | Timestamp сервера |
Статусы стратегий:
| Статус | Описание | Доступна для инвестиций? |
|---|---|---|
active |
Стратегия доступна для инвестиций | ✅ Да |
Ошибки:
| Код статуса | Код ошибки | Описание |
|---|---|---|
| 401 | TOKEN_EXPIRED |
JWT токен истёк |
| 401 | TOKEN_INVALID |
Неверная подпись JWT |
| 500 | INTERNAL_ERROR |
Ошибка получения стратегий из конфигурации |
Пример cURL¶
TOKEN="eyJhbGciOiJIUzI1NiIs..."
curl -X GET https://app.saga.surf/api/user/strategies \
-H "Authorization: Bearer $TOKEN"
Пример TypeScript¶
interface Strategy {
id: string;
name: string;
description: string;
riskLevel: 'low' | 'medium' | 'high';
expectedYield: string;
minInvestment: string;
maxInvestment: string;
status: string;
isActive: boolean;
}
interface StrategiesResponse {
strategies: Strategy[];
debug: {
strategiesCount: number;
configType: string;
endpointCalled: boolean;
serverTimestamp: string;
};
}
const fetchStrategies = async (): Promise<Strategy[]> => {
const token = localStorage.getItem('saga_access_token');
const response = await fetch('/api/user/strategies', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch strategies: ${response.statusText}`);
}
const { data } = await response.json();
return data.strategies;
};
// Использование
const strategies = await fetchStrategies();
console.log(`Доступно ${strategies.length} стратегий`);
strategies.forEach(strategy => {
console.log(`\n${strategy.name}`);
console.log(` APY: ${strategy.expectedYield}%`);
console.log(` Риск: ${strategy.riskLevel}`);
console.log(` Диапазон инвестиции: $${strategy.minInvestment} - $${strategy.maxInvestment}`);
});
GET /api/user/strategies/:id¶
Получить детальную информацию о конкретной стратегии.
Запрос¶
Path параметры:
| Параметр | Тип | Обязательно | Описание |
|---|---|---|---|
id |
string | ✅ Да | ID стратегии (например, balanced-10, conservative-5, aggressive-20) |
Ответ¶
Успех (200 OK):
{
"success": true,
"data": {
"id": "balanced-10",
"name": "Balanced 10% APY Strategy",
"description": "Оптимальный баланс риск-доходность через диверсифицированное распределение по проверенным DeFi протоколам. Сочетает стабильную доходность от Pendle Foundation с вознаграждениями за ликвидность от Curve и усиленной доходностью от Convex.",
"riskLevel": "medium",
"expectedYield": "10.00",
"minInvestment": "10.00",
"maxInvestment": "1000000.00",
"isActive": true,
"active": true,
"createdAt": "2025-07-01T00:00:00Z",
"updatedAt": "2025-10-06T12:00:00Z"
},
"timestamp": "2025-10-06T12:34:56Z"
}
Поля ответа:
| Поле | Тип | Описание |
|---|---|---|
data.id |
string | Уникальный идентификатор стратегии |
data.name |
string | Полное название стратегии |
data.description |
string | Детальное описание стратегии |
data.riskLevel |
string | Уровень риска: low, medium, high |
data.expectedYield |
string | Целевая годовая доходность (SafeDecimal) |
data.minInvestment |
string | Минимальная сумма инвестиции (SafeDecimal) |
data.maxInvestment |
string | Максимальная сумма инвестиции (SafeDecimal) |
data.isActive |
boolean | Активна ли стратегия |
data.active |
boolean | Дублирует isActive (legacy compatibility) |
data.createdAt |
string | Дата создания стратегии (ISO 8601) |
data.updatedAt |
string | Дата последнего обновления (ISO 8601) |
Ошибки:
| Код статуса | Код ошибки | Описание |
|---|---|---|
| 400 | VALIDATION_ERROR |
Strategy ID не предоставлен |
| 404 | STRATEGY_NOT_FOUND |
Стратегия с указанным ID не существует |
| 401 | TOKEN_EXPIRED |
JWT токен истёк |
| 401 | TOKEN_INVALID |
Неверная подпись JWT |
Пример cURL¶
TOKEN="eyJhbGciOiJIUzI1NiIs..."
curl -X GET https://app.saga.surf/api/user/strategies/balanced-10 \
-H "Authorization: Bearer $TOKEN"
Пример TypeScript¶
interface StrategyDetails {
id: string;
name: string;
description: string;
riskLevel: 'low' | 'medium' | 'high';
expectedYield: string;
minInvestment: string;
maxInvestment: string;
isActive: boolean;
active: boolean;
createdAt: string;
updatedAt: string;
}
const getStrategyDetails = async (strategyId: string): Promise<StrategyDetails> => {
const token = localStorage.getItem('saga_access_token');
const response = await fetch(`/api/user/strategies/${strategyId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Strategy ${strategyId} not found`);
}
throw new Error(`Failed to fetch strategy: ${response.statusText}`);
}
const { data } = await response.json();
return data;
};
// Использование
const strategy = await getStrategyDetails('balanced-10');
console.log(`Стратегия: ${strategy.name}`);
console.log(`APY: ${strategy.expectedYield}%`);
console.log(`Риск: ${strategy.riskLevel}`);
console.log(`Диапазон: $${strategy.minInvestment} - $${strategy.maxInvestment}`);
GET /api/admin/strategies¶
Получить список всех стратегий с административной информацией.
Требуется роль: Admin
Запрос¶
Заголовки:
| Заголовок | Значение | Обязательно | Описание |
|---|---|---|---|
Authorization |
Bearer <admin_token> |
✅ Да | Admin JWT токен |
Ответ¶
Успех (200 OK):
{
"success": true,
"data": [
{
"id": "conservative-5",
"name": "Conservative 5% APY Strategy",
"description": "Стратегия с минимальным риском, фокусирующаяся на проверенных протоколах стейблкоинов",
"riskLevel": "low",
"apy": "5.00",
"minAmount": "10.00",
"maxAmount": "1000000.00",
"isActive": true
},
{
"id": "balanced-10",
"name": "Balanced 10% APY Strategy",
"description": "Оптимальный баланс риск-доходность через диверсифицированные DeFi протоколы",
"riskLevel": "medium",
"apy": "10.00",
"minAmount": "10.00",
"maxAmount": "1000000.00",
"isActive": true
},
{
"id": "aggressive-20",
"name": "Aggressive 20% APY Strategy",
"description": "Максимальная доходность через высокорисковые DeFi протоколы",
"riskLevel": "high",
"apy": "20.00",
"minAmount": "10.00",
"maxAmount": "1000000.00",
"isActive": true
}
],
"timestamp": "2025-10-06T12:34:56Z"
}
Поля ответа:
| Поле | Тип | Описание |
|---|---|---|
data[] |
array | Массив всех стратегий в системе |
data[].id |
string | Уникальный идентификатор стратегии |
data[].name |
string | Название стратегии |
data[].description |
string | Описание стратегии |
data[].riskLevel |
string | Уровень риска: low, medium, high |
data[].apy |
string | Годовая доходность (SafeDecimal) |
data[].minAmount |
string | Минимальная инвестиция (SafeDecimal) |
data[].maxAmount |
string | Максимальная инвестиция (SafeDecimal) |
data[].isActive |
boolean | Активна ли стратегия |
Ошибки:
| Код статуса | Код ошибки | Описание |
|---|---|---|
| 401 | UNAUTHORIZED |
Требуется Admin токен |
| 403 | FORBIDDEN |
Недостаточно прав (не Admin) |
| 500 | INTERNAL_ERROR |
Ошибка получения стратегий |
Пример cURL¶
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIs..."
curl -X GET https://admin.saga.surf/api/admin/strategies \
-H "Authorization: Bearer $ADMIN_TOKEN"
Пример TypeScript¶
interface AdminStrategy {
id: string;
name: string;
description: string;
riskLevel: 'low' | 'medium' | 'high';
apy: string;
minAmount: string;
maxAmount: string;
isActive: boolean;
}
const fetchAdminStrategies = async (): Promise<AdminStrategy[]> => {
const adminToken = localStorage.getItem('admin_auth_token');
const response = await fetch('/api/admin/strategies', {
headers: {
'Authorization': `Bearer ${adminToken}`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch admin strategies: ${response.statusText}`);
}
const { data } = await response.json();
return data;
};
// Использование
const strategies = await fetchAdminStrategies();
console.log(`Всего стратегий в системе: ${strategies.length}`);
strategies.forEach(strategy => {
console.log(`\n${strategy.name}`);
console.log(` ID: ${strategy.id}`);
console.log(` APY: ${strategy.apy}%`);
console.log(` Риск: ${strategy.riskLevel}`);
console.log(` Активна: ${strategy.isActive ? 'Да' : 'Нет'}`);
});
✏️ PUT /api/admin/strategies/:id¶
Обновить конфигурацию стратегии (MVP: config-based, ограниченная функциональность).
Требуется роль: Admin
⚠️ MVP Ограничение: В текущей версии стратегии управляются через конфигурационные файлы. Этот endpoint принимает запросы но не применяет изменения к реальным стратегиям. Возвращает warning о необходимости изменения config файлов вручную.
Запрос¶
PUT /api/admin/strategies/balanced-10
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
{
"isActive": false,
"notes": "Временно отключена для обслуживания"
}
Path параметры:
| Параметр | Тип | Обязательно | Описание |
|---|---|---|---|
id |
string | ✅ Да | ID стратегии для обновления |
Заголовки:
| Заголовок | Значение | Обязательно | Описание |
|---|---|---|---|
Authorization |
Bearer <admin_token> |
✅ Да | Admin JWT токен |
Content-Type |
application/json |
✅ Да | JSON payload |
Тело запроса:
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
isActive |
boolean | ✅ Да | Активность стратегии (true/false) |
notes |
string | ❌ Нет | Примечания об изменении |
Ответ¶
Успех (200 OK):
{
"success": true,
"data": {
"id": "balanced-10",
"isActive": false,
"updatedBy": "admin-uuid-123",
"updatedAt": "2025-10-06T12:34:56Z",
"notes": "Временно отключена для обслуживания",
"warning": "В MVP режиме стратегии управляются через конфигурационные файлы. Для реального изменения необходимо обновить config/limits.yaml и выполнить make generate-config && make restart"
},
"timestamp": "2025-10-06T12:34:56Z"
}
Поля ответа:
| Поле | Тип | Описание |
|---|---|---|
data.id |
string | ID обновленной стратегии |
data.isActive |
boolean | Новое значение активности (из request) |
data.updatedBy |
string | ID администратора выполнившего обновление |
data.updatedAt |
string | Timestamp обновления (ISO 8601) |
data.notes |
string | Примечания администратора |
data.warning |
string | КРИТИЧНО: Предупреждение о MVP ограничениях |
Ошибки:
| Код статуса | Код ошибки | Описание |
|---|---|---|
| 400 | VALIDATION_ERROR |
Невалидное тело запроса (отсутствует isActive) |
| 401 | UNAUTHORIZED |
Требуется Admin токен |
| 403 | FORBIDDEN |
Недостаточно прав (не Admin) |
| 404 | STRATEGY_NOT_FOUND |
Стратегия с указанным ID не существует |
Пример cURL¶
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIs..."
# Отключить стратегию
curl -X PUT https://admin.saga.surf/api/admin/strategies/balanced-10 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"isActive": false,
"notes": "Временно отключена для обслуживания"
}'
# Включить стратегию
curl -X PUT https://admin.saga.surf/api/admin/strategies/balanced-10 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"isActive": true,
"notes": "Обслуживание завершено"
}'
Пример TypeScript¶
interface StrategyUpdateRequest {
isActive: boolean;
notes?: string;
}
interface StrategyUpdateResponse {
id: string;
isActive: boolean;
updatedBy: string;
updatedAt: string;
notes: string;
warning: string;
}
const updateStrategy = async (
strategyId: string,
update: StrategyUpdateRequest
): Promise<StrategyUpdateResponse> => {
const adminToken = localStorage.getItem('admin_auth_token');
const response = await fetch(`/api/admin/strategies/${strategyId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(update)
});
if (!response.ok) {
throw new Error(`Failed to update strategy: ${response.statusText}`);
}
const { data } = await response.json();
// Показать warning пользователю
if (data.warning) {
console.warn('⚠️ MVP Ограничение:', data.warning);
}
return data;
};
// Использование - отключить стратегию
const result = await updateStrategy('balanced-10', {
isActive: false,
notes: 'Временно отключена для обслуживания'
});
console.log(`Стратегия ${result.id} обновлена`);
console.log(`Статус: ${result.isActive ? 'Активна' : 'Отключена'}`);
console.log(`Обновлена: ${result.updatedBy} в ${result.updatedAt}`);
// ⚠️ ВАЖНО: Для реального изменения необходимо обновить config файлы
Use Cases¶
Use Case 1: Strategy Selection Component¶
Сценарий: Пользователь выбирает инвестиционную стратегию при создании инвестиции
React Component:
import React, { useEffect, useState } from 'react';
interface Strategy {
id: string;
name: string;
description: string;
riskLevel: 'low' | 'medium' | 'high';
expectedYield: string;
minInvestment: string;
maxInvestment: string;
}
const StrategySelector: React.FC<{
onStrategySelect: (strategyId: string) => void;
selectedStrategyId?: string;
}> = ({ onStrategySelect, selectedStrategyId }) => {
const [strategies, setStrategies] = useState<Strategy[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchStrategies = async () => {
try {
const token = localStorage.getItem('saga_access_token');
const response = await fetch('/api/user/strategies', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
throw new Error('Failed to fetch strategies');
}
const { data } = await response.json();
setStrategies(data.strategies);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchStrategies();
}, []);
if (loading) return <div>Загрузка стратегий...</div>;
if (error) return <div>Ошибка: {error}</div>;
const getRiskColor = (riskLevel: string) => {
switch (riskLevel) {
case 'low': return 'text-green-600';
case 'medium': return 'text-yellow-600';
case 'high': return 'text-red-600';
default: return 'text-gray-600';
}
};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Выберите стратегию</h3>
{strategies.map((strategy) => (
<div
key={strategy.id}
onClick={() => onStrategySelect(strategy.id)}
className={`
p-4 border-2 rounded-lg cursor-pointer transition-all
${selectedStrategyId === strategy.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}
`}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<h4 className="font-semibold">{strategy.name}</h4>
<p className="text-sm text-gray-600 mt-1">{strategy.description}</p>
</div>
<div className="text-right ml-4">
<div className="text-2xl font-bold text-blue-600">
{strategy.expectedYield}%
</div>
<div className="text-xs text-gray-500">APY</div>
</div>
</div>
<div className="mt-3 flex justify-between items-center text-sm">
<span className={`font-medium ${getRiskColor(strategy.riskLevel)}`}>
Риск: {strategy.riskLevel === 'low' ? 'Низкий' :
strategy.riskLevel === 'medium' ? 'Средний' : 'Высокий'}
</span>
<span className="text-gray-500">
Min: ${strategy.minInvestment} | Max: ${strategy.maxInvestment}
</span>
</div>
</div>
))}
</div>
);
};
export default StrategySelector;
Результат: Пользователь видит визуальную карточку с каждой стратегией, может выбрать одну, и выбор сохраняется для создания инвестиции.
Use Case 2: Strategy Comparison Dashboard¶
Сценарий: Пользователь сравнивает характеристики нескольких стратегий перед инвестицией
React Hook:
import { useState, useEffect } from 'react';
interface Strategy {
id: string;
name: string;
expectedYield: string;
riskLevel: string;
minInvestment: string;
maxInvestment: string;
}
export const useStrategyComparison = (strategyIds: string[]) => {
const [strategies, setStrategies] = useState<Strategy[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchStrategies = async () => {
try {
const token = localStorage.getItem('saga_access_token');
const promises = strategyIds.map(id =>
fetch(`/api/user/strategies/${id}`, {
headers: { 'Authorization': `Bearer ${token}` }
}).then(res => res.json())
);
const results = await Promise.all(promises);
setStrategies(results.map(r => r.data));
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
if (strategyIds.length > 0) {
fetchStrategies();
}
}, [strategyIds.join(',')]);
return { strategies, loading, error };
};
// Компонент сравнения
const StrategyComparisonTable: React.FC = () => {
const strategyIds = ['conservative-5', 'balanced-10', 'aggressive-20'];
const { strategies, loading, error } = useStrategyComparison(strategyIds);
if (loading) return <div>Загрузка...</div>;
if (error) return <div>Ошибка: {error}</div>;
return (
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="p-3 text-left">Стратегия</th>
<th className="p-3 text-left">APY</th>
<th className="p-3 text-left">Риск</th>
<th className="p-3 text-left">Мин. инвестиция</th>
<th className="p-3 text-left">Макс. инвестиция</th>
</tr>
</thead>
<tbody>
{strategies.map(strategy => (
<tr key={strategy.id} className="border-b hover:bg-gray-50">
<td className="p-3 font-medium">{strategy.name}</td>
<td className="p-3 text-blue-600 font-bold">{strategy.expectedYield}%</td>
<td className="p-3">{strategy.riskLevel}</td>
<td className="p-3">${strategy.minInvestment}</td>
<td className="p-3">${strategy.maxInvestment}</td>
</tr>
))}
</tbody>
</table>
);
};
Результат: Таблица сравнения показывает ключевые параметры всех стратегий рядом для легкого сравнения.
Use Case 3: Admin Strategy Management Dashboard¶
Сценарий: Администратор просматривает и управляет стратегиями
React Component:
import React, { useEffect, useState } from 'react';
interface AdminStrategy {
id: string;
name: string;
apy: string;
riskLevel: string;
minAmount: string;
maxAmount: string;
isActive: boolean;
}
const AdminStrategyDashboard: React.FC = () => {
const [strategies, setStrategies] = useState<AdminStrategy[]>([]);
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState<string | null>(null);
const fetchStrategies = async () => {
const adminToken = localStorage.getItem('admin_auth_token');
const response = await fetch('/api/admin/strategies', {
headers: { 'Authorization': `Bearer ${adminToken}` }
});
const { data } = await response.json();
setStrategies(data);
setLoading(false);
};
const toggleStrategyStatus = async (strategyId: string, currentStatus: boolean) => {
setUpdating(strategyId);
try {
const adminToken = localStorage.getItem('admin_auth_token');
const response = await fetch(`/api/admin/strategies/${strategyId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
isActive: !currentStatus,
notes: `${currentStatus ? 'Отключено' : 'Включено'} администратором`
})
});
const { data } = await response.json();
// Показать MVP warning
if (data.warning) {
alert(`⚠️ MVP Ограничение:\n\n${data.warning}`);
}
// Обновить список
await fetchStrategies();
} catch (error) {
console.error('Failed to update strategy:', error);
} finally {
setUpdating(null);
}
};
useEffect(() => {
fetchStrategies();
}, []);
if (loading) return <div>Загрузка...</div>;
return (
<div className="p-6">
<h2 className="text-2xl font-bold mb-6">Управление стратегиями</h2>
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6">
<p className="text-sm">
⚠️ <strong>MVP Режим:</strong> Стратегии управляются через конфигурационные файлы.
Для реальных изменений необходимо обновить <code>config/limits.yaml</code>
</p>
</div>
<table className="w-full border-collapse bg-white shadow rounded">
<thead className="bg-gray-100">
<tr>
<th className="p-3 text-left">Стратегия</th>
<th className="p-3 text-left">APY</th>
<th className="p-3 text-left">Риск</th>
<th className="p-3 text-left">Диапазон</th>
<th className="p-3 text-left">Статус</th>
<th className="p-3 text-left">Действия</th>
</tr>
</thead>
<tbody>
{strategies.map(strategy => (
<tr key={strategy.id} className="border-b hover:bg-gray-50">
<td className="p-3 font-medium">{strategy.name}</td>
<td className="p-3 text-blue-600 font-bold">{strategy.apy}%</td>
<td className="p-3">{strategy.riskLevel}</td>
<td className="p-3 text-sm text-gray-600">
${strategy.minAmount} - ${strategy.maxAmount}
</td>
<td className="p-3">
<span className={`
px-2 py-1 rounded text-xs font-semibold
${strategy.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}
`}>
{strategy.isActive ? 'Активна' : 'Отключена'}
</span>
</td>
<td className="p-3">
<button
onClick={() => toggleStrategyStatus(strategy.id, strategy.isActive)}
disabled={updating === strategy.id}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{updating === strategy.id ? 'Обновление...' :
strategy.isActive ? 'Отключить' : 'Включить'}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default AdminStrategyDashboard;
Результат: Админ видит все стратегии с возможностью быстрого переключения статуса, получает предупреждение о MVP ограничениях.
Use Case 4: Strategy Details Modal¶
Сценарий: Пользователь кликает на стратегию чтобы увидеть полную информацию
React Component:
import React, { useState, useEffect } from 'react';
import { Dialog } from '@headlessui/react';
interface StrategyDetails {
id: string;
name: string;
description: string;
riskLevel: string;
expectedYield: string;
minInvestment: string;
maxInvestment: string;
isActive: boolean;
}
const StrategyDetailsModal: React.FC<{
strategyId: string;
isOpen: boolean;
onClose: () => void;
}> = ({ strategyId, isOpen, onClose }) => {
const [strategy, setStrategy] = useState<StrategyDetails | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (isOpen && strategyId) {
const fetchStrategyDetails = async () => {
try {
const token = localStorage.getItem('saga_access_token');
const response = await fetch(`/api/user/strategies/${strategyId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const { data } = await response.json();
setStrategy(data);
} catch (error) {
console.error('Failed to fetch strategy details:', error);
} finally {
setLoading(false);
}
};
fetchStrategyDetails();
}
}, [strategyId, isOpen]);
return (
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="bg-white rounded-lg shadow-xl max-w-2xl w-full p-6">
{loading ? (
<div className="text-center py-8">Загрузка...</div>
) : strategy ? (
<>
<Dialog.Title className="text-2xl font-bold mb-4">
{strategy.name}
</Dialog.Title>
<div className="space-y-4">
<div className="bg-blue-50 p-4 rounded">
<div className="text-4xl font-bold text-blue-600">
{strategy.expectedYield}%
</div>
<div className="text-sm text-gray-600">Целевая годовая доходность</div>
</div>
<div>
<h3 className="font-semibold mb-2">Описание</h3>
<p className="text-gray-700">{strategy.description}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="font-semibold text-sm text-gray-600">Уровень риска</h4>
<p className="text-lg capitalize">{strategy.riskLevel}</p>
</div>
<div>
<h4 className="font-semibold text-sm text-gray-600">Минимальная инвестиция</h4>
<p className="text-lg">${strategy.minInvestment}</p>
</div>
<div>
<h4 className="font-semibold text-sm text-gray-600">Максимальная инвестиция</h4>
<p className="text-lg">${strategy.maxInvestment}</p>
</div>
<div>
<h4 className="font-semibold text-sm text-gray-600">Статус</h4>
<p className="text-lg">
{strategy.isActive ?
<span className="text-green-600">Активна</span> :
<span className="text-red-600">Отключена</span>
}
</p>
</div>
</div>
</div>
<div className="mt-6 flex justify-end space-x-3">
<button
onClick={onClose}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
Закрыть
</button>
<button
onClick={() => {
// Navigate to investment creation with this strategy
window.location.href = `/invest?strategy=${strategy.id}`;
}}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Инвестировать
</button>
</div>
</>
) : (
<div className="text-center py-8 text-red-600">
Ошибка загрузки стратегии
</div>
)}
</Dialog.Panel>
</div>
</Dialog>
);
};
export default StrategyDetailsModal;
Результат: Модальное окно с полной информацией о стратегии и кнопкой для инвестирования.
Use Case 5: Strategy Filter and Sort¶
Сценарий: Пользователь фильтрует стратегии по уровню риска и сортирует по APY
React Component:
import React, { useEffect, useState, useMemo } from 'react';
interface Strategy {
id: string;
name: string;
expectedYield: string;
riskLevel: 'low' | 'medium' | 'high';
minInvestment: string;
}
const StrategyFilterList: React.FC = () => {
const [strategies, setStrategies] = useState<Strategy[]>([]);
const [riskFilter, setRiskFilter] = useState<string>('all');
const [sortBy, setSortBy] = useState<'apy' | 'risk'>('apy');
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchStrategies = async () => {
const token = localStorage.getItem('saga_access_token');
const response = await fetch('/api/user/strategies', {
headers: { 'Authorization': `Bearer ${token}` }
});
const { data } = await response.json();
setStrategies(data.strategies);
setLoading(false);
};
fetchStrategies();
}, []);
const filteredAndSortedStrategies = useMemo(() => {
let result = strategies;
// Фильтр по риску
if (riskFilter !== 'all') {
result = result.filter(s => s.riskLevel === riskFilter);
}
// Сортировка
result = [...result].sort((a, b) => {
if (sortBy === 'apy') {
return parseFloat(b.expectedYield) - parseFloat(a.expectedYield);
} else {
const riskOrder = { low: 1, medium: 2, high: 3 };
return riskOrder[a.riskLevel] - riskOrder[b.riskLevel];
}
});
return result;
}, [strategies, riskFilter, sortBy]);
if (loading) return <div>Загрузка...</div>;
return (
<div className="space-y-4">
<div className="flex gap-4 items-center">
<div>
<label className="text-sm font-medium mr-2">Уровень риска:</label>
<select
value={riskFilter}
onChange={(e) => setRiskFilter(e.target.value)}
className="border rounded px-3 py-1"
>
<option value="all">Все</option>
<option value="low">Низкий</option>
<option value="medium">Средний</option>
<option value="high">Высокий</option>
</select>
</div>
<div>
<label className="text-sm font-medium mr-2">Сортировать по:</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'apy' | 'risk')}
className="border rounded px-3 py-1"
>
<option value="apy">APY (по убыванию)</option>
<option value="risk">Риск (по возрастанию)</option>
</select>
</div>
<div className="text-sm text-gray-600">
Найдено: {filteredAndSortedStrategies.length} стратегий
</div>
</div>
<div className="grid gap-4">
{filteredAndSortedStrategies.map(strategy => (
<div key={strategy.id} className="border rounded-lg p-4 hover:shadow-md transition">
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold text-lg">{strategy.name}</h3>
<p className="text-sm text-gray-600">
Риск: {strategy.riskLevel} | Мин: ${strategy.minInvestment}
</p>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-blue-600">
{strategy.expectedYield}%
</div>
<div className="text-xs text-gray-500">APY</div>
</div>
</div>
</div>
))}
</div>
</div>
);
};
export default StrategyFilterList;
Результат: Интерактивный список с фильтрацией по риску и сортировкой по APY/риску.
Техническая информация¶
Архитектура Strategy System¶
Config-Based Architecture:
- Стратегии определены в
config/limits.yaml - Загружаются через UnifiedConfig систему
- Иммутабельны в runtime (изменения требуют rebuild + restart)
Strategy Service:
GetStrategiesForSelection()- возвращает активные стратегии для пользователяGetStrategyByID()- получает детали конкретной стратегии- Кеширование стратегий в memory для производительности
MVP Ограничения:
- Admin PUT endpoint не модифицирует реальные стратегии
- Изменения требуют обновления config файлов вручную
- Необходимо выполнить
make generate-config && make restartдля применения
Type Safety¶
Backend (Go):
type Strategy struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
RiskLevel string `json:"riskLevel"`
ExpectedYield SafeDecimal `json:"expectedYield"`
MinInvestment SafeDecimal `json:"minInvestment"`
MaxInvestment SafeDecimal `json:"maxInvestment"`
IsActive bool `json:"isActive"`
}
Frontend (TypeScript):
// Auto-generated через make generate-types
interface Strategy {
id: string;
name: string;
description: string;
riskLevel: 'low' | 'medium' | 'high';
expectedYield: string; // SafeDecimal string representation
minInvestment: string;
maxInvestment: string;
isActive: boolean;
}
Security¶
Authentication:
- User endpoints: требуют User JWT токен
- Admin endpoints: требуют Admin JWT токен с role validation
Authorization:
- Admin endpoints проверяют admin role через middleware
- Неавторизованные запросы возвращают 401/403
Data Validation:
- Strategy ID validation на backend
- SafeDecimal для всех финансовых полей
- Проверка существования стратегии перед возвратом
Performance Considerations¶
Caching:
- Стратегии кешируются в memory после загрузки из config
- Кеш инвалидируется только при restart сервера
Network Optimization:
- GET /api/user/strategies возвращает все стратегии сразу (всего 3-5 стратегий)
- Minimize round trips: fetch всех стратегий вместо отдельных requests
Response Size:
- User endpoints: ~1-2KB для списка стратегий
- Admin endpoints: аналогично user endpoints
Связанная документация¶
Другие User Endpoints:
- Инвестиции - Создание инвестиций используя стратегии
- Dashboard - Обзор портфеля по стратегиям
Admin Endpoints:
- Admin Investments - Управление инвестициями по стратегиям
- Admin Dashboard - Административная аналитика стратегий
Архитектура:
- UnifiedConfig System - Config-based strategy management
- SafeDecimal Pattern - Финансовая точность
Руководства:
- Strategy Selection Guide - Помощь пользователям в выборе стратегии
📋 Метаданные¶
Версия: 2.6.268
Обновлено: 2025-10-21
Статус: Published