Build an AML Rule Engine
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)
- R1 large cash: The cash total that the same customer moved on the same day in the same direction (deposit
CASH_DEPor withdrawalCASH_WDR) is 10,000,000 won or more. Weight 1. - R2 suspected structuring: A customer has 3 or more cash transactions of at least 8,000,000 won and under 10,000,000 won. Weight 3.
- R3 high-risk country: A transfer whose
cp_countryis in thehigh_risk_countrytable. Weight 2. - R4 customer verification elapsed:
risk_gradeisHIGHorpepis 1, and 365 days or more have passed sincekyc_date. Weight 1. - R5 activity after dormancy: The first transaction day in August is 180 days or more after
last_activity, and the amount of that first transaction is 5,000,000 won or more. Weight 2. - Score = the sum of the weights of the rules triggered. The rule names are joined with
+in numerical order, likeR1+R3.
Steps
- Save the generator in the example on the right as
/root/bank/make_aml.pyand run it withpython3 make_aml.py.aml.dbwill contain 20 customers, 124 transactions, and 3 high-risk countries. - Write the R1 result in
/root/bank/ctr.csvwith the headercust_id,biz_date,direction,cash_total.directionisDEPorWDR. - Write the R2 result in
/root/bank/structuring.csvwith the headercust_id,txn_count,cash_total,first_date,last_date. - Write the R3 result in
/root/bank/highrisk.csvwith the headertxn_id,cust_id,type,cp_country,amount. - Write the R4 result in
/root/bank/kyc_due.csvwith the headercust_id,risk_grade,pep,kyc_date,days_since, and the R5 result in/root/bank/dormant.csvwith the headercust_id,last_activity,first_txn_date,dormant_days,amount. - Write the customers that triggered at least one rule in
/root/bank/alerts.csvwith the headercust_id,rules,score, in descending order of score (ascending bycust_idif equal). - Write the report draft for the highest-scoring customer in
/root/bank/str_<cust_id>.json. The keys arecust_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), andcustomer_notified(must befalse). - 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
- Taking only the date with
substr(ts, 1, 10)gives a day-level aggregation.julianday('2026-08-31') - julianday(kyc_date)is the number of days elapsed. - One SQL query per rule is enough. Step 6 is the work of combining five results, so Python is convenient.
- Common mistake 1: counting deposits and withdrawals together in R1. Count them separately by direction.
- Common mistake 2: taking R5's first transaction as the largest amount in all of August. It is the first transaction in August.
- Common mistake 3: writing a customer's real name or account number in the report. Not even
name_maskedis needed — the customer number is enough.
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.