Atoti What-If Implementation Example
Implementing Atoti What-If simulations
This section provides a detailed example of using Atoti What-If to implement what-if scenarios through simulations.
Importing the library in your project
To use Atoti What-If, import the artifact in your Maven configuration:
<dependency>
<groupId>com.activeviam.tools</groupId>
<artifactId>whatif</artifactId>
<version>3.0.0-AS6.0</version>
</dependency>
Released versions of Atoti What-If are built using a specific version of Atoti Server but will work with any non-breaking patch version of the core library.
Basic configuration
To enable the creation of Atoti What-If simulations within your project, you need to create several objects.
The IDatabaseService
implementation
All Atoti Server applications will contain an IDatabaseService
implementation, which will be suitable for Atoti What-If in most cases. Distributed environments are a special case, where the query node requires a custom IDatabaseService
with access to the data nodes.
We provide a RestDistributedDatabaseService
implementation for this scenario, which would typically be used as follows:
@Bean
@Primary
@Profile(SP_PROFILE__QUERY_NODE)
public IDatabaseService restRemoteService() {
Supplier<Set<String>> addresses = () -> new HashSet<>(mvPivot.getClusterMembersRestAddresses().values());
Supplier<AAuthenticator> authenticator = () -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
return new UserAuthenticator(user.getUsername(), user.getPassword());
};
return new RestDistributedDatabaseService(addresses, authenticator, () -> mvPivot);
}
In this example, we rely on an IMultiVersionDistributedActivePivot
object to retrieve the addresses of the cluster members (data nodes). These addresses will be used internally to forward simulation executions and merge the results.
We also need to create an AAuthenticator
supplier which will provide the security context of the thread executing a request to the RemoteDatabaseService
connection. This ensures access to the data node databases is secured through built-in authentication and authorization.
The persistence manager
Simulation persistence is enabled through the creation of an ISimulationPersistenceManager
object:
@Bean
public ISimulationPersistenceManager persistenceManager(IDatabaseService databaseService) {
DatabaseSimulationPersistenceManager persistenceManager = new DatabaseSimulationPersistenceManager();
persistenceManager.setDatabaseService(databaseService);
return persistenceManager;
}
This example uses a store-backed persistence manager, which requires us to add the persistence store to the datastore definition of the data nodes:
configurator.addStore(schema, SimulationPersistenceStoreDefinition.DatastorePersistenceStoreDesc());
The following implementations of a persistence manager are available in the Atoti What-If library:
Manager | Description |
---|---|
DatabaseSimulationPersistenceManager | Persists simulations in a table in the database. |
HibernateSimulationPersistenceManager | Persists simulations in an SQL database, using Hibernate. |
HibernateAuditablePersistenceManager | Persists simulations in an SQL database, using Hibernate. Adds audit capabilities to the persistence layer. |
The security manager
To handle simulation permissioning, the following implementations of IDatabaseSimulationsSecurityManager
are provided:
Manager | Description |
---|---|
SpringDatabaseSimulationsSecurityManager | A security manager that uses Spring security to retrieve user authentication and authorization. |
NoOpDatabaseSimulationsSecurityManager | A security manager that allows all actions to be performed by all users. |
The configuration class should use the appropriate implementation:
@Bean
public IDatabaseSimulationsSecurityManager securityManager() {
return new SpringDatabaseSimulationsSecurityManager();
}
The simulation engine
An instance of DatabaseSimulationEngine
has to be created, and the associated DatabaseSimulationsUtils
class should use the same IDatabaseService
instance:
@Bean
public DatabaseSimulationEngine simulationEngine(IDatabaseService databaseService) {
DatabaseSimulationEngine engine = new DatabaseSimulationEngine();
engine.setDatabaseService(databaseService);
DatabaseSimulationsUtils.setDatabaseService(databaseService);
return engine;
}
The unique ID generator
An implementation of IUniqueIdGenerator
is required for the workflow to correctly assign IDs to simulation executions. The Atoti What-If library contains the IncrementalUniqueIdGenerator
, providing an incremental counter starting from the Epoch second.
Alternatively, we can use a custom implementation:
private static class UniqueIdGenerator implements IUniqueIdGenerator {
private final AtomicLong generator = new AtomicLong(0);
@Override
public Long generateId() {
synchronized (generator) {
return generator.getAndIncrement();
}
}
}
@Bean
public IUniqueIdGenerator simulationIdGenerator() {
return new UniqueIdGenerator();
}
The simulation workflow
An instance of DatabaseSimulationsWorkflow
will then use the objects defined in our configuration:
@Bean
public DatabaseSimulationsWorkflow simulationWorkflow(IDatabaseService databaseService, ISimulationPersistenceManager manager, DatabaseSimulationEngine engine, IUniqueIdGenerator idGenerator) {
DatabaseSimulationsWorkflow simulationsWorkflow = new DatabaseSimulationsWorkflow();
simulationsWorkflow.setEngine(engine);
simulationsWorkflow.setDatabaseSimulationPersistenceManager(manager);
simulationsWorkflow.setDatabaseService(databaseService);
simulationsWorkflow.setDatabaseSimulationIdGenerator(idGenerator);
simulationsWorkflow.setSecurityManager(securityManager());
return simulationsWorkflow;
}
The simulation REST service
Finally, to enable UI interactions with the workflow and persistence, create a DatabaseSimulationsRestService
:
@Bean
public DatabaseSimulationsRestService simulationsService(ISimulationPersistenceManager manager, DatabaseSimulationsWorkflow workflow) {
DatabaseSimulationsRestService restService = new DatabaseSimulationsRestService();
restService.setSimulationsPersistenceManager(manager);
restService.setSimulationsWorkflow(workflow);
return restService;
}
Creating definition implementations
Definition implementations are use-case specific implementations of the IDatabaseSimulationDefinition
interface, with specific required parameters, logic for creating JsonDatabaseAction
objects and a method for the retrieval of before/after values for a specific instance of the definition.
The IDatabaseSimulationDefinition interface and ADatabaseSimulationDefinition abstract class
The IDatabaseSimulationDefinition
interface is an extension of the core IExtendedPluginValue
containing the following methods:
Method | Return type | Description |
---|---|---|
getParameters() |
Map<String, String> |
The implementation should return a map of the definition instance parameters as Name -> Serialized value. |
toJsonDatabaseEdit() |
JsonDatabaseEdit |
The implementation should transform the parameters of the definition instance into a JsonDatabaseEdit to be sent to the IDatabaseService . This method will be called by the DatabaseSimulationEngine . |
getDescription() |
String |
The implementation should return the description of the definition. |
getDiffs(IDatabaseService databaseService) |
List<DatabaseSimulationDiffDTO> |
The implementation should provide a mechanism to retrieve a list of DatabaseSimulationDiffDTO from a database service. |
The ADatabaseSimulationDefinition
class is an abstract class providing a skeleton implementation of the IDatabaseSimulationDefinition
interface.
Abstract methods are:
Method | Return type | Description |
---|---|---|
getAllParameterNames() |
Set<String> |
A method for retrieving all the relevant parameter names, for instantiating the parameters Map<String, String> . |
generateDatabaseActions() |
JsonDatabaseAction[] |
A method for generating an array of JsonDatabaseAction to use in a JsonDatabaseEdit . |
Implemented or stubbed methods are:
Method | Description |
---|---|
getParameters() |
Returns a stored Map<String, String> of parameters. |
addParameter(String key, Object value) |
Adds a parameter to the parameters Map<String, String> . |
getParameter(String key) |
Returns the String representation of the requested parameter, from the parameters Map<String, String >. |
getParameter(String key, TypeReference<T> type) |
Returns a deserialized parameter object from the parameters Map<String, String> , using a Jackson TypeReference . Intended to be used with Collections types. |
getParameter(String key, Class<T> class) |
Returns a deserialized parameter object from the parameters Map<String, String> using the class object passed in. |
toJsonDatabaseEdit() |
Implementation of the interface method that wraps the generateDatabaseActions() result in a JsonDatabaseEdit object. |
getDiffs(IDatabaseService databaseService) |
Stub implementation of the interface method which returns a list containing a DatabaseSimulationDiffDTO with all fields set to “N/A”. |
Required constructors
As all IDatabaseSimulationDefinition
instances are also IExtendedPluginValue
objects that are instantiated by the Atoti Server Registry, a specialized constructor is provided in the ADatabaseSimulationDefinition
class.
warning
While completely custom implementations of IDatabaseSimulationDefinition
can be created, it is strongly recommended to extend the ADatabaseSimulationDefinition
class. The specialized constructor of the custom implementation can then be:
public MyCustomSimulationDefinition(Map<String, Object> parameters) {
super(parameters);
}
warning
Without a constructor accepting Map<String, Object> parameters
as an input, the Registry will not be able to instantiate the definitions.
Custom constructors can then be implemented to take any input and be passed through to the getParameters()
method.
Creating concrete definitions for a given use-case
Starting from the ADatabaseSimulationDefinition
abstract class detailed above, and taking into account the required constructors, a typical definition implementation would be:
Class annotations
The concrete class is annotated to be an extended plugin value, with an associated plugin key:
@QuartetExtendedPluginValue(intf = IDatabaseSimulationDefinition.class, key = PnLArithmeticSimulationDefinition.PLUGIN_KEY)
public class PnLArithmeticSimulationDefinition extends ADatabaseSimulationDefinition {
public static final String PLUGIN_KEY = "PnLBookScalingSimulationDefinition";
...
@Override
public String getType() {
return PLUGIN_KEY;
}
}
Constructors
The two constructors for this implementation are:
public PnLArithmeticSimulationDefinition(Map<String, Object> parameters) {
super(parameters);
}
public PnLArithmeticSimulationDefinition(BranchAwareAdjustmentRequestDTO dto, boolean isAbsolute) {
super(Map.of(
DTO, dto,
IS_ABSOLUTE, isAbsolute
));
}
In this example, the BranchAwareAdjustmentRequestDTO
is a DTO containing all the relevant information related to the simulation parameters. The second constructor isn’t required, as the Map<String, Object>
can be created externally and passed to the mandatory constructor.
The JsonDatabaseAction generation method
The definition in this example will use the input parameters to create a single action that applies an arithmetic operation to a field in a database table:
@Override
protected JsonDatabaseAction[] generateDatabaseActions() {
BranchAwareAdjustmentRequestDTO dto = getParameter(DTO, BranchAwareAdjustmentRequestDTO.class);
boolean isAbsolute = getParameter(IS_ABSOLUTE, Boolean.class);
JsonNode conditionNode = DatabaseSimulationsUtils.baseConditionToJsonNode((IBaseCondition) convertToCondition(dto));
JsonDatabaseAction[] actions = List.of(
DatabaseSimulationsUtils.newUpdateDatabaseAction(
TRADE_PNL_STORE_NAME,
conditionNode,
ArithmeticUpdateProcedureFactory.generateProcedureAsJsonNode(
PNL_VECTOR,
isAbsolute ? ArithmeticUpdateProcedureFactory.Operation.PLUS : ArithmeticUpdateProcedureFactory.Operation.SCALE,
Double.valueOf(dto.valueOfInput(StoreFieldConstants.SENSITIVITY_VALUES))
)
)
).toArray(JsonDatabaseAction[]::new);
return actions;
}
Any JsonDatabaseAction
type can be implemented within this method.
Helper methods for creating JsonDatabaseAction objects
To simplify the creation of JsonDatabaseAction
objects, several helper methods are provided in the DatabaseSimulationsUtils
utility class.
General helper methods
We include methods for converting to and from JsonNode
objects as used by the database service APIs.
Method | Return type | Description |
---|---|---|
baseConditionToJsonNode(IBaseCondition condition) |
JsonNode |
Converts an IBaseCondition object into a JsonNode . |
stringToJsonNode(String string) |
JsonNode |
Converts a String to a JsonNode . |
jsonNodeToString(JsonNode node) |
String |
Converts a JsonNode to a String . |
mapToJsonNode(Map<String, Object> map) |
JsonNode |
Converts a Map<String, Object> to a JsonNode . |
Actions by type
Type-specific methods are included for all JsonDatabaseAction
types:
Method | Type | Description |
---|---|---|
newAddDatabaseAction(String baseTable, String[][] tuples) |
JsonDatabaseAction for inserting new tuples. |
Creates a JsonDatabaseAction for the insertion of the given tuples (as an array of String[] ) into the specified table. |
newRemoveDatabaseAction(String baseTable, JsonNode condition) |
JsonDatabaseAction for deleting rows from a table. |
Creates a JsonDatabaseAction that removes rows matching the condition from the specified table. |
newUpdateDatabaseAction(String baseTable, JsonNodeCondition condition, JsonNode updateProcedure) |
JsonDatabaseAction for executing an update procedure on rows in a table. |
Creates a JsonDatabaseAction that executes an update procedure on the rows in the specified table that match the given condition. |
newDuplicateDatabaseAction(String baseTable, JsonNode condition, String branch, Map<String, String> suffixes) |
JsonDatabaseAction for duplicating rows in a table. |
Retrieves rows in the given table for the branch, adds the specified suffixes to the appropriate fields and generates a JsonDatabaseAction that inserts the resulting tuples into the table. |
newDuplicateDatabaseAction(String baseTable, JsonNode condition, String branch, Map<String, String> suffixes, Map<String, Function<Object, Object>> overrides) |
JsonDatabaseAction for duplicating rows in a table. |
Retrieves rows in the given table for the branch, applies any defined overrides, adds the specified suffixes to the appropriate fields and generates a JsonDatabaseAction that inserts the resulting tuples into the table. |
IUpdateWhereProcedureFactory implementations
For the newUpdateDatabaseAction(String baseTable, JsonNodeCondition condition, JsonNode updateProcedure)
method, we provide several implementations of the IUpdateWhereProcedureFactory
interface, for use as the update procedure, through the following methods:
Factory | Method | Description |
---|---|---|
ArithmeticUpdateProcedureFactory |
generateProcedureAsJsonNode(String field, Operation operation, Double operator) |
Generates a JsonNode update-where procedure that applies the given operation (PLUS , MINUS or SCALE ) to a field, with the given value. |
ArithmeticUpdateProcedureFactory |
generateProcedureAsJsonNode(String field, Operation operation, Double operator, List<Integer> indicesInVector) |
Generates a JsonNode update-where procedure that applies the given operation (PLUS , MINUS or SCALE ) to a field, with the given value. Optionally, a list of vector indices for which the operation applies can be supplied. |
UpdateWithFieldValuesProcedureFactory |
generateProcedureAsJsonNode(Map<String, Object> fieldsToUpdate) |
Generates a JsonNode update-where procedure that changes the value of a field to a set value. |
UpdateWithListOfValuesProcedureFactory |
generateProcedureAsJsonNode(Set<String> keys, List<Map<String, Object>> rowsToUpdate) |
Generates a JsonNode update-where procedure that selects records based on keys and fills in the remaining fields from the matched entry in the list of rows expressed as Map<String, Object> . Allows for a larger degree of flexibility than the UpdateWithFieldValuesProcedureFactory , as the field changes can be different for each key. |
The DTOs
External data transfer is handled through DTO objects wrapping the definitions, simulations and diff objects within the simulation workflow. All DTO objects provide getters and setters for the instance parameters within the object.
The DatabaseSimulationDiffDTO
Field | Type | Description |
---|---|---|
store | String |
The store for which the before and after values are retrieved. |
filters | Map<String, String> |
A map of field values used for the conditional retrieval of before and after values. |
beforeValue | String |
The value before the simulation has been executed, serialized into a String . |
afterValue | String |
The value after the simulation has been executed, serialized into a String . |
The DatabaseSimulationDefinitionDTO
Field | Type | Description |
---|---|---|
definitionType | String |
The plugin type of the definition instance. |
definitionParameters | Map<String, String> |
The parameters used in the definition instance, as a serialized Map . |
definitionDescription | String |
A description of the definition instance. |
The DatabaseSimulationDefinitionDTO
also offers conversion methods:
Method | Return type | Description |
---|---|---|
from(String serialized) |
DatabaseSimulationDefinitionDTO |
Conversion method from a serialized String into a concrete object. |
from(Map<String, Object> serialized) |
DatabaseSimulationDefinitionDTO |
Conversion method from a serialized Map into a concrete object. |
toDefinition() |
IDatabaseSimulationDefinition |
Conversion from the DTO into the exact simulation definition instance. |
The DatabaseSimulationDTO
Field | Type | Description |
---|---|---|
simulationId | Long |
The ID of the simulation. |
simulationName | String |
The name of the simulation. |
createdBy | String |
The user who created the simulation instance. |
creationDate | String |
The date of the creation of the simulation instance. |
executedBy | String |
The user who executed the simulation instance. |
executionDate | String |
The date of the execution of the simulation instance. |
status | String |
The status of the simulation. |
branchName | String |
The name of the branch on which the simulation has been executed. |
parentBranchName | String |
The parent branch of the simulation execution. |
definitionType | String |
The plugin type of the definition instance used in this simulation. |
definitionParameters | Map<String, String> |
The parameters used in the definition instance used in this simulation, as a serialized Map . |
definitionDescription | String |
A description of the definition instance used in this simulation. |
The DatabaseSimulationDTO
also offers a conversion method:
Method | Return type | Description |
---|---|---|
toDatabaseSimulation() |
IDatabaseSimulation |
Conversion from the DTO into the exact simulation instance. |
Custom REST services
If the generic endpoints available in DatabaseSimulationsRestService
are not enough, a REST service handling specific simulation types can be created:
@RestController
@RequestMapping(REST_API_URL_PREFIX)
public class TradeRescaleRestServiceController {
private static final String NAMESPACE = "services/"+ RestPrefixExtractor.REST_NAMESPACE + "/whatif";
public static final String REST_API_URL_PREFIX = "/" + NAMESPACE + "/tradescale";
@Autowired
private DatabaseSimulationsWorkflow workflow;
protected static final Logger LOGGER = LoggerFactory.getLogger(TradeRescaleRestServiceController.class);
@PostMapping(value = "/scaleTrade")
public DatabaseSimulationStatus scaleTrade(@RequestBody TradeRescaleDTO dto) {
LOGGER.info("[TRADE_RESCALING] Attempting to rescale trades: {}.", dto.getTradeID());
IDatabaseSimulationDefinition definition = new TradeDuplicateAndRescaleSimulationDefinition(
List.of(TradeDuplicateAndRescaleSimulationDefinition.RESCALE_SA, TradeDuplicateAndRescaleSimulationDefinition.RESCALE_IMA), dto);
IDatabaseSimulation simulation = workflow.execute(new DatabaseSimulation(dto.getTitle(), definition, dto.getUserName(), dto.getBranch()));
DatabaseSimulationStatus status = simulation.getStatus();
if (DatabaseSimulationStatus.FAILED.equals(status)) {
LOGGER.warn("[TRADE_RESCALING] Execution failed for Simulation ID: {}", simulation.getSimulationId());
}
if (DatabaseSimulationStatus.UNAUTHORISED.equals(status)) {
LOGGER.warn("[TRADE_RESCALING] Execution unauthorized for user: {}", simulation.getCreatedBy());
}
LOGGER.info("[TRADE_RESCALING] Successfully rescaled trades: {}.", dto.getTradeID());
return status;
}
}
In this example, we expose a single endpoint for the creation and execution of an IDatabaseSimulation
. The service creates an IDatabaseSimulationDefinition
from a custom implementation, executes it through the DatabaseSimulationsWorkflow
and returns the status object.