Strategy Architecture¶
Обзор¶
Technical architecture документация для investment strategy system в Saga DeFi Platform - от конфигурации до blockchain integration.
Architecture Layers¶
┌─────────────────────────────────────────────────────────────┐
│ Strategy Architecture │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Configuration Layer (UnifiedConfig) │ │
│ │ config/limits.yaml → strategies[] │ │
│ └────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Backend Service Layer │ │
│ │ InvestmentService, StrategyValidator │ │
│ └────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Blockchain Layer │ │
│ │ StakingProtocol5/10/20 Contracts │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Strategy Configuration Schema¶
UnifiedConfig Structure¶
File: config/limits.yaml
limits:
investment_strategies:
- id: 1
name: "Conservative"
apy: 5.0
risk_level: "low"
contract_address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6"
implementation_address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853"
st_token_address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"
min_amount: "10.00"
max_amount: "10000.00"
enabled: true
recommended_for:
- "beginners"
- "low_risk_tolerance"
compound_frequency: "daily" # Interest compounds daily
- id: 2
name: "Balanced"
apy: 10.0
risk_level: "medium"
contract_address: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788"
implementation_address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"
st_token_address: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9"
min_amount: "10.00"
max_amount: "10000.00"
enabled: true
recommended_for:
- "experienced"
- "medium_risk_tolerance"
compound_frequency: "daily"
- id: 3
name: "Aggressive"
apy: 20.0
risk_level: "high"
contract_address: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0"
implementation_address: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e"
st_token_address: "0x0165878A594ca255338adfa4d48449f69242Eb8F"
min_amount: "10.00"
max_amount: "10000.00"
enabled: true
recommended_for:
- "expert_traders"
- "high_risk_tolerance"
compound_frequency: "daily"
Go Model Structure¶
// backend/config/unified_config.go
type InvestmentStrategy struct {
ID int `yaml:"id" json:"id"`
Name string `yaml:"name" json:"name"`
APY float64 `yaml:"apy" json:"apy"`
RiskLevel string `yaml:"risk_level" json:"risk_level"`
ContractAddress string `yaml:"contract_address" json:"contract_address"`
ImplementationAddress string `yaml:"implementation_address" json:"implementation_address"`
StTokenAddress string `yaml:"st_token_address" json:"st_token_address"`
MinAmount string `yaml:"min_amount" json:"min_amount"`
MaxAmount string `yaml:"max_amount" json:"max_amount"`
Enabled bool `yaml:"enabled" json:"enabled"`
RecommendedFor []string `yaml:"recommended_for" json:"recommended_for"`
CompoundFrequency string `yaml:"compound_frequency" json:"compound_frequency"`
}
// UnifiedConfig holds all strategies
type UnifiedConfig struct {
InvestmentStrategies []InvestmentStrategy `yaml:"investment_strategies"`
}
// Accessor methods
func (cfg *UnifiedConfig) GetInvestmentStrategy(id int) *InvestmentStrategy {
for _, strategy := range cfg.InvestmentStrategies {
if strategy.ID == id {
return &strategy
}
}
return nil
}
func (cfg *UnifiedConfig) GetAllStrategies() []InvestmentStrategy {
strategies := []InvestmentStrategy{}
for _, strategy := range cfg.InvestmentStrategies {
if strategy.Enabled {
strategies = append(strategies, strategy)
}
}
return strategies
}
Backend Services Architecture¶
Strategy Validator Service¶
// backend/shared/services/strategy_validator.go
type StrategyValidator struct {
config *config.UnifiedConfig
logger *logging.CanonicalLogger
}
func NewStrategyValidator(cfg *config.UnifiedConfig) *StrategyValidator {
return &StrategyValidator{
config: cfg,
logger: logging.GetGlobalCanonicalLogger(),
}
}
// Validate strategy selection
func (sv *StrategyValidator) ValidateStrategy(strategyID int, amount SafeDecimal) error {
// 1. Load strategy from config
strategy := sv.config.GetInvestmentStrategy(strategyID)
if strategy == nil {
return errors.New("invalid strategy ID")
}
// 2. Check if strategy is enabled
if !strategy.Enabled {
return errors.New("strategy is currently disabled")
}
// 3. Validate amount limits
minAmount := SafeDecimalFromString(strategy.MinAmount)
maxAmount := SafeDecimalFromString(strategy.MaxAmount)
if amount.LessThan(minAmount) {
return fmt.Errorf("amount below minimum: $%s (min: $%s)",
amount.String(),
minAmount.String(),
)
}
if amount.GreaterThan(maxAmount) {
return fmt.Errorf("amount exceeds maximum: $%s (max: $%s)",
amount.String(),
maxAmount.String(),
)
}
// 4. Validate blockchain contract
if !sv.isValidContractAddress(strategy.ContractAddress) {
return errors.New("invalid strategy contract address")
}
return nil
}
func (sv *StrategyValidator) isValidContractAddress(address string) bool {
// Check Ethereum address format
return common.IsHexAddress(address) && len(address) == 42
}
// Get strategy recommendation based on user profile
func (sv *StrategyValidator) RecommendStrategy(userProfile UserProfile) (*InvestmentStrategy, error) {
strategies := sv.config.GetAllStrategies()
for _, strategy := range strategies {
if sv.matchesProfile(strategy, userProfile) {
return &strategy, nil
}
}
// Default: Conservative strategy
return sv.config.GetInvestmentStrategy(1), nil
}
func (sv *StrategyValidator) matchesProfile(strategy InvestmentStrategy, profile UserProfile) bool {
// Match based on recommended_for tags
for _, recommendation := range strategy.RecommendedFor {
if contains(profile.Tags, recommendation) {
return true
}
}
return false
}
Investment Service Integration¶
// backend/shared/services/investment_service.go
type InvestmentService struct {
config *config.UnifiedConfig
validator *StrategyValidator
repo *InvestmentRepository
blockchain *BlockchainStakingService
}
func (s *InvestmentService) CreateInvestment(ctx context.Context, userID int64, req InvestmentRequest) (*Investment, error) {
// 1. Validate strategy and amount
err := s.validator.ValidateStrategy(req.StrategyID, req.Amount)
if err != nil {
return nil, err
}
// 2. Load strategy configuration
strategy := s.config.GetInvestmentStrategy(req.StrategyID)
// 3. Create investment record
investment := &Investment{
UserID: userID,
StrategyID: strategy.ID,
Amount: req.Amount,
APY: strategy.APY,
Status: "pending",
CreatedAt: time.Now().UTC(),
}
// 4. Save to database
err = s.repo.CreateInvestment(ctx, investment)
if err != nil {
return nil, err
}
// 5. Initiate blockchain staking
go s.blockchain.StakeTokens(investment, strategy)
s.logger.InfoStructured("Investment created",
"investment_id", investment.ID,
"strategy_id", strategy.ID,
"strategy_name", strategy.Name,
"apy", strategy.APY,
"amount", req.Amount.String(),
)
return investment, nil
}
Blockchain Integration Architecture¶
Smart Contract Mapping¶
// backend/shared/services/blockchain_staking_service.go
type BlockchainStakingService struct {
config *config.UnifiedConfig
logger *logging.CanonicalLogger
}
func (s *BlockchainStakingService) StakeTokens(investment *Investment, strategy *InvestmentStrategy) {
// 1. Connect to blockchain
client, err := ethclient.Dial(s.config.GetBlockchainRPCURL())
if err != nil {
s.markInvestmentFailed(investment, err)
return
}
// 2. Load staking protocol contract (UUPS Proxy)
stakingAddress := common.HexToAddress(strategy.ContractAddress)
staking, err := contracts.NewStakingProtocol(stakingAddress, client)
if err != nil {
s.markInvestmentFailed(investment, err)
return
}
// 3. Approve USDC для staking
auth, err := s.getTransactionAuth(client, investment.UserID)
usdcAddress := common.HexToAddress(s.config.GetUSDCContractAddress())
usdc, err := contracts.NewTestUSDC(usdcAddress, client)
amount := investment.Amount.ToBigInt()
approveTx, err := usdc.Approve(auth, stakingAddress, amount)
if err != nil {
s.markInvestmentFailed(investment, err)
return
}
// Wait for approval confirmation
approveReceipt, err := bind.WaitMined(context.Background(), client, approveTx)
if err != nil || approveReceipt.Status != types.ReceiptStatusSuccessful {
s.markInvestmentFailed(investment, errors.New("approval failed"))
return
}
// 4. Stake USDC → receive stTokens
stakeTx, err := staking.Stake(auth, amount)
if err != nil {
s.markInvestmentFailed(investment, err)
return
}
// Wait for stake confirmation
stakeReceipt, err := bind.WaitMined(context.Background(), client, stakeTx)
if err != nil || stakeReceipt.Status != types.ReceiptStatusSuccessful {
s.markInvestmentFailed(investment, errors.New("staking failed"))
return
}
// 5. Update investment status
investment.Status = "active"
investment.StakeTxHash = stakeTx.Hash().Hex()
investment.StakedAt = time.Now().UTC()
investment.StTokenAddress = strategy.StTokenAddress
s.repo.UpdateInvestment(context.Background(), investment)
s.logger.InfoStructured("Investment staked successfully",
"investment_id", investment.ID,
"strategy_id", strategy.ID,
"stake_tx_hash", stakeTx.Hash().Hex(),
"st_token_address", strategy.StTokenAddress,
)
}
Interest Calculation Architecture¶
Smart Contract Side (Continuous Compounding):
// blockchain/contracts/StakingProtocol.sol
contract StakingProtocol is Initializable, OwnableUpgradeable, UUPSUpgradeable {
uint256 public apy; // e.g., 5 for 5%, 10 for 10%, 20 for 20%
// Simulate one day of interest accrual
function simulateDay() external {
uint256 totalStaked = usdcToken.balanceOf(address(this));
// Daily interest = (totalStaked * apy) / 365 / 100
uint256 interest = (totalStaked * apy) / 365 / 100;
// Mint new USDC to cover interest
usdcToken.mint(address(this), interest);
// Exchange rate updates automatically through stToken rebase
// Users' stToken balances remain same, but value increases
}
}
Backend Calculation (Query Current Value):
// backend/shared/services/investment_value_service.go
func (s *InvestmentValueService) GetCurrentValue(ctx context.Context, investment *Investment) (SafeDecimal, error) {
// Load strategy to get stToken address
strategy := s.config.GetInvestmentStrategy(investment.StrategyID)
// Connect to blockchain
client, err := ethclient.Dial(s.config.GetBlockchainRPCURL())
// Load stToken contract
stTokenAddress := common.HexToAddress(strategy.StTokenAddress)
stToken, err := contracts.NewStToken(stTokenAddress, client)
// Get user's stToken balance
userAddress := s.getUserWalletAddress(ctx, investment.UserID)
stBalance, err := stToken.BalanceOf(&bind.CallOpts{}, common.HexToAddress(userAddress))
// Get exchange rate (stToken → USDC)
exchangeRate, err := stToken.ExchangeRate(&bind.CallOpts{})
// Calculate current value
// currentValue = stBalance * exchangeRate / 1e18
currentValue := new(big.Int).Mul(stBalance, exchangeRate)
currentValue.Div(currentValue, big.NewInt(1e18))
return SafeDecimalFromBigInt(currentValue, 6), nil
}
API Layer Architecture¶
Strategy Endpoints¶
// backend/shared/routing/strategy_handler.go
type StrategyHandler struct {
config *config.UnifiedConfig
}
// GET /api/strategies
func (h *StrategyHandler) GetAllStrategies(w http.ResponseWriter, r *http.Request) {
strategies := h.config.GetAllStrategies()
// Transform для frontend (hide internal fields)
publicStrategies := make([]StrategyResponse, len(strategies))
for i, strategy := range strategies {
publicStrategies[i] = StrategyResponse{
ID: strategy.ID,
Name: strategy.Name,
APY: strategy.APY,
RiskLevel: strategy.RiskLevel,
MinAmount: strategy.MinAmount,
MaxAmount: strategy.MaxAmount,
}
}
http_helpers.SendSuccess(w, publicStrategies)
}
// GET /api/strategies/{id}
func (h *StrategyHandler) GetStrategy(w http.ResponseWriter, r *http.Request) {
strategyID, _ := strconv.Atoi(chi.URLParam(r, "id"))
strategy := h.config.GetInvestmentStrategy(strategyID)
if strategy == nil {
http_helpers.SendError(w, "Strategy not found", http.StatusNotFound)
return
}
response := StrategyResponse{
ID: strategy.ID,
Name: strategy.Name,
APY: strategy.APY,
RiskLevel: strategy.RiskLevel,
MinAmount: strategy.MinAmount,
MaxAmount: strategy.MaxAmount,
CompoundFrequency: strategy.CompoundFrequency,
}
http_helpers.SendSuccess(w, response)
}
type StrategyResponse struct {
ID int `json:"id"`
Name string `json:"name"`
APY float64 `json:"apy"`
RiskLevel string `json:"risk_level"`
MinAmount string `json:"min_amount"`
MaxAmount string `json:"max_amount"`
CompoundFrequency string `json:"compound_frequency"`
}
Testing Architecture¶
Unit Tests¶
// backend/tests/unit/strategy_validator_test.go
func TestStrategyValidator_ValidateStrategy(t *testing.T) {
cfg := loadTestConfig()
validator := NewStrategyValidator(cfg)
tests := []struct {
name string
strategyID int
amount SafeDecimal
wantError bool
}{
{
name: "Valid Conservative strategy",
strategyID: 1,
amount: SafeDecimalFromString("100.00"),
wantError: false,
},
{
name: "Amount below minimum",
strategyID: 1,
amount: SafeDecimalFromString("5.00"),
wantError: true,
},
{
name: "Amount above maximum",
strategyID: 1,
amount: SafeDecimalFromString("50000.00"),
wantError: true,
},
{
name: "Invalid strategy ID",
strategyID: 999,
amount: SafeDecimalFromString("100.00"),
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateStrategy(tt.strategyID, tt.amount)
if (err != nil) != tt.wantError {
t.Errorf("ValidateStrategy() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
Integration Tests¶
// backend/tests/integration/strategy_lifecycle_test.go
func TestStrategyLifecycle(t *testing.T) {
server := setupTestServer()
defer server.Close()
// 1. Get all strategies
resp := makeRequest(server, "GET", "/api/strategies", nil)
var strategies []StrategyResponse
json.Unmarshal(resp.Body, &strategies)
assert.Equal(t, 3, len(strategies))
assert.Equal(t, "Conservative", strategies[0].Name)
assert.Equal(t, 5.0, strategies[0].APY)
// 2. Create investment with strategy
investmentReq := InvestmentRequest{
StrategyID: 2, // Balanced
Amount: SafeDecimalFromString("500.00"),
}
investResp := makeAuthRequest(server, "POST", "/api/investments", investmentReq, userToken)
var investment Investment
json.Unmarshal(investResp.Body, &investment)
assert.Equal(t, int64(2), investment.StrategyID)
assert.Equal(t, 10.0, investment.APY)
// 3. Wait for blockchain staking
time.Sleep(15 * time.Second)
// 4. Verify investment active
getResp := makeAuthRequest(server, "GET",
fmt.Sprintf("/api/investments/%d", investment.ID),
nil, userToken)
var activeInvestment Investment
json.Unmarshal(getResp.Body, &activeInvestment)
assert.Equal(t, "active", activeInvestment.Status)
}
Related Documentation¶
📋 Метаданные¶
Версия: 2.4.82
Обновлено: 2025-10-21
Статус: Published