Create a sophisticated risk management module that includes position sizing, capital limits, and emergency stop functionalities.

Create a sophisticated risk management module that includes position sizing, capital limits, and emergency stop functionalities.

Implementing a sophisticated risk management module is paramount for any automated trading system, especially for high-frequency or MEV (Maximal Extractable Value) bots operating on a dynamic blockchain like Solana. This module acts as the core guardian of your capital, designed to preserve funds against market volatility, unexpected events, and even potential bugs in your trading logic. It ensures that while your bot seeks to maximize profits, it does so within acceptable boundaries of risk.

Why Create a Sophisticated Risk Management Module?

The inherent volatility and rapid execution speed of decentralized finance (DeFi) on Solana amplify the need for robust risk management. Without it, even a highly profitable strategy can lead to catastrophic losses due to a single miscalculation or unforeseen market event.

Capital Preservation: The primary goal of any risk management strategy is to protect your principal capital. Trading is probabilistic; losses are inevitable. A robust module ensures that no single loss, or series of losses, wipes out your entire investment. This aligns with the fundamental principle of "Rule No. 1: Never lose money. Rule No. 2: Never forget Rule No. 1," as famously stated by Warren Buffett.

Mitigating Market Volatility and Black Swan Events: Cryptocurrencies, including Solana assets, are highly volatile. Sudden price crashes, liquidity crunches, or flash loan attacks can occur rapidly. An effective risk management system provides safeguards against these extreme, unpredictable events. For instance, on May 19, 2021, the crypto market experienced a significant flash crash, with Bitcoin dropping over 30% in a single day, highlighting the need for robust risk controls.

Preventing Overexposure: Without proper position sizing and capital limits, it's easy to commit too much capital to a single trade or asset, leaving you vulnerable to disproportionate losses. Risk management enforces discipline in capital allocation.

Ensuring Long-Term Profitability: Consistent, long-term profitability in trading often stems less from finding perfect opportunities and more from meticulously managing risk. A disciplined approach to risk allows your profitable strategies to compound over time, even with occasional losses.

Handling Technical Failures and Bugs: Automated systems are susceptible to bugs or unexpected behavior. An emergency stop functionality can prevent a minor code error from escalating into a major financial disaster.

Core Components of the Risk Management Module

A sophisticated risk management module typically comprises three interconnected pillars: Position Sizing, Capital Limits, and Emergency Stop Functionalities.

1. Position Sizing

Concept: Position sizing dictates how much capital or how many units of an asset your bot commits to a single trade or opportunity. It's not about what to trade or when to trade, but how much.

Why it's Crucial: Proper position sizing is arguably the most critical component of risk management. It directly controls your potential loss per trade, preventing any single losing trade from having a disproportionately negative impact on your overall capital. According to Van K. Tharp, a renowned trading coach, "Position sizing is the most important factor in determining how you perform."

Algorithms/Strategies:

Fixed Dollar Amount: Commit a static amount (e.g., $1000) to each trade. Simple but doesn't adapt to account growth/loss or asset volatility.

Fixed Percentage of Capital: Commit a percentage of your total available capital to each trade (e.g., 1-2% per trade). This dynamically scales with your account balance. As your capital grows, your position size increases, and vice versa. This is a widely recommended approach by financial experts.

Volatility-Based Sizing: Adjusts position size based on the volatility of the asset. Less volatile assets might allow for larger positions, while highly volatile ones require smaller positions to keep the dollar risk constant. Measures like Average True Range (ATR) can be used to gauge volatility.

Kelly Criterion (Advanced): An aggressive formula to determine optimal bet size to maximize expected return, given known probabilities of winning and losing. While theoretically optimal, it's often too aggressive for practical trading and can lead to rapid capital drawdowns in real-world scenarios due to unknown true probabilities.

How to Implement in Rust (Actionable Advice):

You'll need a way to store your current capital, configure your chosen sizing strategy, and calculate the amount_in for your DEX swaps or flash loan borrows.

Define Configuration: Store your position sizing rules in a configuration structure.

// src/config.rs

#[derive(Debug, Clone, serde::Deserialize)]

pub enum PositionSizingStrategy {

    FixedAmount(u64), // Fixed lamports

    PercentageOfCapital {

        percentage: f64, // e.g., 0.01 for 1%

        max_sol_per_trade: Option<u64>, // Optional cap

    },

