Перейти к содержанию

Инвестиционные стратегии (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

Получить список всех доступных инвестиционных стратегий для пользователя.

Запрос

GET /api/user/strategies
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Заголовки:

Заголовок Значение Обязательно Описание
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

Получить детальную информацию о конкретной стратегии.

Запрос

GET /api/user/strategies/balanced-10
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

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

Запрос

GET /api/admin/strategies
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Заголовки:

Заголовок Значение Обязательно Описание
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 - Административная аналитика стратегий

Архитектура:

Руководства:




📋 Метаданные

Версия: 2.6.268

Обновлено: 2025-10-21

Статус: Published