TT Lab
Blog Get started
Learn Learning paths Courses

The Language of Banking

Build an AML Rule Engine

Continue in TT Lab

Goal

You implement five suspicious transaction detection rules with SQL and Python to produce alert scores per customer, and write the suspicious transaction report draft for the highest-scoring customer from facts only.

Why it matters

Anti-money-laundering (AML) is the regulatory work that bank IT meets most often. The law puts two reporting duties on banks. The currency transaction report (CTR), which is filed on its own when cash above a certain amount moves in a day, and the suspicious transaction report (STR), which is filed when money laundering is suspected regardless of amount. A CTR is mechanical and an STR is judgment. What helps that judgment is a rule engine.

A rule engine is not a fancy model. It is a bundle of rules such as "does the day's cash total exceed the threshold," "was it split many times into amounts just under the threshold," "did it transact with a high-risk country," "is the customer verification old," and "is an account that had been inactive for a long time suddenly moving a large sum." A single rule has many false positives, but when several rules overlap on one person, it becomes a signal. So each rule gets a weight and they are summed.

Two constraints come with this work. The report contains only facts — the transaction numbers, amounts, dates, and the rules that were triggered. "It looks suspicious" is a basis, not a conclusion. And you do not notify the customer that a report was made. This is a duty of confidentiality set by law, and the system must be designed so that the fact does not leak onto channel screens or customer notifications.

Rules of this lab (reference date 2026-08-31)

Steps

  1. Save the generator in the example on the right as /root/bank/make_aml.py and run it with python3 make_aml.py. aml.db will contain 20 customers, 124 transactions, and 3 high-risk countries.
  2. Write the R1 result in /root/bank/ctr.csv with the header cust_id,biz_date,direction,cash_total. direction is DEP or WDR.
  3. Write the R2 result in /root/bank/structuring.csv with the header cust_id,txn_count,cash_total,first_date,last_date.
  4. Write the R3 result in /root/bank/highrisk.csv with the header txn_id,cust_id,type,cp_country,amount.
  5. Write the R4 result in /root/bank/kyc_due.csv with the header cust_id,risk_grade,pep,kyc_date,days_since, and the R5 result in /root/bank/dormant.csv with the header cust_id,last_activity,first_txn_date,dormant_days,amount.
  6. Write the customers that triggered at least one rule in /root/bank/alerts.csv with the header cust_id,rules,score, in descending order of score (ascending by cust_id if equal).
  7. Write the report draft for the highest-scoring customer in /root/bank/str_<cust_id>.json. The keys are cust_id, rules (a list), score, txn_ids (the list of transaction numbers that triggered rules for that customer), total_amount (the sum of the amounts of those transactions), narrative (at least 100 characters, facts only), prepared_by (a role, not a person's name), and customer_notified (must be false).
  8. In /root/bank/aml_report.md, write a report in four sections: ## Detection rules and results, ## Reporting targets, ## False positives and limitations, and ## Confidentiality and personal information.

Notes

Create the customer and transaction data

Save the generator in the example on the right as /root/bank/make_aml.py and run it with python3 make_aml.py. aml.db will contain 20 customers, 124 transactions, and 3 high-risk countries.

Save the generator in the lab instructions as /root/bank/make_aml.py and run it with python3. If you edit it by hand, all the numbers in the later steps go off.

R1 large cash — count by the day's total

Write the R1 result in /root/bank/ctr.csv with the header cust_id,biz_date,direction,cash_total. direction is DEP or WDR.

Pick only the cash types, GROUP BY cust_id, substr(ts,1,10), and direction, and apply HAVING SUM(amount) >= 10000000. Sum deposits and withdrawals separately, and include days whose total is exactly 10 million won. It is the day's total, not a single transaction.

R2 suspected structuring — count just below the threshold

Write the R2 result in /root/bank/structuring.csv with the header cust_id,txn_count,cash_total,first_date,last_date.

Group by cust_id the cases that are cash types and whose amount is at least 8,000,000 and under 10,000,000, and keep only people with COUNT(*) >= 3. Get the dates with MIN/MAX.

R3 high-risk country — check against the list

Write the R3 result in /root/bank/highrisk.csv with the header txn_id,cust_id,type,cp_country,amount.

An INNER JOIN of txn and high_risk_country on cp_country is all there is to it. There is no amount condition. The 20 million won that went out to AE must not be caught.

R4 and R5 — customer verification elapsed and activity after dormancy

Write the R4 result in /root/bank/kyc_due.csv with the header cust_id,risk_grade,pep,kyc_date,days_since, and the R5 result in /root/bank/dormant.csv with the header cust_id,last_activity,first_txn_date,dormant_days,amount.

R4 can be done with customer alone: (risk_grade='HIGH' OR pep=1) AND julianday('2026-08-31')-julianday(kyc_date) >= 365. For R5, first get each customer's MIN(ts) (a CTE) and join that transaction back. A person who was dormant for a long time but with a small amount must not be caught.

Combine the rules to produce alert scores

Write the customers that triggered at least one rule in /root/bank/alerts.csv with the header cust_id,rules,score, in descending order of score (ascending by cust_id if equal).

Collect the cust_id values from the five CSVs of steps 2–5 to build each customer's set of rules, and add the weights (R1 1 · R2 3 · R3 2 · R4 1 · R5 2). Join rules with + in numerical order, and sort by score descending and cust_id ascending.

Write the suspicious transaction report draft from facts only

Write the report draft for the highest-scoring customer in /root/bank/str_<cust_id>.json. The keys are cust_id, rules (a list), score, txn_ids (the list of transaction numbers that triggered rules for that customer), total_amount (the sum of the amounts of those transactions), narrative (at least 100 characters, facts only), prepared_by (a role, not a person's name), and customer_notified (must be false).

The first line of alerts.csv is the target. For each rule that customer triggered, collect the matching transactions (for R1 that day's cash, for R2 cash of 8–10 million won, for R3 high-risk country transactions, for R5 the first transaction in August), remove duplicates, and sort. Do not include transactions that triggered no rule. Write with json.dump, and customer_notified is Python False.

Write the AML inspection report

In /root/bank/aml_report.md, write a report in four sections: ## Detection rules and results, ## Reporting targets, ## False positives and limitations, and ## Confidentiality and personal information.

Use one line starting with ## for each of the four section titles in the task (the English titles in backticks, or titles with the same meaning in Korean, Japanese, or Chinese). In the detection rules section, write the number of alerted customers with a counting word, like "N customers"; in the reporting targets section, the customer number with the highest score; and in the confidentiality section, a sentence saying the customer is not told about the report (no tipping-off — for example, the customer is not notified). Do not write real names.