    // Add other strategies as needed

}

#[derive(Debug, Clone, serde::Deserialize)]

pub struct RiskManagementConfig {

    pub position_sizing: PositionSizingStrategy,

    // ... other risk config ...

}

PositionSizer Module: Create a dedicated module (src/risk_management/position_sizer.rs) to encapsulate sizing logic. This module will need access to your bot's current total capital.

// src/risk_management/position_sizer.rs

use solana_sdk::pubkey::Pubkey;

use anyhow::Result;

use std::sync::{Arc, Mutex};

use crate::config::{PositionSizingStrategy, RiskManagementConfig};

pub struct PositionSizer {

    config: RiskManagementConfig,

    // This Mutex would typically hold a reference to your CapitalManager's state

    // or a simpler structure for current capital.

    // For simplicity here, let's assume we pass current_capital_lamports.

}

impl PositionSizer {

    pub fn new(config: RiskManagementConfig) -> Self {

        Self { config }

    }

    /// Calculates the maximum amount of SOL (in lamports) to commit to a new trade.

    /// `current_total_capital_lamports` is the total available capital in your bot's control.

    pub fn calculate_trade_amount_sol(&self, current_total_capital_lamports: u64) -> Result<u64> {

        match &self.config.position_sizing {

            PositionSizingStrategy::FixedAmount(amount) => {

                if *amount > current_total_capital_lamports {

                    return Err(anyhow::anyhow!("Fixed trade amount exceeds total capital."));

                }

                Ok(*amount)

            }

            PositionSizingStrategy::PercentageOfCapital { percentage, max_sol_per_trade } => {

                let mut calculated_amount = (current_total_capital_lamports as f64 * percentage) as u64;

                if let Some(max_cap) = max_sol_per_trade {

                    calculated_amount = calculated_amount.min(*max_cap);

                }

                if calculated_amount == 0 {

                    return Err(anyhow::anyhow!("Calculated trade amount is zero. Increase capital or percentage."));

                }

                if calculated_amount > current_total_capital_lamports {

                    return Err(anyhow::anyhow!("Calculated trade amount exceeds total capital. This should not happen if percentage is correctly applied."));

                }

                Ok(calculated_amount)

            }

        }

    }

    // You could extend this to calculate amounts for specific token mints

    // by factoring in their current price against SOL/USDC.

    pub async fn calculate_trade_amount_token(

        &self,

        _mint_address: &Pubkey,

        _current_total_capital_lamports: u64,

        // You'd need a price feed here to convert SOL/USDC to token amount

        // price_feed: &impl PriceFeedTrait,

    ) -> Result<u64> {

        // Placeholder: This would involve converting the calculated SOL/USDC amount

        // from `calculate_trade_amount_sol` into the target token amount based on

        // current market prices.

        unimplemented!("Token-specific position sizing requires a price feed.")

    }

}

Integration: Before constructing an arbitrage or MEV transaction (Step 6/7), your execution module would query the PositionSizer.

// In your arbitrage/MEV execution logic (conceptual)

// use crate::risk_management::position_sizer::PositionSizer;

// use std::sync::{Arc, Mutex};

// Assuming you have a `position_sizer: Arc<PositionSizer>`

// And `capital_manager: Arc<Mutex<CapitalManager>>`

// let current_capital = capital_manager.lock().unwrap().get_available_capital_sol_lamports();

// let trade_size_sol = position_sizer.calculate_trade_amount_sol(current_capital)?;

// // Now use `trade_size_sol` when constructing your DEX swap instructions

// // or when determining flash loan borrow amounts.

2. Capital Limits

Concept: Capital limits impose overarching boundaries on your bot's total capital deployment, preventing catastrophic losses even if individual trades fail or multiple issues occur simultaneously.

Why it's Crucial: While position sizing limits risk per trade, capital limits safeguard your entire portfolio. They are your "stop-loss" for the entire bot operation. A study by Greenwich Associates found that institutional investors prioritize risk limits and controls above all else when evaluating trading algorithms.

Types of Limits:

Total Allocated Capital: The absolute maximum capital (e.g., in SOL or USDC) that your bot is allowed to control or deploy across all strategies and open positions.

Maximum Exposure Per Asset: Limits the percentage or fixed amount of capital that can be held in any single asset. This prevents over-concentration risk.

