Case 19: Investor suitability assessment
This case corresponds to USE-CASES scenario 19, “Investor risk-level suitability assessment,” in the financial compliance domain. It targets investor appropriateness management at banks, securities firms, and other wealth-management sales channels, automatically completing client maturity assessment, product risk-level matching, and purchase-allowed / dual-recording-required / purchase-banned decisions.
Industry scenario
Section titled “Industry scenario”Under the Measures for the Administration of Suitability of Securities and Futures Investors, financial institutions must complete a suitability match before selling wealth-management products to a client. Client data includes age, annual income, household financial net assets, years of investment experience, risk-tolerance questionnaire rank (C1 conservative through C5 aggressive), proposed product risk rank (R1 low risk through R5 high risk), product name, and risk-disclosure letter signing status. The system must provide a fully traceable match conclusion the instant the client places an order, avoiding compliance risks from mismatched sales.
The decision has two layers: pre-check and matching. The Service first applies two hard blocks: a client under 18 lacks civil capacity to purchase financial products; a client who has not signed the product’s risk-disclosure letter must complete signing and the risk assessment first. After passing, the rule script performs five-dimension maturity scoring: age (0-15, ages 25-50 are the most stable investment career), income (0-15), assets (0-20), investment experience (0-20), and risk-tolerance questionnaire (0-30), totaling 100 points.
The match conclusion has three states. When the product risk rank exceeds the client’s tolerance rank (R rank greater than C rank, positive gap), it is a suitability hard block (BLOCK) — sales are prohibited under the regulatory measures; this policy lives on the Java side and does not change with the scoring script. When the ranks match but the client is aged 65 or older purchasing R3 or above medium-high-risk products, or the maturity score is below 50 purchasing R3 or above, the conclusion is CONFIRM and must undergo dual recording (audio and video recording) at the branch — elderly clients must be accompanied by an adult direct relative for confirmation, low-maturity clients must hand-copy a special risk warning and pass a 24-hour cooling-off period before a follow-up confirmation. All other cases are ALLOW with direct clearance. The decision result also backfills the rank gap, the highest product risk rank the client may purchase, and every block/restriction reason, directly serving the sales page and compliance audit trail.
Business pain points
Section titled “Business pain points”- Regulatory red lines and scoring tuning must be layered: sales above rank (R rank higher than C rank) is a statutory hard block and cannot be loosened even if the maturity score is high; the maturity-questionnaire criteria need continuous tuning, and mixing the two types of logic easily causes accidental loosening.
- Questionnaire and assessment criteria are scattered across channels: the five-dimension brackets for age, income, assets, experience, and questionnaire are hard-coded in mobile banking, counter, and other systems, and inconsistent criteria cause the same client to get different conclusions across channels.
- Dual-recording trigger rules are complexly combined: elderly clients buying medium-high-risk products and low-maturity clients buying medium-high-risk products coexist, with different follow-up scripts (relative accompaniment vs. hand-copying + cooling-off), and manual execution easily misses the judgment or uses the wrong script.
- High compliance traceability requirements: every transaction must be traceable back to the client rank, product rank, rank gap, block reason, and score breakdown; hard-coded decisions are hard to output as structured evidence.
- Pre-conditions such as disclosure-letter signing and civil capacity are easily missed: if the score is computed before checking signing status, unsigned clients would also see scores and product information, making the process non-compliant.
- Legacy wealth-management sales systems still run on JDK 8, and domestic-platform environments cannot adopt heavy compliance rule platforms: a lightweight rule capability with no standalone middleware and capable of embedding into the sales-transaction chain is needed.
Solution architecture
Section titled “Solution architecture”This case follows the six-piece skeleton: input model + decision result model + rule interface + XML rule script + Service orchestration + 6 test cases covering real business profiles.
| Layer | Responsibility | Files |
|---|---|---|
| Domain model | Client assessment and proposed-product fields, suitability-match decision result (three-state conclusion / rank gap / dual recording / reasons) | InvestorAssessment.java, SuitabilityDecision.java |
| Rule set & XML | Five-dimension maturity scoring, dual-recording conditions, risk-rank gap | InvestorSuitabilityRules.java, investor-suitability.xml |
| Orchestration service | Age/signing pre-check block, above-rank hard block, dual-recording routing and reason assembly | InvestorSuitabilityService.java |
| Tests | 6 client profiles (HNW / conservative depositor / above-rank / elderly / unsigned / low maturity) | InvestorSuitabilityServiceTest.java |
Rule method breakdown
Section titled “Rule method breakdown”| Rule method | Criteria | Score/output |
|---|---|---|
scoreAge |
Ages 25-50 -> 15, 51-60 -> 12, 18-24 -> 8, 61-64 -> 6, otherwise 3 | 0-15 |
scoreIncome |
Annual income >=500K -> 15, >=200K -> 12, >=100K -> 8, >=50K -> 4, otherwise 2 | 0-15 |
scoreAssets |
Household financial net assets >=3M -> 20, >=1M -> 15, >=300K -> 10, >=100K -> 5, otherwise 2 | 0-20 |
scoreExperience |
Investment experience >=10 years -> 20, >=5 years -> 15, >=3 years -> 10, >=1 year -> 5, otherwise 0 | 0-20 |
scoreTolerance |
Risk-tolerance rank C5/C4/C3/C2/others -> 30/24/18/10/5 | 0-30 |
needConfirm |
Maturity below 50 purchasing R3 or above, or elderly client purchasing R3 or above, returns true | boolean |
rankGap |
Product risk rank - client tolerance rank (positive means the product exceeds tolerance) | Integer rank gap |
Code implementation (complete & runnable)
Section titled “Code implementation (complete & runnable)”Place the following files into the test directory of a JQuick-Java project following the source package structure: the two models go to src/test/java/com/github/paohaijiao/demo/model/, the rule interface and test class go to src/test/java/com/github/paohaijiao/demo/, the Service goes to src/test/java/com/github/paohaijiao/demo/service/, and the XML script goes to src/test/resources/demo/. Keep the package names exactly as in the source (com.github.paohaijiao.demo / .model / .service). Then run mvn test -Dtest=InvestorSuitabilityServiceTest to execute all 6 cases.
InvestorAssessment.java
Section titled “InvestorAssessment.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 投资者风险承受能力测评与拟购产品要素(投资者适当性管理) */@Data@NoArgsConstructor@AllArgsConstructorpublic class InvestorAssessment {
/** * 客户号 */ private String investorId;
/** * 客户姓名 */ private String investorName;
/** * 年龄 */ private int age;
/** * 年收入(元) */ private double annualIncome;
/** * 家庭金融净资产(元) */ private double netAssets;
/** * 投资经验年限 */ private int investExperienceYears;
/** * 风险承受能力等级(1-5,对应 C1 保守型至 C5 激进型) */ private int riskToleranceRank;
/** * 拟购产品风险等级(1-5,对应 R1 低风险至 R5 高风险) */ private int productRiskRank;
/** * 拟购产品名称 */ private String productName;
/** * 是否已签署产品风险揭示书 */ private boolean signedRiskNotice;}SuitabilityDecision.java
Section titled “SuitabilityDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 投资者适当性匹配决策结果 */@Datapublic class SuitabilityDecision {
/** * 客户号 */ private String investorId;
/** * 匹配结论:ALLOW-可购买 / CONFIRM-确认后购买(双录+特别风险揭示) / BLOCK-禁止购买 */ private String decision;
/** * 投资者成熟度评分(0-100) */ private int maturityScore;
/** * 风险承受等级(1-5) */ private int riskToleranceRank;
/** * 产品风险等级(1-5) */ private int productRiskRank;
/** * 风险等级差(产品等级 - 承受等级,正数表示产品超出承受能力) */ private int rankGap;
/** * 该客户允许购买的最高产品风险等级 */ private int maxAllowedRank;
/** * 是否必须双录(录音录像)并手抄风险提示 */ private boolean doubleRecordingRequired;
/** * 阻断/限制原因 */ private List<String> reasons = new ArrayList<String>();
/** * 评分明细 */ private List<String> scoreDetails = new ArrayList<String>();
public void addReason(String reason) { this.reasons.add(reason); }
public void addScoreDetail(String detail) { this.scoreDetails.add(detail); }}InvestorSuitabilityRules.java
Section titled “InvestorSuitabilityRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 19(生产级样板):投资者适当性匹配规则集 * <p> * 五维成熟度评分(年龄 / 收入 / 资产 / 经验 / 风险承受问卷),再按产品与承受等级差、 * 高龄、低成熟度决定可购/确认/禁购;风险揭示书签署校验由 Service 前置。 * 规则脚本见 {@code src/test/resources/demo/investor-suitability.xml}。 */public interface InvestorSuitabilityRules {
/** * 年龄成熟度评分(0-15) */ int scoreAge(@Param("age") int age);
/** * 收入成熟度评分(0-15) */ int scoreIncome(@Param("annualIncome") double annualIncome);
/** * 资产成熟度评分(0-20) */ int scoreAssets(@Param("netAssets") double netAssets);
/** * 投资经验评分(0-20) */ int scoreExperience(@Param("investExperienceYears") int investExperienceYears);
/** * 风险承受问卷评分(0-30) */ int scoreTolerance(@Param("riskToleranceRank") int riskToleranceRank);
/** * 是否需要确认(双录):高龄客户或低成熟度客户购买 R3 及以上产品时触发 */ boolean needConfirm(@Param("maturityScore") int maturityScore, @Param("productRiskRank") int productRiskRank, @Param("elderly") boolean elderly);
/** * 风险等级差(产品 - 承受) */ int rankGap(@Param("riskToleranceRank") int riskToleranceRank, @Param("productRiskRank") int productRiskRank);}investor-suitability.xml
Section titled “investor-suitability.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.InvestorSuitabilityRules">
<!-- 年龄成熟度评分:0-15,25-50 岁投资生涯最稳健 --> <java name="scoreAge" returnClass="int"> <![CDATA[ int def scoreAge(int:age) { if (age >= 25 && age <= 50) { return 15; } else if (age >= 51 && age <= 60) { return 12; } else if (age >= 18 && age <= 24) { return 8; } else if (age >= 61 && age <= 64) { return 6; } else { return 3; } } ]]> </java>
<!-- 收入成熟度评分:0-15 --> <java name="scoreIncome" returnClass="int"> <![CDATA[ int def scoreIncome(double:annualIncome) { if (annualIncome >= 500000.0) { return 15; } else if (annualIncome >= 200000.0) { return 12; } else if (annualIncome >= 100000.0) { return 8; } else if (annualIncome >= 50000.0) { return 4; } else { return 2; } } ]]> </java>
<!-- 资产成熟度评分:0-20 --> <java name="scoreAssets" returnClass="int"> <![CDATA[ int def scoreAssets(double:netAssets) { if (netAssets >= 3000000.0) { return 20; } else if (netAssets >= 1000000.0) { return 15; } else if (netAssets >= 300000.0) { return 10; } else if (netAssets >= 100000.0) { return 5; } else { return 2; } } ]]> </java>
<!-- 投资经验评分:0-20 --> <java name="scoreExperience" returnClass="int"> <![CDATA[ int def scoreExperience(int:investExperienceYears) { if (investExperienceYears >= 10) { return 20; } else if (investExperienceYears >= 5) { return 15; } else if (investExperienceYears >= 3) { return 10; } else if (investExperienceYears >= 1) { return 5; } else { return 0; } } ]]> </java>
<!-- 风险承受问卷评分:0-30 --> <java name="scoreTolerance" returnClass="int"> <![CDATA[ int def scoreTolerance(int:riskToleranceRank) { if (riskToleranceRank >= 5) { return 30; } else if (riskToleranceRank == 4) { return 24; } else if (riskToleranceRank == 3) { return 18; } else if (riskToleranceRank == 2) { return 10; } else { return 5; } } ]]> </java>
<!-- 是否需要确认(双录):高龄购中高风险,或低成熟度购中高风险 --> <java name="needConfirm" returnClass="boolean"> <![CDATA[ boolean def needConfirm(int:maturityScore, int:productRiskRank, boolean:elderly) { boolean confirm = false; if (maturityScore < 50) { if (productRiskRank >= 3) { confirm = true; } } if (elderly) { if (productRiskRank >= 3) { confirm = true; } } return confirm; } ]]> </java>
<!-- 风险等级差 --> <java name="rankGap" returnClass="int"> <![CDATA[ int def rankGap(int:riskToleranceRank, int:productRiskRank) { return productRiskRank - riskToleranceRank; } ]]> </java>
</javas>InvestorSuitabilityService.java
Section titled “InvestorSuitabilityService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.InvestorSuitabilityRules;import com.github.paohaijiao.demo.model.InvestorAssessment;import com.github.paohaijiao.demo.model.SuitabilityDecision;
/** * 场景 19:投资者适当性匹配服务 * <p> * 规则侧(XML):成熟度评分口径、等级差与确认/禁购阈值。 * Java 侧:年龄与签署前置校验、阻断原因组装、双录要求与可购等级回填。 */public class InvestorSuitabilityService {
/** * 高龄投资者门槛 */ private static final int ELDERLY_AGE = 65;
private final InvestorSuitabilityRules rules;
public InvestorSuitabilityService(InvestorSuitabilityRules rules) { this.rules = rules; }
public SuitabilityDecision evaluate(InvestorAssessment assessment) { SuitabilityDecision decision = new SuitabilityDecision(); decision.setInvestorId(assessment.getInvestorId()); decision.setRiskToleranceRank(assessment.getRiskToleranceRank()); decision.setProductRiskRank(assessment.getProductRiskRank()); decision.setMaxAllowedRank(assessment.getRiskToleranceRank()); decision.setRankGap(rules.rankGap( assessment.getRiskToleranceRank(), assessment.getProductRiskRank()));
if (assessment.getAge() < 18) { block(decision, "客户未满 18 周岁,不具备金融产品购买民事行为能力"); return decision; } if (!assessment.isSignedRiskNotice()) { block(decision, "未签署《" + assessment.getProductName() + "》产品风险揭示书,须先完成签署与风险测评"); return decision; }
int ageScore = rules.scoreAge(assessment.getAge()); int incomeScore = rules.scoreIncome(assessment.getAnnualIncome()); int assetsScore = rules.scoreAssets(assessment.getNetAssets()); int experienceScore = rules.scoreExperience(assessment.getInvestExperienceYears()); int toleranceScore = rules.scoreTolerance(assessment.getRiskToleranceRank()); int totalScore = ageScore + incomeScore + assetsScore + experienceScore + toleranceScore; if (totalScore > 100) { totalScore = 100; }
decision.addScoreDetail("年龄成熟度评分: " + ageScore + " / 15"); decision.addScoreDetail("收入成熟度评分: " + incomeScore + " / 15"); decision.addScoreDetail("资产成熟度评分: " + assetsScore + " / 20"); decision.addScoreDetail("投资经验评分: " + experienceScore + " / 20"); decision.addScoreDetail("风险承受问卷评分: " + toleranceScore + " / 30"); decision.addScoreDetail("汇总评分: " + totalScore); decision.setMaturityScore(totalScore);
// 产品风险高于承受等级:适当性硬阻断(Java 侧硬政策,不随评分脚本调整) if (assessment.getProductRiskRank() > assessment.getRiskToleranceRank()) { decision.setDecision("BLOCK"); decision.addReason("产品风险等级 R" + assessment.getProductRiskRank() + " 高于客户风险承受等级 C" + assessment.getRiskToleranceRank() + ",依据《证券期货投资者适当性管理办法》禁止向其销售"); return decision; }
boolean elderly = assessment.getAge() >= ELDERLY_AGE; if (rules.needConfirm(totalScore, assessment.getProductRiskRank(), elderly)) { decision.setDecision("CONFIRM"); decision.setDoubleRecordingRequired(true); if (elderly) { decision.addReason("客户年满 " + ELDERLY_AGE + " 周岁购买 R" + assessment.getProductRiskRank() + " 中高风险产品,须在网点双录并由成年直系亲属陪同确认"); } else { decision.addReason("客户投资成熟度评分 " + totalScore + " 低于 50,购买 R" + assessment.getProductRiskRank() + " 产品须双录并手抄特别风险提示,冷静期 24 小时后回访确认"); } return decision; }
decision.setDecision("ALLOW"); return decision; }
private void block(SuitabilityDecision decision, String reason) { decision.setDecision("BLOCK"); decision.setMaturityScore(0); decision.addReason(reason); }}InvestorSuitabilityServiceTest.java
Section titled “InvestorSuitabilityServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.InvestorAssessment;import com.github.paohaijiao.demo.model.SuitabilityDecision;import com.github.paohaijiao.demo.service.InvestorSuitabilityService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 19 生产级样板:理财客户风险等级适当性匹配 * <p> * 覆盖 6 组客户形态:C5 高净值购 R4、C2 保守购 R1 存款、C2 越级购 R3 禁购、 * 高龄客户购 R3 双录确认、未签揭示书阻断、低成熟度客户购 R4 冷静期确认。 */public class InvestorSuitabilityServiceTest {
private InvestorSuitabilityService service;
@Before public void setUp() { InvestorSuitabilityRules rules = JQuickJava.create() .createApi(InvestorSuitabilityRules.class, "demo/investor-suitability.xml"); this.service = new InvestorSuitabilityService(rules); }
/** * C5 高净值客户购买 R4 偏股型基金:100 分直接放行 */ @Test public void shouldAllowSophisticatedInvestor() { InvestorAssessment assessment = new InvestorAssessment( "INV001", "高净值客户韩冰", 45, 800000.0, 5000000.0, 12, 5, 4, "沪深300指数增强基金", true);
SuitabilityDecision decision = service.evaluate(assessment);
Assert.assertEquals(100, decision.getMaturityScore()); Assert.assertEquals("ALLOW", decision.getDecision()); Assert.assertEquals(-1, decision.getRankGap()); Assert.assertFalse(decision.isDoubleRecordingRequired()); }
/** * C2 保守型年轻客户购买 R1 大额存单:成熟度虽低但 R1 不触发确认,直接放行 */ @Test public void shouldAllowConservativeInvestorForDeposit() { InvestorAssessment assessment = new InvestorAssessment( "INV002", "职场新人许晴", 30, 120000.0, 150000.0, 1, 2, 1, "三年期大额存单", true);
SuitabilityDecision decision = service.evaluate(assessment);
Assert.assertEquals(43, decision.getMaturityScore()); Assert.assertEquals("ALLOW", decision.getDecision()); Assert.assertEquals(2, decision.getMaxAllowedRank()); Assert.assertTrue(decision.getReasons().isEmpty()); }
/** * C2 客户越级购买 R3 混合基金:适当性硬阻断 */ @Test public void shouldBlockMismatchedRiskProduct() { InvestorAssessment assessment = new InvestorAssessment( "INV003", "保守客户冯鑫", 35, 150000.0, 200000.0, 3, 2, 3, "平衡型混合基金", true);
SuitabilityDecision decision = service.evaluate(assessment);
Assert.assertEquals("BLOCK", decision.getDecision()); Assert.assertEquals(1, decision.getRankGap()); Assert.assertTrue(decision.getReasons().contains( "产品风险等级 R3 高于客户风险承受等级 C2,依据《证券期货投资者适当性管理办法》禁止向其销售")); }
/** * 68 岁 C3 客户购买 R3 债券基金:可购但必须网点双录、家属陪同 */ @Test public void shouldRequireDoubleRecordingForElderly() { InvestorAssessment assessment = new InvestorAssessment( "INV004", "老年客户沈奶奶", 68, 300000.0, 2000000.0, 8, 3, 3, "中短债基金", true);
SuitabilityDecision decision = service.evaluate(assessment);
Assert.assertEquals(63, decision.getMaturityScore()); Assert.assertEquals("CONFIRM", decision.getDecision()); Assert.assertTrue(decision.isDoubleRecordingRequired()); Assert.assertTrue(decision.getReasons().contains( "客户年满 65 周岁购买 R3 中高风险产品,须在网点双录并由成年直系亲属陪同确认")); }
/** * 未签署风险揭示书:前置阻断 */ @Test public void shouldBlockWithoutSignedRiskNotice() { InvestorAssessment assessment = new InvestorAssessment( "INV005", "未签揭示书客户", 40, 600000.0, 3000000.0, 10, 4, 4, "私募股权投资基金", false);
SuitabilityDecision decision = service.evaluate(assessment);
Assert.assertEquals("BLOCK", decision.getDecision()); Assert.assertTrue(decision.getReasons().contains( "未签署《私募股权投资基金》产品风险揭示书,须先完成签署与风险测评")); }
/** * 低成熟度 C4 年轻客户购买 R4 产品:评分 43,双录 + 手抄 + 24 小时冷静期 */ @Test public void shouldConfirmLowMaturityInvestor() { InvestorAssessment assessment = new InvestorAssessment( "INV006", "新手投资者蒋帆", 23, 80000.0, 80000.0, 1, 4, 4, "行业主题股票基金", true);
SuitabilityDecision decision = service.evaluate(assessment);
Assert.assertEquals(43, decision.getMaturityScore()); Assert.assertEquals("CONFIRM", decision.getDecision()); Assert.assertTrue(decision.isDoubleRecordingRequired()); Assert.assertTrue(decision.getReasons().contains( "客户投资成熟度评分 43 低于 50,购买 R4 产品须双录并手抄特别风险提示,冷静期 24 小时后回访确认")); }}Run & verified results
Section titled “Run & verified results”Run from the project root:
mvn test -Dtest=InvestorSuitabilityServiceTestTest coverage
Section titled “Test coverage”| Test method | Constructed business profile | Key assertions / expected decision |
|---|---|---|
shouldAllowSophisticatedInvestor |
Age 45, annual income 800K, net assets 5M, 12 years experience, C5 purchasing R4 index-enhanced fund, signed | Maturity 100 points, ALLOW; rank gap -1; dual recording not required |
shouldAllowConservativeInvestorForDeposit |
Age 30, income 120K, net assets 150K, 1 year experience, C2 purchasing R1 time-deposit certificate | Maturity 43 points, ALLOW (R1 does not trigger dual recording); max allowed rank backfilled as 2; reason list empty |
shouldBlockMismatchedRiskProduct |
Age 35, C2 purchasing above-rank R3 balanced hybrid fund | BLOCK; rank gap 1; reason contains “R3 higher than C2; sales prohibited under the Measures for the Administration of Suitability of Securities and Futures Investors” |
shouldRequireDoubleRecordingForElderly |
Age 68, net assets 2M, 8 years experience, C3 purchasing R3 short-to-medium-term bond fund | Maturity 63 points, CONFIRM; dual-recording flag true; reason contains age 65 or older requiring branch dual recording and adult direct relative accompaniment |
shouldBlockWithoutSignedRiskNotice |
Age 40, C4 purchasing R4 private-equity fund, signedRiskNotice=false |
BLOCK; reason contains “risk-disclosure letter for the Private-Equity Investment Fund not signed; must complete signing and risk assessment first” |
shouldConfirmLowMaturityInvestor |
Age 23, income 80K, net assets 80K, 1 year experience, C4 purchasing R4 sector-theme stock fund | Maturity 43 points, CONFIRM; dual recording true; reason contains score below 50, hand-copying special risk warning, and 24-hour cooling-off period follow-up |
Technical highlights
Section titled “Technical highlights”- Compliance hard policies and tunable scoring layered separately: above-rank bans (product R rank higher than client C rank) live in the Service and are annotated as “hard policy on the Java side, not adjusted by the scoring script”; the five-dimension maturity criteria are externalized to the XML for continuous tuning, and changes to the two layers never contaminate each other.
- Pre-check blocking before scoring: under-18 and unsigned risk-disclosure letters short-circuit to BLOCK before any scoring action, with maturity set to 0; unsigned clients never access the assessment or product information.
- Dual-recording trigger rules scripted, divergent scripts assembled on the Java side:
needConfirmuniformly evaluates both elderly and low-maturity trigger conditions, and the Service then generates the two compliance reasons — “relative accompaniment” for elderly clients and “hand-copying + 24-hour cooling-off” for low-maturity clients. - The script does not support the
!negation operator, so conditions are written positively:needConfirmaccumulates a local boolean variableconfirmthrough two positiveifblocks to express the OR logic of “low maturity or elderly,” avoiding the negation-syntax limitation;rankGapreturns a signed integer gap, where the sign directly expresses the direction of the above-rank condition. - Structured decisions directly serve compliance traceability: the three-state conclusion ALLOW/CONFIRM/BLOCK, rank gap, max allowed rank, dual-recording flag, per-dimension score breakdown, and itemized reasons are all backfilled, so the sales page and audit ledger share the same source.
@Paramname binding + proxy reuse + ASM call chain: 7 rule methods share a single dynamic proxy and are called embedded in the transaction chain; Java 8 with no heavy dependencies, capable of running on legacy wealth-management systems and domestic-platform JDKs.