Source code for nqs_sdk.core.simulation
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from nqs_sdk.interfaces import ProtocolFactory
from nqs_sdk.nqs_sdk import ProtocolFactoryAdapter, Simulator, SimulatorBuilder
from .config import ConfigLoader
[docs]
class Simulation:
"""
Main orchestrator for running simulations.
This class serves as the primary interface for users to configure, build, and execute
simulations involving multiple DeFi protocols. It handles the coordination between
protocol factories, configuration loading, and simulation execution.
Protocols and configuration are provided during initialization, and the simulation
can be run multiple times with different parameters.
Attributes:
protocols: List of protocol factories or protocol factory adapters that define the DeFi protocols to simulate
config: Configuration data (file path, dict, or YAML/JSON content)
simulator: Internal rust-based simulator instance (built lazily)
"""
[docs]
def __init__(
self,
protocols: Union[ProtocolFactory | ProtocolFactoryAdapter, List[ProtocolFactory | ProtocolFactoryAdapter]],
config: Union[str, dict, Path],
namespace: Optional[str] = None,
):
"""
Initialize a new simulation with specified protocols and configuration.
Args:
protocols: Single ProtocolFactory or ProtocolFactoryAdapter,
or list of ProtocolFactorys or ProtocolFactoryAdapters defining
the protocols to include in the simulation
config: Configuration for the simulation. Can be:
- Path to a YAML/JSON configuration file
- Dictionary containing configuration parameters
- String containing YAML/JSON configuration content
Example:
>>> from nqs_sdk import Simulation
>>> from nqs_sdk.protocols import UniswapV3Factory
>>> from nqs_sdk_extension.protocols import CompoundV2Factory
>>> uniswap = UniswapV3Factory()
>>> compound = CompoundV2Factory()
>>> sim = Simulation([uniswap, compound], "config.yaml")
"""
self.protocols = [protocols] if not isinstance(protocols, list) else protocols
self.config = config
self.simulator = None
self.is_backtest = False # FIXME: get this from binding instead
self._build()
def _build(self) -> None:
"""
Internal method to construct the simulation from protocols and configuration.
This method:
1. Loads and parses the configuration (YAML or JSON)
2. Creates a SimulatorBuilder with the parsed configuration
3. Registers all protocol factories with the builder
4. Builds the final simulator instance
Raises:
ValueError: If configuration format is invalid
FileNotFoundError: If configuration file doesn't exist
"""
config_content, config_format = ConfigLoader.load(self.config)
if isinstance(config_content, str):
self.is_backtest = "backtest" in config_content.lower()
elif isinstance(self.config, dict):
self.is_backtest = "backtest" in self.config or any(
"backtest" in str(k).lower() for k in self.config.keys()
)
if config_format == "yaml":
builder = SimulatorBuilder.from_yaml(config_content)
else:
builder = SimulatorBuilder.from_json(config_content)
for protocol in self.protocols:
if not isinstance(protocol, ProtocolFactoryAdapter):
protocol = ProtocolFactoryAdapter(protocol)
builder.add_factory(protocol)
self.simulator = builder.build()
[docs]
def get_protocol(self, protocol_id: str) -> Any:
"""
Retrieve a protocol instance by its identifier.
Args:
protocol_id: Unique identifier for the protocol (e.g., "uniswap_v3")
Returns:
The protocol instance corresponding to the given ID
Raises:
RuntimeError: If simulation hasn't been built yet
KeyError: If protocol_id doesn't exist in the simulation
Example:
>>> uniswap_protocol = sim.get_protocol("uniswap_v3")
>>> current_price = uniswap_protocol.get_current_price()
"""
if not self.simulator:
raise RuntimeError("Simulation has not been built yet")
return self.simulator.get_py_protocol(protocol_id)
[docs]
def run(self) -> Dict[str, Any]:
"""
Execute the simulation and return results.
This method runs the entire simulation from start to end block/timestamp,
processing all transactions and collecting metrics along the way.
Returns:
SimulationResults: Object containing all simulation data including:
- Protocol states at each block
- Agent portfolio values over time
- Transaction logs and fees
- Observable metrics and KPIs
Raises:
RuntimeError: If simulation hasn't been built yet
Example:
>>> results = sim.run()
>>> portfolio_value = results.get_agent_metric("alice", "total_holding")
"""
if not self.simulator:
raise RuntimeError("Simulation has not been built yet")
logging.info("Simulation starting...")
results = self.simulator.run_to_dict()
logging.info("Simulation ended")
return results
def to_json(self) -> str:
"""
Serialize the simulation configuration to JSON.
Returns:
JSON representation of the complete simulation configuration,
including all protocols, agents, and parameters
Raises:
RuntimeError: If simulation hasn't been built yet
"""
if not self.simulator:
raise RuntimeError("Simulation has not been built yet")
return self.simulator.to_json()
@classmethod
def from_json(cls, json_data: str) -> "Simulation":
"""
Deserialize a simulation from JSON configuration.
This class method allows reconstructing a Simulation instance from
a previously serialized JSON configuration.
Args:
json_data: JSON string containing simulation configuration
Returns:
New Simulation instance configured from the JSON data
Raises:
ValueError: If JSON is malformed or contains invalid configuration
Example:
>>> json_config = previous_sim.to_json()
>>> new_sim = Simulation.from_json(json_config)
"""
simulation = cls([], {})
simulation.simulator = Simulator.from_json(json_data)
return simulation