The Atoti Market Risk schema contains a number of side stores. These stores are not part of the star-schema â they are not joined to any of the base stores â but
they contain reference data. During a calculation data is retrieved from these stores by way of get-by-key, list, or distinct queries. In the course of a complex
calculation, such as PnL Explain, there may be many thousands of these queries.
With an in-memory Atoti application, the datastore is highly optimized to handle these operations and, even with high volumes, they are
executed quickly. However, in an Atoti application using DirectQuery, each of these queries must be executed on the external database.
This may mean many thousands of database queries over a network, which will drastically slow any user workflows.
Atoti Market Risk includes two solutions to this problem: full table caching and the DirectQuery Local Cache.
Full table caching
Overview
With this approach, at startup, a side store is copied in its entirety from the external database to an in-memory datastore. From there, the data can be queried with
the full performance of an Atoti Server in-memory application. This is the default approach in Atoti Market Risk, used for all side stores.
Specifically, the following stores will be cached:
- SpotMarketData
- CurveMarketData
- SurfaceMarketData
- CubeMarketData
- FxRateMarketData
- Scenarios
- CorrelationMarketData
- DividendMarketData
- SplitRatioMarketData
- MarketShifts
- DynamicTenors
- DynamicMaturities
- DynamicMoneyness
- SensiLadders
- CubeLevelAdjustments (if using Sign-Off)
Quickstart
If you are using Atoti Market Risk out-of-the box, without customization, you don’t need to make any changes to use this feature. Simply follow the
instructions to enable DirectQuery and the configured stores listed in the preceding section will be automatically cached at startup.
Detailed setup
If you have made changes to Atoti Market Risk or are only using specific components, this section describes how caching is configured and the classes you
need to ensure are present in your application.
The ISideStoresToClone interface is used to declare stores to be cached. Beans of this type are autowired to DirectQueryActivePivotConfig where they
are configured for caching.
Atoti Market Risk has configuration classes that define these beans for you. Make sure they are imported in your project for the configured stores to be cached.
| Store | Configuration class | Module | Imported by |
|---|
| SpotMarketData | DQCommonStoreConfig | mr-common-config | MarketRiskConfig |
| CurveMarketData | DQCommonStoreConfig | mr-common-config | MarketRiskConfig |
| SurfaceMarketData | DQCommonStoreConfig | mr-common-config | MarketRiskConfig |
| CubeMarketData | DQCommonStoreConfig | mr-common-config | MarketRiskConfig |
| FxRateMarketData | DQCommonStoreConfig | mr-common-config | MarketRiskConfig |
| Scenarios | DQStandardStoreConfig | mr-common-config | MarketRiskConfig |
| CorrelationMarketData | DQSensiStoreConfig | mr-sensi-config | SensiCompleteConfig |
| DividendMarketData | DQSensiStoreConfig | mr-sensi-config | SensiCompleteConfig |
| SplitRatioMarketData | DQSensiStoreConfig | mr-sensi-config | SensiCompleteConfig |
| MarketShifts | DQSensiStoreConfig | mr-sensi-config | SensiCompleteConfig |
| DynamicTenors | DQSensiStoreConfig | mr-sensi-config | SensiCompleteConfig |
| DynamicMaturities | DQSensiStoreConfig | mr-sensi-config | SensiCompleteConfig |
| DynamicMoneyness | DQSensiStoreConfig | mr-sensi-config | SensiCompleteConfig |
| SensiLadders | DQSensiStoreConfig | mr-sensi-config | SensiCompleteConfig |
| CubeLevelAdjustments | SignOffTaskConfig | mr-application | MarketRiskConfig |
At startup, Atoti Market Risk will create the appropriate DirectQuery schema and copy all rows from the underlying database table to the cache store. Once
this is complete, you can query the cache stores just as you would query any of these stores when running Atoti Market Risk in-memory. Indeed, the cache stores
are created with the same name as the store would have in an in-memory application, so you should not need to make any changes to existing post-processors.
- This feature relies on the
Migrator, a component used by Atoti Market Risk to create the Atoti Server schema. Ensure you are using the Migrator
in your project to use this feature.
- The
DirectQueryActivePivotConfig class in the mr-directquery module contains the Migrator configuration. Ensure this class is imported in your project
for the cache to work. This class is imported by MRDirectQueryConfig which is itself imported by MarketRiskConfig in the mr-application module.
Customizations
Caching a new store
To create and cache a new side store, you must add the store to the datastore description
and define a source configuration.
The source configuration is used to load data into the store.
With that in place, simply expose an ISideStoresToClone bean providing the name of your store to be cached.
@Bean
public ISideStoresToClone customStoresToClone() {
return collection -> {
collection.add("MyCustomTable");
};
}
- This feature relies on the
Migrator, a component used by Atoti Market Risk to create the Atoti Server schema. Ensure you are using the Migrator
in your project to use this feature.
- The
DirectQueryActivePivotConfig class in the mr-directquery module contains the Migrator configuration. Ensure this class is imported in your project
for the cache to work. This class is imported by MRDirectQueryConfig which is itself imported by MarketRiskConfig in the mr-application module.
Only side stores can be cached in-memory.You can’t cache referenced stores as the reference would be between in-memory and remote tables, which is not supported.
Validate the setup
You can validate that a cache is configured correctly by taking a look in Atoti Admin UI. You should see one store with the “_CACHE_SOURCE” suffix â
this is the table in the database. The in-memory table is the one without the suffix.
In the screenshot you can see the Table “FXRates” as both an in-memory store (named “FXRates”) and as a remote DirectQuery database table (named “FXRates_CACHE_SOURCE”).
Both of these stores contain the same data.
DirectQuery Local Cache
Overview
Full table caching provides excellent performance in terms of retrieving data. However, it may result in high memory consumption.
Some side stores contain a significant volume of data and, particularly when the dataset spans a long time, it may be prohibitive to store this
entirely in memory.
The DirectQuery Local Cache
is a solution for these cases. Instead of caching the entire table, the DirectQuery Local Cache only stores partitions of the underlying table. These partitions
are loaded when required by a particular query and held in memory until the cache exceeds a specified size. This cache is built into Atoti Server,
and Atoti Market Risk is configured to use this cache for specific side stores.
See the Atoti Server DirectQuery Local Cache documentation
to understand how the cache works, and for details on how to use it in an Atoti application. For this guide, we will assume you have read this documentation.
The following stores are currently configured to work with the DirectQuery Local Cache:
- SpotMarketData
- CurveMarketData
- SurfaceMarketData
- CubeMarketData
- FxRateMarketData
- CorrelationMarketData
- DividendMarketData
- SplitRatioMarketData
- MarketShifts
Quickstart
If you are using Atoti Market Risk out-of-the box, without customization, this feature is enabled by setting the following property:
mr.enable.preview.directquery-cache=true
With this property set, the configured stores listed in the preceding sections will be configured to use the DirectQuery Local Cache. All other side stores will continue to use
full table caching. Atoti Market Risk post-processors that retrieve data from these stores are configured to populate the caches as required.
Cache partitioning
Each cache stores a number of cache partitions.
Each partition is a subset of the overall table. Partitions are loaded into the cache based on the requirements of a query.
For the market data stores â SpotMarketData, CurveMarketData, SurfaceMarketData, CubeMarketData, FxRateMarketData, CorrelationMarketData and DividendMarketData
â the caches are partitioned by AsOfDate and MarketDataSet. The SplitRatioMarketData store is partitioned by AsOfDate only.
For the MarketShifts store, the cache is partitioned by AsOfDate and ScenarioSet.
Cache capacity
Each cache is created with a cache capacity,
which determines the maximum number of rows or partitions to keep in the cache. You can configure the cache capacity by exposing a supplier bean for the relevant cache.
@Configuration
public class CacheCapacityConfig {
// Set the capacity of the SpotMarketData store
@Bean
public SpotMarketDataDirectQueryCacheConfig.CacheCapacitySupplier spotCapacity() {
return () -> CacheCapacity.MaxPartitionsCount.of(5);
}
// Set the capacity of the CurveMarketData store
@Bean
public CurveMarketDataDirectQueryCacheConfig.CacheCapacitySupplier curvCapacity() {
return () -> CacheCapacity.MaxPartitionsCount.of(5);
}
// Set the capacity of the SurfaceMarketData store
@Bean
public SurfaceMarketDataDirectQueryCacheConfig.CacheCapacitySupplier surfaceCapacity() {
return () -> CacheCapacity.MaxPartitionsCount.of(5);
}
// Set the capacity of the CubeMarketData store
@Bean
public CubeMarketDataDirectQueryCacheConfig.CacheCapacitySupplier cubeCapacity() {
return () -> CacheCapacity.MaxPartitionsCount.of(5);
}
// Set the capacity of the FxRateMarketData store
@Bean
public FxRateMarketDataDirectQueryCacheConfig.CacheCapacitySupplier fxRateCapacity() {
return () -> CacheCapacity.MaxPartitionsCount.of(5);
}
// Set the capacity of the CorrelationMarketData store
@Bean
public CorrelationMarketDataDirectQueryCacheConfig.CacheCapacitySupplier correlationCapacity() {
return () -> CacheCapacity.MaxPartitionsCount.of(5);
}
// Set the capacity of the DividendMarketData store
@Bean
public DividendMarketDataDirectQueryCacheConfig.CacheCapacitySupplier dividendCapacity() {
return () -> CacheCapacity.MaxPartitionsCount.of(5);
}
// Set the capacity of the SplitRatioMarketData store
@Bean
public SplitRatioMarketDataDirectQueryCacheConfig.CacheCapacitySupplier splitRatioCapacity() {
return () -> CacheCapacity.MaxPartitionsCount.of(5);
}
// Set the capacity of the MarketShifts store
@Bean
public MarketShiftDirectQueryCacheConfig.CacheCapacitySupplier marketShiftCapacity() {
return () -> CacheCapacity.MaxPartitionsCount.of(5);
}
}
Detailed setup
If you have made changes to Atoti Market Risk or are only using specific components, this section describes how caching is configured and the classes you need to ensure
are present in your application.
Cache descriptions
For a store to be cached, a cache description must be created at startup and fed into the Atoti application. Atoti Market Risk includes cache descriptions
for each of the configured stores, contained in a Spring configuration class and exposed as a bean.
| Store | Configuration class | Module | Imported by |
|---|
| SpotMarketData | SpotMarketDataDirectQueryCacheConfig | market-data-config (Atoti Market Data) | AtotiMarketDataDirectQueryCacheConfig, which is imported by MarketRiskConfig |
| CurveMarketData | CurveMarketDataDirectQueryCacheConfig | market-data-config (Atoti Market Data) | AtotiMarketDataDirectQueryCacheConfig, which is imported by MarketRiskConfig |
| SurfaceMarketData | SurfaceMarketDataDirectQueryCacheConfig | market-data-config (Atoti Market Data) | AtotiMarketDataDirectQueryCacheConfig, which is imported by MarketRiskConfig |
| CubeMarketData | CubeMarketDataDirectQueryCacheConfig | market-data-config (Atoti Market Data) | AtotiMarketDataDirectQueryCacheConfig, which is imported by MarketRiskConfig |
| FxRateMarketData | FxRateMarketDataDirectQueryCacheConfig | market-data-config (Atoti Market Data) | AtotiMarketDataDirectQueryCacheConfig, which is imported by MarketRiskConfig |
| CorrelationMarketData | CorrelationMarketDataDirectQueryCacheConfig | mr-sensi-config | MarketRiskConfig |
| DividendMarketData | DividendMarketDataDirectQueryCacheConfig | mr-sensi-config | MarketRiskConfig |
| SplitRatioMarketData | SplitRatioMarketDataDirectQueryCacheConfig | mr-sensi-config | MarketRiskConfig |
| MarketShifts | MarketShiftDirectQueryCacheConfig | mr-sensi-config | MarketRiskConfig |
If you aren’t using all of these stores in your project, you may remove the corresponding imports from MarketRiskConfig.
The DirectQueryActivePivotConfig class in the mr-directquery module collects these cache description beans and feeds them into the application configuration.
Make sure this class is imported in your project for the cache to work. This class is imported by MRDirectQueryConfig which is itself imported by
MarketRiskConfig in the mr-application module.
IDatabaseCacheManager
You add data to the cache by defining a DatabaseCachePrefetcher in post-processors that query the cache. To create this prefetcher, the post-processor requires an instance of the
IDatabaseCacheManager.
The cache manager is exposed as a bean by the DirectQueryActivePivotConfig class in the mr-directquery module, after it has created
the application. Make sure this class is imported in your project to have access to the cache manager. This class is imported by MRDirectQueryConfig, which is
itself imported by MarketRiskConfig in the mr-application module.
The cache manager is injected into relevant post-processors by the RiskPostProcessorInjector configuration class in the mr-application module. Make sure
this class is imported in your project for the injection to take place. This class is imported by MarketRiskConfig in the mr-application module. If you are
not using the RiskPostProcessorInjector you will need to inject the cache manager yourself:
Registry.getExtendedPlugin(IPostProcessor.class)
.values()
.stream()
.filter(ppFactory -> IMRDirectQueryCachingPostProcessor.class.isAssignableFrom(ppFactory.getImplementationClass()))
.forEach(ppFactory -> ppFactory.getStateTransfer().inject(IDirectQueryCachingPostProcessor.PROPERTY_NAME, databaseCacheManager));
- Atoti Market Risk uses the Atoti Market Data module for the market data stores and post-processors. The
IDatabaseCacheManager is injected into Atoti Market
Data post-processors by the com.activeviam.marketdata.config.directquery.DatabaseCacheManagerInjectionConfig configuration class.
Ensure this is imported in your project.
- If you are using the out-of-the-box Atoti Market Risk project, it is imported in
AtotiMarketDataDirectQueryCacheConfig, which is itself imported by MarketRiskConfig.
Step 2: The measure configuration
We will define a custom measure to use our post-processor in the package com.activeviam.mr.application.examples.dqcache.measure.
We will use the annotation @SensitivitiesCopperContextBean to achieve that.
We simply create a post-processed measure with Copper, using the plugin key of the post-processor and assigning a name to the measure:
package com.activeviam.mr.application.examples.dqcache.measure;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import com.activeviam.activepivot.copper.api.Copper;
import com.activeviam.activepivot.copper.api.CopperMeasure;
import com.activeviam.mr.application.examples.dqcache.postprocessor.CustomMarketShiftLookUpPostProcessor;
import com.activeviam.mr.sensi.measures.spring.intf.SensitivitiesCopperContextBean;
@Configuration
public class CustomMeasureMarketShiftsConfig {
@SensitivitiesCopperContextBean
@Qualifier("customMarketShiftLookUpMeasure")
public CopperMeasure customMarketShiftLookUpMeasure() {
return Copper.newPostProcessor(CustomMarketShiftLookUpPostProcessor.PLUGIN_KEY).withFormatter("DOUBLE[#,##0.0000;-#,##0.0000]").as("Custom Market Shift measure");
}
}
We need to import the configuration class that we have just created in the MarketRiskConfig class:
@Import(value = {
...
// Added after all the other imports
CustomSideStoreConfig.class,
CustomSideStoreCacheConfig.class,
CustomSideStoreLookUpMeasureConfig.class,
CustomMeasureMarketShiftsConfig.class
})
@Slf4j
public class MarketRiskConfig {
...
}
Testing the custom measure
The implementation is complete, we’re now going to see how that measure can be displayed.
We will update the integration test that we created earlier
with a new method:
@Test
void testCustomMarketShiftLookUpQuery() {
CubeFeedingPromise.waitForAllSchemaAttachDatabase(apConfig.activePivotManager());
CubeTester tester = CubeTester.from(manager);
var testResult = tester.mdxQuery().withMdx(
"""
SELECT
NON EMPTY {
[Measures].[Custom Market Shift measure]
} ON COLUMNS,
NON EMPTY Hierarchize(
Descendants(
{
[Risk].[Risk Factors].[ALL].[AllMember]
},
1,
SELF_AND_BEFORE
)
) ON ROWS
FROM (
SELECT
{
[Risk].[Risk Factors].[ALL].[AllMember].[AB Volvo_Implied volatility],
[Risk].[Risk Factors].[ALL].[AllMember].[AB Volvo_Spot price],
[Risk].[Risk Factors].[ALL].[AllMember].[Allegheny Energy_Credit spread]
} ON COLUMNS
FROM [Sensitivity Cube]
)
CELL PROPERTIES VALUE, FORMATTED_VALUE, BACK_COLOR, FORE_COLOR, FONT_FLAGS
""").run().show().getTester();
assertEquals(3, testResult.getCellsCount());
assertEquals(-0.14143821059364572, testResult.findCell().coordinate(RISK_FACTOR_LEVEL, "AB Volvo_Implied volatility").getCell().getValue());
assertEquals(-16.923453485618467, testResult.findCell().coordinate(RISK_FACTOR_LEVEL, "AB Volvo_Spot price").getCell().getValue());
assertEquals(-6.973620821803489, testResult.findCell().coordinate(RISK_FACTOR_LEVEL, "Allegheny Energy_Credit spread").getCell().getValue());
}