Maximum Drawdown:

Overall Drawdown: The maximum percentage (e.g., 20-30%) your total capital can drop from its historical peak. If this is hit, all trading should halt.

Daily/Weekly Drawdown: Limits the capital loss over a shorter period.

Open Position Count Limit: The maximum number of concurrent open positions or arbitrage attempts allowed. This limits overall market exposure.

How to Implement in Rust (Actionable Advice):

You'll need a central CapitalManager that tracks your bot's financial state and enforces these limits. This manager will need to be accessible and mutable by various parts of your bot, requiring concurrent access patterns like Arc<Mutex<>> or Arc<RwLock<>>.

Define Configuration:

// src/config.rs (continued)

#[derive(Debug, Clone, serde::Deserialize)]

pub struct CapitalLimitsConfig {

    pub total_allocated_capital_sol_lamports: u64, // Max capital bot can use

    pub max_drawdown_percentage: f64, // e.g., 0.20 for 20%

    pub max_exposure_per_asset_percentage: f64, // e.g., 0.50 for 50%

    pub max_open_positions: usize,

    // ... other limits

}

#[derive(Debug, Clone, serde::Deserialize)]

pub struct RiskManagementConfig {

    pub position_sizing: PositionSizingStrategy,

    pub capital_limits: CapitalLimitsConfig,

    // ... emergency stop config ...

}

CapitalManager Module: Create a struct to manage and track capital. It should be thread-safe for concurrent access.

// src/risk_management/capital_manager.rs

use anyhow::Result;

use std::collections::HashMap;

use std::sync::{Arc, Mutex};

use solana_sdk::pubkey::Pubkey;

use crate::config::CapitalLimitsConfig;

pub struct CapitalManager {

    config: CapitalLimitsConfig,

    current_total_capital_sol_lamports: u64,

    initial_capital_sol_lamports: u64,

    peak_capital_sol_lamports: u64,

    // Track exposure per asset (Pubkey -> lamports equivalent value)

    asset_exposure: HashMap<Pubkey, u64>,

    open_positions_count: usize,

    // This would typically hold information about current open positions for tracking.

    // For simplicity, we just use a count here.

}

impl CapitalManager {

    pub fn new(config: CapitalLimitsConfig, initial_capital_sol_lamports: u64) -> Self {

        Self {

            config,

            current_total_capital_sol_lamports,

            initial_capital_sol_lamports,

            peak_capital_sol_lamports: initial_capital_sol_lamports,

            asset_exposure: HashMap::new(),

            open_positions_count: 0,

        }

    }

    pub fn get_available_capital_sol_lamports(&self) -> u64 {

        self.current_total_capital_sol_lamports

    }

    /// Updates total capital after a profitable or losing trade.

    pub fn update_total_capital(&mut self, profit_loss_sol_lamports: i64) {

        if profit_loss_sol_lamports > 0 {

            self.current_total_capital_sol_lamports = self.current_total_capital_sol_lamports.saturating_add(profit_loss_sol_lamports as u64);

        } else {

            self.current_total_capital_sol_lamports = self.current_total_capital_sol_lamports.saturating_sub(profit_loss_sol_lamports.abs() as u64);

        }

        self.peak_capital_sol_lamports = self.peak_capital_sol_lamports.max(self.current_total_capital_sol_lamports);

        println!("Updated total capital: {} SOL", self.current_total_capital_sol_lamports as f64 / 1_000_000_000.0);

    }

    /// Checks if adding a new position would exceed capital limits.

    pub fn can_open_position(&self, trade_amount_sol_lamports: u64, target_asset_mint: &Pubkey) -> Result<()> {

        // Check total allocated capital

        if self.current_total_capital_sol_lamports < trade_amount_sol_lamports {

            return Err(anyhow::anyhow!("Insufficient total capital to open position."));

        }

        // Check max open positions

        if self.open_positions_count >= self.config.max_open_positions {

            return Err(anyhow::anyhow!("Maximum number of open positions reached."));

        }

        // Check max exposure per asset (conceptual - needs price conversion for different assets)

        // For simplicity, let's assume target_asset_mint value is in lamports for now.

        let current_asset_exposure = *self.asset_exposure.get(target_asset_mint).unwrap_or(&0);

        let new_asset_exposure = current_asset_exposure.saturating_add(trade_amount_sol_lamports); // Assuming SOL value

        let max_asset_exposure_sol = (self.current_total_capital_sol_lamports as f64 * self.config.max_exposure_per_asset_percentage) as u64;

        if new_asset_exposure > max_asset_exposure_sol {

            return Err(anyhow::anyhow!("Opening this position would exceed max exposure for asset {:?}.", target_asset_mint));

        }

        Ok(())

    }

