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 supports 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
This means Xval is installed and you are ready to roll.
You can also use Xact through the xval python sdk.
> pip install xval
See the GitHub repo and the the pypi page for usage.
We start with the time-honored "Hello, World!" program.
config {
out_ft: "term";
}
table hello {
nrows: 1;
columns: {
hello <string>: "Hello, World!";
}
}
> xval run hello.xact
hello columns 0 through 1
| t | hello |
| 0 | Hello, World! |
We can also do "Hello, World!" with python. This requires python to be installed on your machine.
config {
out_ft: "term";
}
python hello {
python_path: "python";
```print("Hello, World!")```
}
> xval run hellopy.xact
info: Running hellopy.xact
info: Starting python block 'hello'
info: Hello, World!
info: Finished python block 'hello'
info: Completed run of hellopy.xact.
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.
The config block declares module-level options that override the usual defaults. config's attributes correspond to the options available via xval run. The config block can be used only once and must occur at the very beginning of the xact file.
config {
audit: true; // default false
hardcode: "table_or_vtable_name"; // hardcode this table/vtable in audit
hardcode: "other_vtable_name"; // can be used multiple times
timestamp: true; // default false
write_all_vtables: true; // default false
write_all_inputs: true; // default false
out_name: "different filename"; // default is name of .xact file
out_dir: "out"; // default is current directory
out_ft: "xlsx"; // default is csv
}
Options specified by the command line or python sdk will override config attributes.
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">: "2024-02-29"; // "2024-02-29" is the default value.
// defaults must be specified as string, and will be used for any missing values.
'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,,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 model/inputs.xact
Xact searches in two folders for inputs in the following priority order:
As long as an input is found in either of those two folders, Xact will load it, prioritizing the current working directory. Otherwise it will raise an error.
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.
Xact supports running python scripts inline or by file path relative to the xact script. This is distinct from the python sdk . This allows xact to run a python program. The python sdk allows python to run an xact program. You could use these in tandem to produce an infinite loop. But that would likely be unhelpful.
Nonetheless, in order to use a python block, you must have python installed on your machine. Xact defaults to using the python installation found by typing "python" into a terminal. If you have python installed in a different location or would like to use a virtual environment, you can specify the python_path.
python inline {
python_path: "py";
```
print("Hello, Monty!")
```
}
python src {
python_path: "path/to/my/interpreter/python.exe";
source: "./snakes.py"; //relative to .xact file
}
The source attribute specifies a path to a python script relative to the xact file. The python script, whether inline or sourced, will then be executed from the current working directory.
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 denote the 0-indexed row number. 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
ceiling <int>: $ceiling( 2.3 ); // 3
if <int>: $if( t == 1, 1, 0); // 0
lookup <int>: $lookup(
operators.plus[:],
operators.minus[:] == -2
); // 4
sum <int>: $sum(operators.plus[:]); // 14
days360 <int>: $days360( $date(2024, 2, 28), $date(2024, 3, 28) ); // 30
min <int>: $min(2, 7); // 2
max <int>: $max(2, 7); // 7
mod <int>: $mod(26, 12); // 2
or <bool>: $or(true, false); // true
min_range <int>: $min_range(operators.plus[:]); // 2
max_range <int>: $max_range(operators.plus[:]); // 5
rows <int>: $rows(operators.plus[:]); // 4
sumifs <float>: $sumifs(operators.plus[:], operators.not_equal[:]); // 11
eomonth <date>: $eomonth(self.date[t], 1); // 2024-03-31
one_day <date>: self.eomonth[t] + $one_day(); // 2024-04-01 -- equals 1 in Excel or 86400 in compute, used to make
// audit and compute date addition/subtraction consistent
}
}
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.xact --audit --out-ft xlsx
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
template Get an example template locally
license Print the license text for xval
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 Options:
--audit Include .xlsx audit file with output
--hardcode <name> Hardcode table <name> in audit output;
--hc <name> can be used multiple times
Output Options:
--timestamp Include the timestamp in the output
--ts file name(s); default is false
--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 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>
csv (Default) Comma separated values
xlsx Excel .xlsx format
term Terminal, for debugging
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
Xval provides built-in model templates that you can use as a starting point.
> xval run -h
info: Usage: xval template <name> [path] [options]
Arguments:
name Name of template;
use --list for available templates
path Output directory for template;
default is "./{name}"
Options:
-h, --help Print this help and exit
--list List available templates and exit
For example, the following would put the FDA Stat AG33 template into the current working directory.
> xval template fda_stat_ag33