Thursday, March 1, 2018

PowerShell : scenario generator

There are always some game-changing moments in life. Earlier this year, I finally discovered the greatness of PowerShell. Love at first sight. As a decent "honeymoon project", I re-implemented one of my C# programs to PowerShell. This post is opening this particular program, which takes base market data as input and creates stress market scenarios (for a third-party analytics software), based on given XML configuration files.

It should be noted, that this particular program uses CSV file input for constructing base market data. However, since PowerShell does enjoy the benefits of accessing .NET framework classes, one may request source data directly from database - if so desired.

Program configurations


The following screenshot shows general configurations for this program. SourceFilePath attribute captures the source market data CSV file and TargetFolderPath captures the folder, into which all market scenario files will be created. Finally, ScenarioConfigurationsPath captures the folder, which contains all XML scenario configuration files. This configuration XML file should be stored in a chosen directory.

<Configurations>
    <!-- attributes for scenario generator settings -->
    <ScenarioConfigurationsPath>\\Temp\ScenarioConfigurations\</ScenarioConfigurationsPath>
    <SourceFilePath>\\Temp\baseMarket.csv</SourceFilePath>
    <TargetFolderPath>\\Temp\Scenarios\</TargetFolderPath>
</Configurations>

Market data


The following screenshot shows base market data. Due to brevity reasons, only USD swap curve has been used here as an example. All risk factors (market data points) are effectively given as key-value pairs (ticker, value). As an example, USD.IBOR.SWAP.3M.10Y.MID is a 10-year mid-market quote for USD-nominated swap, where floating rate index (USD Libor) is fixed and paid on a quarterly basis.

It should be noted, that the system for risk factor tickers construction should always lead to a scheme, in which every risk factor will have one and only one unique ticker. This will then guarantee, that we can drill down and stress individual risk factor with regex expressions, if so desired. This data should be copied to CSV file (directory has been defined in previous configuration file).

"USD.IBOR.CASH.3M.1BD.MID,0.01445"
"USD.IBOR.CASH.3M.1W.MID,0.0147313"
"USD.IBOR.CASH.3M.1M.MID,0.0159563"
"USD.IBOR.CASH.3M.2M.MID,0.0173311"
"USD.IBOR.CASH.3M.3M.MID,0.0189213"
"USD.IBOR.FRA.3M.3M.6M.MID,0.02283"
"USD.IBOR.FRA.3M.6M.9M.MID,0.02363"
"USD.IBOR.FRA.3M.9M.12M.MID,0.02503"
"USD.IBOR.SWAP.3M.2Y.MID,0.025387"
"USD.IBOR.SWAP.3M.3Y.MID,0.026844"
"USD.IBOR.SWAP.3M.4Y.MID,0.027649"
"USD.IBOR.SWAP.3M.5Y.MID,0.028163"
"USD.IBOR.SWAP.3M.6Y.MID,0.028579"
"USD.IBOR.SWAP.3M.7Y.MID,0.028935"
"USD.IBOR.SWAP.3M.8Y.MID,0.029236"
"USD.IBOR.SWAP.3M.9Y.MID,0.029516"
"USD.IBOR.SWAP.3M.10Y.MID,0.029758"
"USD.IBOR.SWAP.3M.11Y.MID,0.030049"
"USD.IBOR.SWAP.3M.12Y.MID,0.030192"
"USD.IBOR.SWAP.3M.15Y.MID,0.030526"
"USD.IBOR.SWAP.3M.20Y.MID,0.030775"

Risk factor class


A simple C# class is used to host one risk factor (ticker, value). It should be noted at this point, that this particular custom C# class will be used within our PowerShell script.

using System;
public class RiskFactor {
    public string key;
    public double value;
    // instance constructor
    public RiskFactor(string stream) {
        this.key = stream.Split(',')[0];
        this.value = Convert.ToDouble(stream.Split(',')[1]);
    }
    // deep copy constructor
    public RiskFactor(RiskFactor riskFactor) {
        key = riskFactor.key;
        value = riskFactor.value;
    }
    // object to string
    public override string ToString() {
        return String.Concat(key, ',', Convert.ToString(value));
    }
}

Scenario configurations


The following screenshot shows XML configurations for one market scenario. One such scenario can have several different scenario items (Say, stress these rates up, stress those rates down, apply these changes to all FX rates against EUR and set hard-coded values for all CDS curves). From these configurations, ID and description are self-explainable. Attribute regExpression captures all regex expressions (scenario items), which will be searched from risk factor tickers. As soon as regex match is found, the program will use corresponding operationType attribute to identify desired stress operation (addition, multiplication or hard-coded value). Finally, the amount of change which will be applied in risk factor value is defined within stressValue attribute. This XML configuration should be stored (directory has been defined in program configuration file).

<!-- operation types : 0 = ADDITION, 1 = MULTIPLICATION, 2 = HARD-CODED VALUE -->
<Scenario>
  <ID>CURVE.STRESS.CUSTOM.1</ID>
  <description>custom stress scenario for USD swap curve</description>
  <regExpression>^USD\.*.*CASH,^USD\.*.*FRA,^USD\.*.*SWAP</regExpression>
  <operationType>1,0,2</operationType>
  <stressValue>1.25,0.015,0.05</stressValue>
</Scenario>

Finally, the following screenshot shows resulting market data, when all configured scenario items have been applied. This is the content of output CSV file, created by PowerShell program.