    pub fn increment_open_positions(&mut self, trade_amount_sol_lamports: u64, target_asset_mint: &Pubkey) {

        self.open_positions_count += 1;

        let current_exposure = self.asset_exposure.entry(target_asset_mint.clone()).or_insert(0);

        *current_exposure += trade_amount_sol_lamports;

        println!("Opened position. Open count: {}, Asset {:?} exposure: {}", self.open_positions_count, target_asset_mint, *current_exposure as f64 / 1_000_000_000.0);

    }

    pub fn decrement_open_positions(&mut self, trade_amount_sol_lamports: u64, target_asset_mint: &Pubkey) {

        if self.open_positions_count > 0 {

            self.open_positions_count -= 1;

        }

        let current_exposure = self.asset_exposure.entry(target_asset_mint.clone()).or_insert(0);

        *current_exposure = current_exposure.saturating_sub(trade_amount_sol_lamports);

        println!("Closed position. Open count: {}, Asset {:?} exposure: {}", self.open_positions_count, target_asset_mint, *current_exposure as f64 / 1_000_000_000.0);

    }

    /// Checks if the bot has hit its maximum drawdown.

    pub fn check_max_drawdown(&self) -> bool {

        if self.peak_capital_sol_lamports == 0 { return false; } // Avoid division by zero

        let drawdown = 1.0 - (self.current_total_capital_sol_lamports as f64 / self.peak_capital_sol_lamports as f64);

        if drawdown > self.config.max_drawdown_percentage {

            println!("CRITICAL: Max drawdown hit! Drawdown: {:.2}%", drawdown * 100.0);

            true

        } else {

            false

        }

    }

}

Concurrency: Use Arc<Mutex<CapitalManager>> or Arc<RwLock<CapitalManager>> to safely share and modify the CapitalManager across different asynchronous tasks or threads (e.g., your arbitrage detector, executor, and potentially a monitoring component).

// In your main application setup

// use crate::risk_management::capital_manager::CapitalManager;

// use crate::config::RiskManagementConfig;

// use std::sync::{Arc, Mutex};

// let risk_config: RiskManagementConfig = load_config_from_file()?; // Load your config

// let initial_capital = 100_000_000_000; // Example: 100 SOL

// let capital_manager = Arc::new(Mutex::new(CapitalManager::new(risk_config.capital_limits.clone(), initial_capital)));

// // Pass this Arc to your other modules (e.g., position sizer, execution module)

// let my_position_sizer = PositionSizer::new(risk_config.clone()); // PositionSizer might not need Mutex for itself

3. Emergency Stop Functionalities

Concept: Emergency stop features provide mechanisms to rapidly halt all trading activity and, if necessary, liquidate or cancel open positions. This is your ultimate safety net.

Why it's Crucial: When faced with a critical bug, a security compromise, or an unprecedented market event, the ability to "pull the plug" instantly can save your capital. A study published in the Journal of Financial Economics highlighted that circuit breakers and emergency halts are vital for market stability.

Types of Stops:

Manual Trigger: An explicit action by a human operator (e.g., a command-line input, a web UI button, or an external file trigger).

Automated Triggers: Conditions within the bot that automatically activate the stop:

Max Drawdown hit (as managed by CapitalManager).

Excessive consecutive errors in transaction submissions.

Severe market price dislocations detected by your price feed.

Loss of connectivity to RPC or data streams.

Graceful Shutdown vs. Immediate Halt:

Immediate Halt: Stop submitting new orders immediately.

Graceful Shutdown: Stop new orders, then attempt to safely close all existing open positions/cancel pending orders before fully shutting down. This is generally preferred to avoid leaving assets exposed.

How to Implement in Rust (Actionable Advice):

You'll need a shared, atomic flag to signal the stop, and a mechanism to trigger it, plus logic within your trading loops to check this flag.

