Xact is a domain-specific programming language for creating fast, transparent, and maintainable actuarial/financial models.
Fast -- Calculations are optimized so that they can finish before your coffee is ready.
Transparent -- Model logic is completely clear and available to the user.
Maintainable -- User can conveniently make any changes they need to their models without unnecessary repetition.
Xact support exporting all model calculations into the xlsx file format with all Excel formulas populated. This allows user to easily examine model logic and communicate to all stakeholders.
Examples are usually the best way to learn, so we lead with examples for all of xact's major features. But first, let's install it if you haven't already.
You can install Xval (which includes Xact) from the install page at xval.io. On Visual Studio Code, the xval extension is recommended because it provides xact syntax highlighting.
After install, you can verify the installation by opening a terminal (e.g. command prompt) and typing "xval". You should see something like the following.
> xval
info: Usage: xval [command] [options]
Commands:
run Run an xact file
General Options:
-h, --help Print command-specific usage
error: command argument expected; not found
Xact cannot print text to a terminal, but we can write "Hello, World!" to our output.
table hello {
nrows: 1,
columns: {
hello <string>: "Hello, World!";
}
}
> xval run hello.xact
Now you should see hello.csv in the current directory.
Xact uses // for single-line comments and /**/ for multi-line comments.
table comments {
// nrows: 7,
nrows: 10,
columns: {
some_incredible_calc: t * 9001; // It's over 9000!
/*
Multi-line comments
let you easily include
an inline haiku.
*/
}
}
> xval run comments.xact
t,some_incredible_calc
0,0
1,9001
2,18002
...
9,81009
Comments will not be executed, but can be very useful -- especially for inlining haikus.
Xact code is organized into blocks declared at the top-level of a module.
An input block declares an expected tabular input, e.g. a csv file. Any columns you use from the input file must be declared in the input block. The input file is allowed to contain unused columns. Types should generally always be declared, but if the type is omitted, the default is float.
input mortality {
type: "csv";
columns: {
AGE <int>;
PROBABILITY <float>;
}
}
input extract {
type: "csv";
columns: {
POLNUM <string>;
ISSUE_DATE <date "%Y-%m-%d">;
'ISSUE AGE'n <int>;
"Is Qualified" <bool>;
OPEN_SEGMENT_YEARS <float>;
}
}
POLNUM,ISSUE_DATE,ISSUE AGE,Is Qualified,OPEN_SEGMENT_YEARS
A123,2025-01-15,22,TRUE,0.133699998
A456,2024-02-29,1,TRUE,3.140000105
A789,2025-05-27,47,FALSE,0.965699971
model/
├── inputs.xact
├── mortality.csv
└── extract.csv
If we run inputs.xact, it will import mortality.csv and extract.csv.
> xval run models/inputs.xact
Now we can use the inputs in a table.
The table is the fundamental unit of Xact. Each table will produce one output sheet. Using the extract from inputs, we can do tranformations in a table.
// ... code from inputs.xact ...
table policy_metadata {
nrows: extract;
columns: {
polnum <string> : extract.POLNUM[t];
issue_year <int> : $year(extract.ISSUE_DATE[t]);
issue_month <int> : $month(extract.ISSUE_DATE[t]);
issue_day <int> : $day(extract.ISSUE_DATE[t]);
issue_date_reconstructed <date> : $date(
self.issue_year[t], self.issue_month[t], self.issue_day[t]
);
open_segment_full_years <float> : $floor(extract.OPEN_SEGMENT_YEARS[t]);
}
}
Here, we set nrows to extract, telling xact that the table will have one row for each row of extract. The nrows keyword accepts an integer or the name of an input.
The index of each row is denoted by t. So, we can refer to the row in the extract that corresponds to each table row.
> xval run table.xact
t,polnum,issue_year,issue_month,issue_day,issue_date_reconstructed,open_segment_full_years
0,A123,2025,1,15,2025-01-15,0
1,A456,2024,2,29,2024-02-29,3
2,A789,2025,5,27,2025-05-27,0
But what if we need a whole table for each policy, instead of just a single row. For example, we might want a projection for each policy. That's where we would use a vtable.
When you use a vtable, xact creates one table per row of the caller. Vtables will not be calculated by themselves. They must be called by a table.
// ... code from table.xact ...
vtable projection {
nrows: 24; // years
columns: {
projection_date <date>: $date(
policy_metadata.issue_year[t1] + t,
policy_metadata.issue_month[t1],
policy_metadata.issue_day[t1]
);
open_segment_years <float>: $if(
extract.OPEN_SEGMENT_YEARS[t1] - t > 1,
extract.OPEN_SEGMENT_YEARS[t1] - t,
extract.OPEN_SEGMENT_YEARS[t1] - policy_metadata.open_segment_full_years[t1]
);
age <int>: extract."ISSUE AGE"[t1] + t;
annual_mort <float>: $lookup(
mortality.PROBABILITY[:],
mortality.AGE[:] == self.age[t]
);
}
}
table summary {
nrows: extract;
vtable: { source: projection; }
columns: {
polnum <string>: extract.POLNUM[t];
is_qualified <bool>: extract.'Is Qualified'n[t];
all_the_ages <int>: $sum(summary.projection.age[:, t]);
all_the_mort <float>: $sum(summary.projection.annual_mort[:, t]);
a_single_mort <float>: summary.projection.annual_mort[13, t];
}
}
The value t1 here is similar to t. Except instead of refering to the row index in projection, it refers to the row index of the table or vtable that called projection. This lets projection refer to the corresponding row in policy_metadata and extract.
> xval run vtable.xact
t,t1,projection_date,open_segment_years,age,annual_mort
0,0,2025-01-15,0.1337,22,0.000473
1,0,2026-01-15,0.1337,23,0.000513
... more rows ...
22,0,2047-01-15,0.1337,44,0.001142
23,0,2048-01-15,0.1337,45,0.001219
0,1,2024-02-29,3.14,1,0.000401
1,1,2025-03-01,2.14,2,0.000275
... more rows ...
22,1,2046-03-01,0.1400001,23,0.000513
23,1,2047-03-01,0.1400001,24,0.000554
0,2,2025-05-27,0.9657,47,0.001454
1,2,2026-05-27,0.9657,48,0.001627
... more rows ...
22,2,2047-05-27,0.9657,69,0.010463
23,2,2048-05-27,0.9657,70,0.011357
Note that t1 goes from 0 to 2 because extract has 3 rows.
A vtable can be called by a table or a vtable, meaning that vtables can be nested. This would be useful, for example, if you wanted to do N policies * 1,000 scenarios * 360 projection months.
Now that we have background on the blocks available in xact, let's dive into parts that make up an expression.
Xact has 5 basic types: float, int, bool, date, and string. You declare types after a column name. If you omit the type The column will default to float. This is usually not a problem because xact supports standard type coercion. However if you need a column to be a string or a date, you must include the type.
my_marvelously_typed_column <date>: $date( 1984, 2, 1); // February 1st, 1984
Special functions and operators usually restrict allowed types, making it important to be explicit with what types you expect in each column.
String types are currently limited to length 255.
Literal values can be specified for each type as follows.
int_value <int>: 7;
float_value <float>: 7.0;
bool_value <bool>: true; // or false
date_value <date>: $date(2018, 7, 7);
string_value <string>: "My auspicious string"; // limited to length 255
Dates require using a function to initialize a date literal. Expressions will be cast into their column's type if possible, otherwise you will get an error. Therefore, as mentioned before, you must specify the type for date and string columns.
The identifiers t and tn where n is any integer (e.g. t1, t2, t7) are special symbols that work as 0-indexed row indexers. 0-indexed means they start from 0.
For example, t is the indexer for the current table. It's value is 0 in the first row, 1 in the second, and so on...
table t_example {
nrows: 5;
columns: {
double_my_index <int>: t * 2; // 0, 2, 4, 6, 8
}
}
tn is a bit more difficult to understand. It is only usable in a vtable that has a parent table at least n levels above it. It represents the index of the row in the parent n levels above it.
For example, t1 in the summary.projection vtable corresponds to the index of the policy being projected. This is because the table summary, 1 level above summary.projection, has one row for each row of extract. If it isn't clear, take a look at summary.projection and see if you can figure out why t1 is what it is in each row.
References allow an expression to refer to columns from itself, an input, or another table or vtable.
self_column: t + 3;
self_ref: self.self_column[t]; // 3, 6, 9, ...
input_or_table_ref: input_or_table.column_name[t]; // get the value in row t
vtable_ref: table_name.vtable_name.column_name[t, 7]; // get the value in row t for vtable index 7
array_ref: $sum(table_name.vtable_name.column_name[t, :]); // sum values from row t across all vtables
A reference to a column in the same table has the form self.column_name[ row_index ].
Here, row_index is usually just t, but it can be any expression.
Self-references do not support ranges.
A reference to a input column has the form input_name.column_name[ row_index ]
or input_name.column_name[:].
As before, row_index can be any expression. The ":" indicates a range.
A reference to a table or vtable
column has the form:
table_name.vtable_name.column_name[ row_index or :, vtable_index or :].
Since vtables can be nested, we can expand this ad-infinitum to:
t_name.vtn....vt1.column_name[ t_ndx or :, t1_ndx or :, ..., tn_ndx].
Note the inversion of the order of the tables and the indices.
A reference to a table column must have the same number of indices and/or ranges as it has tables. So for example:
t_name.vt2.vt1.column_name[ t_ndx, t1_ndx, t2_ndx].requires 3 indices.
Ranges allow selecting more than one element from a column. Currently, xact only supports full ranges. The range symbol ":" means "take all of the items along this dimension". You can only use ranges with built-in functions that support it, such as sum and lookup.
Xact supports the following operators:
table operators {
nrows: 4;
columns: {
plus <int>: t + 2; // 2, 3, 4, 5
minus <int>: t - 4; // -4, -3, -2, -1
times <int>: t * 3; // 0, 3, 6, 9
div <float>: t / 2; // 0, .5, 1, 1.5
unary_minus <int>: -t; // 0, -1, -2, -3
power <int>: t ^ 2; // 0, 1, 4, 9
parentheses <int>: t * ( t + 1); // 0, 2, 6, 12
// comparisons
greater_than <bool>: t > 1; // false, false, true, true
greater_than_eq <bool>: t >= 1; // false, true, true, true
equal <bool>: t == 1; // false, true, false, false
not_equal <bool>: t != 1; // true, false, true, true
less_than <bool>: t < 1; // true, false, false, false
less_than_eq <bool>: t <= 1; // true, true, false, false
}
}
Xact has the following built-in functions:
// ... code from operators.xact
table functions {
nrows: 1;
columns: {
date <date>: $date(2024, 2, 28); // Year, Month, Day
year <int>: $year( self.date[t] ); // 2024
month <int>: $month( self.date[t] ); // 2
day <int>: $day( self.date[t] ); // 28
floor <int>: $floor( 3.14 ); // 3
if <int>: $if( t == 1, 1, 0); // 0
lookup <int>: $lookup(
operators.plus[:],
operators.minus[:] == -2
); // 4
sum <int>: $sum(operators.plus[:]); // 14
}
}
Xact can export all model calculations into the xlsx file format with all formulas populated. Rehashing the complete example from Blocks:
input mortality {
type: "csv";
columns: {
AGE <int>;
PROBABILITY <float>;
}
}
input extract {
type: "csv";
columns: {
POLNUM <string>;
ISSUE_DATE <date "%Y-%m-%d">;
'ISSUE AGE'n <int>;
"Is Qualified" <bool>;
OPEN_SEGMENT_YEARS <float>;
}
}
table policy_metadata {
nrows: extract;
columns: {
polnum <string> : extract.POLNUM[t];
issue_year <int> : $year(extract.ISSUE_DATE[t]);
issue_month <int> : $month(extract.ISSUE_DATE[t]);
issue_day <int> : $day(extract.ISSUE_DATE[t]);
issue_date_reconstructed <date> : $date(
self.issue_year[t], self.issue_month[t], self.issue_day[t]
);
open_segment_full_years <float> : $floor(extract.OPEN_SEGMENT_YEARS[t]);
}
}
vtable projection {
nrows: 24; // years
columns: {
projection_date <date>: $date(
policy_metadata.issue_year[t1] + t,
policy_metadata.issue_month[t1],
policy_metadata.issue_day[t1]
);
open_segment_years <float>: $if(
extract.OPEN_SEGMENT_YEARS[t1] - t > 1,
extract.OPEN_SEGMENT_YEARS[t1] - t,
extract.OPEN_SEGMENT_YEARS[t1] - policy_metadata.open_segment_full_years[t1]
);
age <int>: extract."ISSUE AGE"[t1] + t;
annual_mort <float>: $lookup(
mortality.PROBABILITY[:],
mortality.AGE[:] == self.age[t]
);
}
}
table summary {
nrows: extract;
vtable: { source: projection; }
columns: {
polnum <string>: extract.POLNUM[t];
is_qualified <bool>: extract.'Is Qualified'n[t];
all_the_ages <int>: $sum(summary.projection.age[:, t]);
all_the_mort <float>: $sum(summary.projection.annual_mort[:, t]);
a_single_mort <float>: summary.projection.annual_mort[13, t];
}
}
If we have the file structure:
example/
├── example.xact
├── mortality.csv
└── extract.csv
And we run this command:
> xval run example/example.xact --audit
We will get the following:
example/
├── example.xact
├── mortality.csv
├── extract.csv
├── example.xlsx
└── example-audit.xlsx
The files example.xlsx and example-audit.xlsx will have near identical values (up to rounding differences) but the audit file will have all formulas populated.
Xval serves as the command line interface for running xact models. You can view all available commands in xval using the help option.
> xval --help
info: Usage: xval [command] [options]
Commands:
run Run an xact file
General Options:
-h, --help Print command-specific usage
You can run xact models from the command line using xval run. Use the help option to view all options.
> xval run -h
info: Usage: xval run [path] [options]
Arguments:
path Path to .xact file to run
Options:
-h, --help Print this help and exit
--audit Include .xlsx audit file with output
--input-dir [path] Add path where input files may be found;
may be specified multiple times
Output Options:
--write-all-vtables, Write all vtables to output directory
--wav
--write-all-inputs, Write all inputs to output directory
--wai
--out-name [name] Set name of the output file(s)
default is .xact file name
--out-dir [path] Set output directory to path;
default is current working directory
--out-ft [type] Set the output file type
xlsx (Default) Excel .xlsx format
csv Comma separated values
For example, the following would put all output into csv files and include the inputs with the output.
> xval run myfile.xact --wai --out-ft csv