"USD.IBOR.CASH.3M.1BD.MID,0.0180625"
"USD.IBOR.CASH.3M.1W.MID,0.018414125"
"USD.IBOR.CASH.3M.1M.MID,0.019945375"
"USD.IBOR.CASH.3M.2M.MID,0.021663875"
"USD.IBOR.CASH.3M.3M.MID,0.023651625"
"USD.IBOR.FRA.3M.3M.6M.MID,0.03783"
"USD.IBOR.FRA.3M.6M.9M.MID,0.03863"
"USD.IBOR.FRA.3M.9M.12M.MID,0.04003"
"USD.IBOR.SWAP.3M.2Y.MID,0.05"
"USD.IBOR.SWAP.3M.3Y.MID,0.05"
"USD.IBOR.SWAP.3M.4Y.MID,0.05"
"USD.IBOR.SWAP.3M.5Y.MID,0.05"
"USD.IBOR.SWAP.3M.6Y.MID,0.05"
"USD.IBOR.SWAP.3M.7Y.MID,0.05"
"USD.IBOR.SWAP.3M.8Y.MID,0.05"
"USD.IBOR.SWAP.3M.9Y.MID,0.05"
"USD.IBOR.SWAP.3M.10Y.MID,0.05"
"USD.IBOR.SWAP.3M.11Y.MID,0.05"
"USD.IBOR.SWAP.3M.12Y.MID,0.05"
"USD.IBOR.SWAP.3M.15Y.MID,0.05"
"USD.IBOR.SWAP.3M.20Y.MID,0.05"

Program


# risk factor class
Add-Type @"
using System;
public class RiskFactor {
    public string key;
    public double value;
    // instance constructor
    public RiskFactor(string stream) {
        this.key = stream.Split(',')[0];
        this.value = Convert.ToDouble(stream.Split(',')[1]);
    }
    // deep copy constructor
    public RiskFactor(RiskFactor riskFactor) {
        key = riskFactor.key;
        value = riskFactor.value;
    }
    // object to string
    public override string ToString() {
        return String.Concat(key, ',', Convert.ToString(value));
    }
}
"@


function ProcessScenario([System.Xml.XmlDocument]$scenario, [System.Collections.Generic.List[RiskFactor]]$riskFactors) {
    
    # extract scenario items
    $regExpressions = $scenario.SelectSingleNode("Scenario/regExpression").InnerText.Split(',')
    $operationTypes = $scenario.SelectSingleNode("Scenario/operationType").InnerText.Split(',')
    $stressValues = $scenario.SelectSingleNode("Scenario/stressValue").InnerText.Split(',')
    
    # loop through all scenario regex expression items
    for($i = 0; $i -lt $regExpressions.Count; $i++) {
        # loop through all risk factors
        $riskFactors | ForEach-Object { 
            $match = $_.key -match $regExpressions[$i]
            # conditionally, apply regex expression to risk factor value
            if($match -eq $true) {
                $stressedValue = $_.value
                switch($operationTypes[$i]) {
                    # addition
                    0 { $stressedValue += [double]$stressValues[$i] }
                    # multiplication
                    1 { $stressedValue *= [double]$stressValues[$i] }
                    # hard-coded value
                    2 { $stressedValue = [double]$stressValues[$i] }
                }
                $_.value = $stressedValue
            }
        }
    }
}


function Main() {

    # create program configurations from xml file
    $configurationFilePath = "\\Temp\Configurations.xml"
    $configurations = New-Object System.Xml.XmlDocument
    $configurations.Load($configurationFilePath)

    # extract risk factor objects to master list
    $riskFactors = New-Object System.Collections.Generic.List[RiskFactor]
    Import-Csv -Path $configurations.SelectSingleNode("Configurations/SourceFilePath").InnerText -Header stream | 
        ForEach-Object { $riskFactors.Add((New-Object RiskFactor($_.stream))) }

    # extract all scenario xml configurations to list
    $scenarios = New-Object System.Collections.Generic.List[System.Xml.XmlDocument]
    Get-ChildItem -Path $configurations.SelectSingleNode("Configurations/ScenarioConfigurationsPath").InnerText | 
        ForEach-Object { 
            $scenario = New-Object System.Xml.XmlDocument
            $scenario.Load($_.FullName)
            $scenarios.Add($scenario) 
        }

    # loop through all scenarios xml configurations
    $scenarios | ForEach-Object { 
        # create risk factor list deep copy
        $riskFactorsDeepCopy = New-Object System.Collections.Generic.List[RiskFactor]
        $riskFactors | ForEach-Object { 
            $riskFactorsDeepCopy.Add((New-Object RiskFactor($_))) 
        }
        # apply scenario changes to copied risk factor values
        ProcessScenario $_ $riskFactorsDeepCopy

        # create output directory and remove existing file if exists
        $scenarioFileName = [System.String]::Concat($_.SelectSingleNode("Scenario/ID").InnerText, ".csv")
        $targetFolderPath = $configurations.SelectSingleNode("Configurations/TargetFolderPath").InnerText
        $scenarioFilePathName = [System.IO.Path]::Combine($targetFolderPath, $scenarioFileName)
        Remove-Item -Path $scenarioFilePathName -ErrorAction SilentlyContinue
        
        # create csv output file for processed risk factors
        $riskFactorsDeepCopy | ForEach-Object { New-Object psobject -Property @{string = $_.ToString() } } |
            ConvertTo-Csv -NoTypeInformation | select -Skip 1 | Out-File $scenarioFilePathName
    }
}


# required tag
. Main

Handy way to create and test regex expressions is to use any online tool available. As an example, the first scenario item (^USD\.*.*CASH) has been applied to a given base market data. The last screenshot below shows all regex matches.

Thanks again for reading this blog.
-Mike


No comments:

Post a Comment