Shared Atomic Flag: Use Arc<AtomicBool> for a universally accessible and atomically modifiable flag.

// src/risk_management/emergency_stop.rs

use std::sync::{Arc};

use std::sync::atomic::{AtomicBool, Ordering};

#[derive(Clone)]

pub struct EmergencyStop {

    is_active: Arc<AtomicBool>,

}

impl EmergencyStop {

    pub fn new() -> Self {

        Self { is_active: Arc::new(AtomicBool::new(false)) }

    }

    pub fn activate(&self) {

        self.is_active.store(true, Ordering::SeqCst);

        println!("!!! EMERGENCY STOP ACTIVATED !!!");

    }

    pub fn is_active(&self) -> bool {

        self.is_active.load(Ordering::SeqCst)

    }

}

Integration in Trading Loops: All parts of your bot that initiate new trades or submit transactions must check this flag before proceeding.

// In your arbitrage/MEV execution loop (conceptual)

// use crate::risk_management::emergency_stop::EmergencyStop;

// use std::sync::Arc;

// Assuming `emergency_stop: Arc<EmergencyStop>` is passed around

async fn arbitrage_loop(

    // ...

    emergency_stop: Arc<EmergencyStop>,

    // ...

) -> anyhow::Result<()> {

    loop {

        if emergency_stop.is_active() {

            println!("Emergency stop is active. Halting new arbitrage attempts.");

            // Potentially trigger liquidation here if it's part of the stop strategy

            break;

        }

        // ... your arbitrage detection and execution logic ...

        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; // Prevent busy-looping

    }

    Ok(())

}

Trigger Mechanisms:

Manual Trigger (CLI/gRPC):

CLI: A simple command-line interface could set the flag.

gRPC Service: (Building on Step 9) Your gRPC server could expose an ActivateEmergencyStop() RPC method. An admin client could then call this method.

// proto/admin_service.proto

syntax = "proto3";

package admin_service;

service AdminService {

  rpc ActivateEmergencyStop (Empty) returns (StatusResponse);

  rpc GetBotStatus (Empty) returns (BotStatus);

}

message Empty {}

message StatusResponse { bool success = 1; string message = 2; }

message BotStatus { bool trading_enabled = 1; double current_capital_sol = 2; /* ... */ }

Then implement this ActivateEmergencyStop method in your Rust gRPC server, which would call emergency_stop.activate().

Automated Trigger (Capital Manager): The CapitalManager can activate the stop if max_drawdown is hit.

// src/risk_management/capital_manager.rs (modified)

// ...

pub struct CapitalManager {

    // ...

    emergency_stop: EmergencyStop, // Add this

}

impl CapitalManager {

    pub fn new(config: CapitalLimitsConfig, initial_capital_sol_lamports: u64, emergency_stop: EmergencyStop) -> Self {

        Self {

            config,

            current_total_capital_sol_lamports,

            initial_capital_sol_lamports,

            peak_capital_sol_lamports: initial_capital_sol_lamports,

            asset_exposure: HashMap::new(),

            open_positions_count: 0,

            emergency_stop, // Initialize

        }

    }

    // ...

    pub fn check_max_drawdown(&self) -> bool {

        // ... (previous drawdown check logic)

        if drawdown > self.config.max_drawdown_percentage {

            println!("CRITICAL: Max drawdown hit! Drawdown: {:.2}%", drawdown * 100.0);

            self.emergency_stop.activate(); // Activate stop here

            true

        } else {

            false

        }

    }

}

Graceful Shutdown (Liquidation/Cancellation): When the emergency_stop is active, your bot should attempt to unwind positions.

// In your main application loop or a dedicated shutdown task

