699 lines
16 KiB
C#
699 lines
16 KiB
C#
/*
|
|
* dbMango
|
|
*
|
|
* Copyright 2025 Deutsche Bank AG
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
namespace Tests.Rms.Risk.Mango.Language;
|
|
|
|
[TestFixture]
|
|
public class JsonToScriptTests
|
|
{
|
|
private const string MatchStage =
|
|
"""
|
|
[{
|
|
"$match" : {
|
|
"$and" : [{
|
|
"$and" : [{
|
|
"COB" : { "$date" : "2025-04-25T00:00:00Z" }
|
|
}, {
|
|
"$or" : [{
|
|
"Department" : "FX Forwards Controlling"
|
|
}, {
|
|
"Department" : "FX Metals Controlling"
|
|
}, {
|
|
"Department" : "FX Options Controlling"
|
|
}]
|
|
}]
|
|
}]
|
|
}
|
|
}]
|
|
""";
|
|
private const string MatchScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
WHERE
|
|
COB == date( "2025-04-25T00:00:00Z" )
|
|
AND (Department == "FX Forwards Controlling"
|
|
OR Department == "FX Metals Controlling"
|
|
OR Department == "FX Options Controlling")
|
|
}
|
|
|
|
""";
|
|
|
|
private const string ProjectStage =
|
|
"""
|
|
[{
|
|
"$project" : {
|
|
"CurvePrefix" : 1,
|
|
"Data" : {
|
|
"$objectToArray" : "$RhoDetails"
|
|
}
|
|
}
|
|
}]
|
|
""";
|
|
private const string ProjectScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT
|
|
CurvePrefix,
|
|
objectToArray( RhoDetails ) AS Data
|
|
}
|
|
|
|
""";
|
|
|
|
private const string ProjectWithComplexVarNamedStage =
|
|
"""
|
|
[{
|
|
"$project" : {
|
|
"Data" : "$Very.Complex.Var"
|
|
}
|
|
}]
|
|
""";
|
|
private const string ProjectWithComplexVarNamedScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT
|
|
$Very.Complex.Var AS Data
|
|
}
|
|
|
|
""";
|
|
|
|
private const string ProjectWithComplexVarUnnamedStage =
|
|
"""
|
|
[{
|
|
"$project" : {
|
|
"$Very.Complex.Var" : 1
|
|
}
|
|
}]
|
|
""";
|
|
private const string ProjectWithComplexVarUnnamedScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT
|
|
$Very.Complex.Var
|
|
}
|
|
|
|
""";
|
|
|
|
private const string ProjectExcludeStage =
|
|
"""
|
|
[{
|
|
"$project" : {
|
|
"CurvePrefix" : 0,
|
|
"Data" : 0
|
|
}
|
|
}]
|
|
""";
|
|
private const string ProjectExcludeScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT EXCLUDE
|
|
CurvePrefix,
|
|
Data
|
|
}
|
|
|
|
""";
|
|
|
|
// https://www.mongodb.com/docs/manual/reference/operator/query/exists/
|
|
private const string MatchWithProjectionStage =
|
|
"""
|
|
[{
|
|
"$match" : {
|
|
"$or" : [{
|
|
"RhoDetails.RhoImpact" : {
|
|
"$exists" : true
|
|
}
|
|
}, {
|
|
"RhoDetails.Rho2NdOrderImpact" : {
|
|
"$exists" : true
|
|
}
|
|
}]
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string MatchWithProjectionScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
WHERE
|
|
$RhoDetails.RhoImpact EXISTS
|
|
OR $RhoDetails.Rho2NdOrderImpact EXISTS
|
|
}
|
|
|
|
""";
|
|
|
|
private const string ProjectArrayStage =
|
|
"""
|
|
[{
|
|
"$project" : {
|
|
"CurveKey" : {
|
|
"$concat" : ["$CurvePrefix", "$Data.k"]
|
|
},
|
|
"Ccy" : 1,
|
|
"Curve" : 1,
|
|
"FundingName" : 1,
|
|
"Tenor" : "$Data.k",
|
|
"Order" : 1,
|
|
"Data" : [{
|
|
"Type" : "$Type",
|
|
"Value" : "$Data.v"
|
|
}]
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string ProjectArrayScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT
|
|
concat( CurvePrefix, $Data.k ) AS CurveKey,
|
|
Ccy,
|
|
Curve,
|
|
FundingName,
|
|
$Data.k AS Tenor,
|
|
Order,
|
|
[
|
|
{
|
|
Type,
|
|
$Data.v AS Value
|
|
}
|
|
] AS Data
|
|
}
|
|
|
|
""";
|
|
|
|
private const string ProjectArrayComplexStage =
|
|
"""
|
|
[{
|
|
"$project" : {
|
|
"Tenor" : 1,
|
|
"Value" : {
|
|
"$arrayToObject" : {
|
|
"$map" : {
|
|
"input" : "$Data",
|
|
"as" : "d",
|
|
"in" : ["$$d.Type", "$$d.Value"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string ProjectArrayComplexScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT
|
|
Tenor,
|
|
arrayToObject( map(
|
|
input: Data,
|
|
as: "d",
|
|
in: [
|
|
"$$d.Type",
|
|
"$$d.Value"
|
|
]
|
|
) ) AS Value
|
|
}
|
|
|
|
""";
|
|
|
|
private const string ProjectWithNamedArgsStage =
|
|
"""
|
|
[{
|
|
"$project" : {
|
|
"Value" : {
|
|
"$map" : {
|
|
"input" : "$Data",
|
|
"as" : "d",
|
|
"in" : ["$$d.Type", "$$d.Value"]
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string ProjectWithNamedArgsScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT
|
|
map(
|
|
input: Data,
|
|
as: "d",
|
|
in: [
|
|
"$$d.Type",
|
|
"$$d.Value"
|
|
]
|
|
) AS Value
|
|
}
|
|
|
|
""";
|
|
|
|
private const string ProjectNestedFuncStage =
|
|
"""
|
|
[ {
|
|
"$project" : {
|
|
"Value" : {
|
|
"$arrayToObject" : {
|
|
"$map" : {
|
|
"input" : "$Data",
|
|
"as" : "d",
|
|
"in" : ["$$d.Type", "$$d.Value"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string ProjectNestedFuncScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT
|
|
arrayToObject( map(
|
|
input: Data,
|
|
as: "d",
|
|
in: [
|
|
"$$d.Type",
|
|
"$$d.Value"
|
|
]
|
|
) ) AS Value
|
|
}
|
|
|
|
""";
|
|
|
|
private const string GroupStage =
|
|
"""
|
|
[{
|
|
"$group" : {
|
|
"_id" : {
|
|
"CurveKey" : "$CurveKey",
|
|
"Ccy" : "$Ccy",
|
|
"Curve" : "$Curve",
|
|
"FundingName" : "$FundingName",
|
|
"Tenor" : "$Tenor"
|
|
},
|
|
"Order" : {
|
|
"$max" : "$Order"
|
|
},
|
|
"Rho2ndOrderImpact" : {
|
|
"$sum" : "$Value.Rho2ndOrderImpact"
|
|
}
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string GroupScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
GROUP BY
|
|
CurveKey,
|
|
Ccy,
|
|
Curve,
|
|
FundingName,
|
|
Tenor
|
|
LET
|
|
max( Order ) AS Order,
|
|
sum( $Value.Rho2ndOrderImpact ) AS Rho2ndOrderImpact
|
|
}
|
|
|
|
""";
|
|
|
|
private const string MatchUnaryExpressionsStage =
|
|
"""
|
|
[{
|
|
"$match" : {
|
|
"$or" : [{
|
|
"RhoImpact" : {
|
|
"$gt" : 1E-08
|
|
}
|
|
}]
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string MatchUnaryExpressionsScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
WHERE
|
|
RhoImpact > 1E-08
|
|
}
|
|
|
|
""";
|
|
|
|
private const string LookupStage =
|
|
"""
|
|
[{
|
|
"$lookup" : {
|
|
"from" : "PnL-Market",
|
|
"localField" : "_id.CurveKey",
|
|
"foreignField" : "_id",
|
|
"as" : "Market"
|
|
}
|
|
}]
|
|
""";
|
|
private const string LookupScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
JOIN "PnL-Market" AS Market ON
|
|
$_id.CurveKey == _id
|
|
}
|
|
|
|
""";
|
|
|
|
private const string ProjectWithIdStage =
|
|
"""
|
|
[{
|
|
"$project" : {
|
|
"_id" : {
|
|
"Ccy" : 1,
|
|
"Curve" : 1
|
|
},
|
|
"OpeningCurve" : "$OpeningCurve"
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string ProjectWithIdScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT ID {
|
|
Ccy,
|
|
Curve
|
|
}
|
|
OpeningCurve
|
|
}
|
|
|
|
""";
|
|
|
|
private const string AddFieldsStage =
|
|
"""
|
|
[{
|
|
"$addFields" : {
|
|
"_id.Order" : "$Order",
|
|
"_id.Data" : [{
|
|
"k" : "OpeningCurve",
|
|
"v" : "$Market.Opening"
|
|
}, {
|
|
"k" : "CurveMove",
|
|
"v" : "$Market.Move"
|
|
}, {
|
|
"k" : "Value",
|
|
"v" : "$Value"
|
|
}]
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string AddFieldsScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
ADD
|
|
Order AS "_id.Order",
|
|
[
|
|
{
|
|
"OpeningCurve" AS "k",
|
|
$Market.Opening AS "v"
|
|
},
|
|
{
|
|
"CurveMove" AS "k",
|
|
$Market.Move AS "v"
|
|
},
|
|
{
|
|
"Value" AS "k",
|
|
Value AS "v"
|
|
}
|
|
] AS "_id.Data"
|
|
}
|
|
|
|
""";
|
|
|
|
private const string SubtractStage =
|
|
"""
|
|
[{
|
|
"$project" : {
|
|
"RhoMove" : {
|
|
"$subtract" : ["$ClosingRho", "$OpeningRho"]
|
|
}
|
|
}
|
|
}]
|
|
""";
|
|
|
|
private const string SubtractScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
PROJECT
|
|
ClosingRho - OpeningRho AS RhoMove
|
|
}
|
|
|
|
""";
|
|
|
|
private const string BucketStage =
|
|
"""
|
|
[
|
|
{
|
|
"$bucket": {
|
|
"groupBy": {
|
|
"$divide": [
|
|
"$Field1",
|
|
100.1
|
|
]
|
|
},
|
|
"boundaries": [
|
|
1,
|
|
10,
|
|
100,
|
|
1000
|
|
],
|
|
"default": "Ignored",
|
|
"output": {
|
|
"Gain": {
|
|
"$divide": [
|
|
"$Field1",
|
|
100.1
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
""";
|
|
|
|
private const string BucketScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
BUCKET
|
|
Field1 / 100.1
|
|
BOUNDARIES 1, 10, 100, 1000
|
|
DEFAULT "Ignored"
|
|
LET
|
|
Field1 / 100.1 AS Gain
|
|
}
|
|
|
|
""";
|
|
|
|
private const string BucketAutoStage =
|
|
"""
|
|
[
|
|
{
|
|
"$bucketAuto": {
|
|
"groupBy": {
|
|
"$divide": [
|
|
"$Field1",
|
|
100.1
|
|
]
|
|
},
|
|
"buckets": 5,
|
|
"granularity": "POWERSOF2",
|
|
"output": {
|
|
"Gain": {
|
|
"$divide": [
|
|
"$Field1",
|
|
100.1
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
""";
|
|
|
|
private const string BucketAutoScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
BUCKET AUTO
|
|
Field1 / 100.1
|
|
BUCKETS 5
|
|
GRANULARITY "POWERSOF2"
|
|
LET
|
|
Field1 / 100.1 AS Gain
|
|
}
|
|
|
|
""";
|
|
|
|
private const string FacetStage =
|
|
"""
|
|
[
|
|
{
|
|
"$facet": {
|
|
"categorizedByTags": [
|
|
{ "$unwind": "$tags" },
|
|
{ "$sortByCount": "$tags" }
|
|
],
|
|
"categorizedByPrice": [
|
|
{ "$match": { "price": { "$exists": 1 } } },
|
|
{
|
|
"$bucket": {
|
|
"groupBy": "$price",
|
|
"boundaries": [ 0, 150, 200, 300, 400 ],
|
|
"default": "Other",
|
|
"output": {
|
|
"count": { "$sum": 1 },
|
|
"titles": { "$push": "$title" }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"categorizedByYears(Auto)": [
|
|
{
|
|
"$bucketAuto": {
|
|
"groupBy": "$year",
|
|
"buckets": 4
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
""";
|
|
|
|
private const string FacetScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
FACET
|
|
categorizedByTags PIPELINE {
|
|
UNWIND tags
|
|
DO
|
|
{
|
|
"$sortByCount": "$tags"
|
|
}
|
|
},
|
|
categorizedByPrice PIPELINE {
|
|
WHERE
|
|
price == exists( 1 )
|
|
BUCKET
|
|
price
|
|
BOUNDARIES 0, 150, 200, 300, 400
|
|
DEFAULT "Other"
|
|
LET
|
|
sum( 1 ) AS count,
|
|
push( title ) AS titles
|
|
},
|
|
'categorizedByYears(Auto)' PIPELINE {
|
|
BUCKET AUTO
|
|
year
|
|
BUCKETS 4
|
|
}
|
|
}
|
|
|
|
""";
|
|
|
|
private const string MatchHackExistsStage =
|
|
"""
|
|
[{
|
|
"$match": {
|
|
"$or": [
|
|
{ "RhoDetails.RhoImpact": { "$gt": {} } },
|
|
{ "RhoDetails.Rho2NdOrderImpact": { "$gt": {} } }
|
|
]
|
|
}
|
|
}]
|
|
""";
|
|
private const string MatchHackExistsScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
WHERE
|
|
$RhoDetails.RhoImpact == exists( true )
|
|
OR $RhoDetails.Rho2NdOrderImpact == exists( true )
|
|
}
|
|
|
|
""";
|
|
|
|
private const string MatchExtraFilterStage =
|
|
"""
|
|
[{
|
|
"$match": {
|
|
"$and" : [
|
|
{ "COB" : { "$eq" : { "$date" : "2025-06-02T00:00:00Z" } } },
|
|
{ "$or" : [
|
|
{ "Department" : { "$eq" : "CPM" } },
|
|
{ "Department" : { "$eq" : "FX Metals Controlling" } }
|
|
]}
|
|
]}
|
|
}]
|
|
""";
|
|
private const string MatchExtraFilterScript =
|
|
"""
|
|
FROM "TestCollection" PIPELINE {
|
|
WHERE
|
|
COB == date( "2025-06-02T00:00:00Z" )
|
|
AND (Department == "CPM"
|
|
OR Department == "FX Metals Controlling")
|
|
}
|
|
|
|
""";
|
|
|
|
private static void TestPipeline(string pipeline, string expectedScript)
|
|
{
|
|
var ast = LanguageParser.ParseAggregationJsonToAST("TestCollection", pipeline);
|
|
var text = ast.AsText();
|
|
|
|
Assert.Multiple( () =>
|
|
{
|
|
Assert.That( ast, Is.Not.Null );
|
|
Assert.That( ast.Pipeline, Is.Not.Null );
|
|
|
|
Assert.That( text.Replace("\r", ""), Is.EqualTo(expectedScript.Replace("\r", "")) );
|
|
});
|
|
|
|
}
|
|
|
|
|
|
[Test] public void FxVegaBreakdownByTenor() => TestPipeline(Data.FxVegaBreakdownByTenor.Pipeline, Data.FxVegaBreakdownByTenor.Script);
|
|
[Test] public void RhoBreakdownByTenor() => TestPipeline(Data.RhoBreakdownByTenor.Pipeline, Data.RhoBreakdownByTenor.Script);
|
|
[Test] public void Match() => TestPipeline(MatchStage, MatchScript);
|
|
[Test] public void MatchWithProjection() => TestPipeline(MatchWithProjectionStage, MatchWithProjectionScript);
|
|
[Test] public void Project() => TestPipeline(ProjectStage, ProjectScript);
|
|
[Test] public void ProjectWithComplexVarNamed() => TestPipeline(ProjectWithComplexVarNamedStage, ProjectWithComplexVarNamedScript);
|
|
[Test] public void ProjectWithComplexVarUnnamed() => TestPipeline(ProjectWithComplexVarUnnamedStage, ProjectWithComplexVarUnnamedScript);
|
|
[Test] public void ProjectExclude() => TestPipeline(ProjectExcludeStage, ProjectExcludeScript);
|
|
[Test] public void ProjectArray() => TestPipeline(ProjectArrayStage, ProjectArrayScript);
|
|
[Test] public void ProjectArrayComplex() => TestPipeline(ProjectArrayComplexStage, ProjectArrayComplexScript);
|
|
[Test] public void ProjectWithNamedArgs() => TestPipeline(ProjectWithNamedArgsStage, ProjectWithNamedArgsScript);
|
|
[Test] public void ProjectNestedFunc() => TestPipeline(ProjectNestedFuncStage, ProjectNestedFuncScript);
|
|
[Test] public void Group() => TestPipeline(GroupStage, GroupScript);
|
|
[Test] public void MatchUnaryExpressions() => TestPipeline(MatchUnaryExpressionsStage, MatchUnaryExpressionsScript);
|
|
[Test] public void Lookup() => TestPipeline(LookupStage, LookupScript);
|
|
[Test] public void ProjectWithId() => TestPipeline(ProjectWithIdStage, ProjectWithIdScript);
|
|
[Test] public void AddFields() => TestPipeline(AddFieldsStage, AddFieldsScript);
|
|
[Test] public void Subtract() => TestPipeline(SubtractStage, SubtractScript);
|
|
[Test] public void Bucket() => TestPipeline(BucketStage, BucketScript);
|
|
[Test] public void BucketAuto() => TestPipeline(BucketAutoStage, BucketAutoScript);
|
|
[Test] public void Facet() => TestPipeline(FacetStage, FacetScript);
|
|
[Test] public void MatchHackExists() => TestPipeline(MatchHackExistsStage, MatchHackExistsScript);
|
|
[Test] public void MatchExtraFilter() => TestPipeline(MatchExtraFilterStage, MatchExtraFilterScript);
|
|
|
|
}
|