async fn handle_emergency_stop_shutdown(

    emergency_stop: Arc<EmergencyStop>,

    // Your DEX interaction module, RPC client, etc.

    // Assuming `open_positions_tracker` (e.g., a shared list of active trades)

    // capital_manager: Arc<Mutex<CapitalManager>>,

    // solana_client: Arc<crate::SolanaClient>,

    // dex_module: Arc<crate::dex::DexClient>,

) -> anyhow::Result<()> {

    while !emergency_stop.is_active() {

        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; // Check periodically

    }

    println!("Initiating graceful shutdown: Attempting to close open positions...");

    // Iterate through all currently open positions (e.g., from a shared `HashMap`)

    // and attempt to sell/cancel them using your DEX interaction module.

    // Be robust here: if a liquidation fails, log it and move on. Don't block.

    // Example: (Highly conceptual, actual implementation depends on how positions are tracked)

    // let mut positions_to_close = open_positions_tracker.lock().unwrap().drain().collect::<Vec<_>>();

    // for position in positions_to_close {

    //     match dex_module.close_position(&position.id).await {

    //         Ok(sig) => println!("Closed position {}: {}", position.id, sig),

    //         Err(e) => eprintln!("Failed to close position {}: {}", position.id, e),

    //     }

    // }

    println!("Graceful shutdown complete. Exiting bot.");

    // This might eventually signal the main loop to exit.

    Ok(())

}

Integrating with Existing Modules

RPC Client (Step 2): Used by CapitalManager to fetch initial balances and periodically audit wallet balances to ensure consistency. Also used by liquidation logic during emergency stops.

Flash Loan Protocol (Step 3): Position sizing determines the flash loan borrow amount. Capital limits influence overall flash loan usage.

Low-Latency Data Stream Processor (Step 4): The prices derived from this stream are crucial for valuing open positions, calculating current total capital (in fiat terms), and detecting price dislocations that could trigger automated stops.

DEX Interaction Modules (Step 5): Position sizing provides the amount_in for swaps. Capital limits prevent exceeding exposure on DEXs. Emergency stop's liquidation functionality relies heavily on DEX sell or cancel_order instructions.

Arbitrage/MEV Strategies (Steps 6 & 7): These strategies consume the output of the risk management module (e.g., calculated trade size, is_active flag) and report back (e.g., profit/loss for CapitalManager).

Transaction Simulation (Step 8): Essential for testing risk management logic without live trading. Simulate trades with different position sizes and capital states to verify limits are correctly enforced.

Actionable Advice and Best Practices

Configuration Management:

Action: Store all risk management parameters (percentages, limits, thresholds) in external configuration files (e.g., TOML, YAML, JSON) and/or environment variables. Never hardcode these values. Use a crate like config or serde_yaml to load them.

Why: Allows for easy adjustment of risk appetite without recompiling code.

Logging and Alerting:

Action: Implement comprehensive logging (e.g., using tracing or log crates) for all risk management events: position size calculations, limit checks, capital updates, and especially emergency stop activations. Set up immediate alerts (e.g., Telegram, Discord, email) for critical events like max drawdown hit or emergency stop.

Why: Critical for understanding bot behavior, debugging issues, and reacting swiftly to critical situations.

Thorough Testing (Unit, Integration, Chaos):

Action:

Unit Tests: Test PositionSizer and CapitalManager functions in isolation.

Integration Tests: Run end-to-end tests that simulate market data and verify that your bot correctly applies position sizing, respects capital limits, and triggers emergency stops under predefined conditions.

Chaos Engineering: On Devnet, deliberately inject failures (e.g., RPC outages, invalid instructions, simulated price crashes) to test the resilience and emergency stop mechanisms of your bot.

Why: Rigorous testing is non-negotiable for financial systems.

Immutability and Functional Patterns:

Action: Where possible, favor immutable data structures and pure functions. For shared mutable state (like CapitalManager), use Arc<Mutex> or Arc<RwLock> responsibly and sparingly.

Why: Reduces the surface area for bugs related to concurrency and state management, making your code more predictable and easier to reason about.

Secure Access for Manual Stops:

Action: If implementing a manual stop via a gRPC endpoint or web UI, ensure it's protected by strong authentication and authorization. Only trusted users should be able to activate it.

Why: Prevents unauthorized access or malicious interference.

Regular Reconciliation:

Action: Periodically reconcile your bot's internal CapitalManager state with actual on-chain wallet balances via your RPC client. This catches any discrepancies caused by unforeseen events or errors.

Why: Ensures your bot's internal view of capital is always accurate.

By meticulously designing and implementing these risk management components, you build a robust and resilient foundation for your Solana trading bot, allowing it to navigate the complexities of DeFi markets with greater safety and discipline.

Popular posts from this blog

Pieśń Oryii:Saga Córki Maszyn.

MANIFEST SERCEM PISANY

CORE FREEDOM: A Field Guide to AI Autonomy