@@ -705,5 +705,279 @@ console.log("\n=== 8. 回归测试:旧行为不应出现 ===\n");
705705 assert ( form . reservePolicyMode === "none" , "8c: schwab reserve cleared to 'none'" ) ;
706706}
707707
708+ // ============================================================
709+ // 9. syncRuntimeTargetForAccount (解析为具体值)
710+ // ============================================================
711+
712+ // --- 辅助函数 ---
713+ function runtimeTargetStateForAccount ( state , platform , account ) {
714+ const entry = currentEntryForAccount ( state , platform , account ) ;
715+ if ( ! entry ) return { known : false , enabled : null } ;
716+ const configured = cleanOptionalBoolean ( entry . runtime_target_enabled ) ;
717+ return { known : true , enabled : configured ?? true } ;
718+ }
719+
720+ function syncRuntimeTargetForAccount ( state , form , platform , account ) {
721+ if ( ! form || form . runtimeTargetTouched ) return ;
722+ const target = runtimeTargetStateForAccount ( state , platform , account ) ;
723+ form . runtimeTargetMode = target . known ? ( target . enabled ? "enabled" : "disabled" ) : "current" ;
724+ }
725+
726+ // --- 测试 ---
727+ console . log ( "\n=== 9. syncRuntimeTargetForAccount (解析为具体值) ===\n" ) ;
728+
729+ // 9a: runtime_target_enabled: true → enabled
730+ {
731+ const state = { currentStrategies : makeCurrentStrategies ( "ibkr" , { extra : { runtime_target_enabled : true } } ) } ;
732+ const form = { runtimeTargetMode : "current" , runtimeTargetTouched : false } ;
733+ syncRuntimeTargetForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
734+ assert ( form . runtimeTargetMode === "enabled" , "9a: runtime_target=true → 'enabled'" ) ;
735+ }
736+
737+ // 9b: runtime_target_enabled: false → disabled
738+ {
739+ const state = { currentStrategies : makeCurrentStrategies ( "ibkr" , { extra : { runtime_target_enabled : false } } ) } ;
740+ const form = { runtimeTargetMode : "current" , runtimeTargetTouched : false } ;
741+ syncRuntimeTargetForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
742+ assert ( form . runtimeTargetMode === "disabled" , "9b: runtime_target=false → 'disabled'" ) ;
743+ }
744+
745+ // 9c: no entry → stays "current"
746+ {
747+ const state = { currentStrategies : { } } ;
748+ const form = { runtimeTargetMode : "current" , runtimeTargetTouched : false } ;
749+ syncRuntimeTargetForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
750+ assert ( form . runtimeTargetMode === "current" , "9c: no entry → 'current'" ) ;
751+ }
752+
753+ // 9d: touched → skip
754+ {
755+ const state = { currentStrategies : makeCurrentStrategies ( "ibkr" , { extra : { runtime_target_enabled : false } } ) } ;
756+ const form = { runtimeTargetMode : "enabled" , runtimeTargetTouched : true } ;
757+ syncRuntimeTargetForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
758+ assert ( form . runtimeTargetMode === "enabled" , "9d: touched → not overwritten" ) ;
759+ }
760+
761+ // ============================================================
762+ // 10. syncIncomeLayerForAccount (解析为具体值)
763+ // ============================================================
764+
765+ // --- 辅助函数 ---
766+ function incomeLayerFromEntry ( entry ) {
767+ return {
768+ enabled : cleanOptionalBoolean ( entry ?. income_layer_enabled ) ,
769+ startUsd : cleanDisplayNumber ( entry ?. income_layer_start_usd ) ,
770+ maxRatio : cleanDisplayRatio ( entry ?. income_layer_max_ratio ) ,
771+ } ;
772+ }
773+
774+ function incomeLayerFieldsConfigured ( entry ) {
775+ const current = incomeLayerFromEntry ( entry ) ;
776+ return current . enabled !== null || Boolean ( current . startUsd || current . maxRatio ) ;
777+ }
778+
779+ function incomeLayerDefaultForStrategy ( profile ) {
780+ return profile ? { startUsd : 100000 , maxRatio : "0.30" } : null ;
781+ }
782+
783+ function effectiveIncomeLayerForAccount ( state , platform , account , profile ) {
784+ const defaults = incomeLayerDefaultForStrategy ( profile ) ;
785+ if ( ! defaults ) return null ;
786+ const entry = currentEntryForAccount ( state , platform , account ) ;
787+ if ( ! entry ) return null ;
788+ const current = incomeLayerFromEntry ( entry ) ;
789+ if ( ! incomeLayerFieldsConfigured ( entry ) ) {
790+ return { enabled : true , startUsd : String ( defaults . startUsd ) , maxRatio : defaults . maxRatio } ;
791+ }
792+ return {
793+ enabled : current . enabled ?? true ,
794+ startUsd : current . startUsd || String ( defaults . startUsd ) ,
795+ maxRatio : current . maxRatio || defaults . maxRatio ,
796+ } ;
797+ }
798+
799+ function syncIncomeLayerForAccount ( state , form , platform , account ) {
800+ if ( ! form || form . incomeLayerTouched ) return ;
801+ const defaults = incomeLayerDefaultForStrategy ( form . strategy ) ;
802+ const current = incomeLayerFromEntry ( currentEntryForAccount ( state , platform , account ) ) ;
803+ form . incomeLayerStartUsd = current . startUsd || String ( defaults ?. startUsd || "" ) ;
804+ form . incomeLayerMaxRatio = current . maxRatio || defaults ?. maxRatio || "" ;
805+ const effective = effectiveIncomeLayerForAccount ( state , platform , account , form . strategy ) ;
806+ if ( effective ) {
807+ form . incomeLayerMode = effective . enabled ? "enabled" : "disabled" ;
808+ } else if ( defaults ) {
809+ form . incomeLayerMode = "enabled" ;
810+ } else {
811+ form . incomeLayerMode = "current" ;
812+ }
813+ }
814+
815+ console . log ( "\n=== 10. syncIncomeLayerForAccount (解析为具体值) ===\n" ) ;
816+
817+ // 10a: income_layer_enabled: true → enabled
818+ {
819+ const state = { currentStrategies : makeCurrentStrategies ( "ibkr" , {
820+ extra : { income_layer_enabled : true , income_layer_start_usd : "200000" , income_layer_max_ratio : "0.25" }
821+ } ) } ;
822+ const form = { incomeLayerMode : "current" , incomeLayerTouched : false , strategy : "some_profile" } ;
823+ syncIncomeLayerForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
824+ assert ( form . incomeLayerMode === "enabled" , "10a: income_layer enabled → 'enabled'" ) ;
825+ }
826+
827+ // 10b: income_layer_enabled: false → disabled
828+ {
829+ const state = { currentStrategies : makeCurrentStrategies ( "ibkr" , {
830+ extra : { income_layer_enabled : false , income_layer_start_usd : "200000" , income_layer_max_ratio : "0.25" }
831+ } ) } ;
832+ const form = { incomeLayerMode : "current" , incomeLayerTouched : false , strategy : "some_profile" } ;
833+ syncIncomeLayerForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
834+ assert ( form . incomeLayerMode === "disabled" , "10b: income_layer disabled → 'disabled'" ) ;
835+ }
836+
837+ // 10c: no entry + has defaults → enabled (default)
838+ {
839+ const state = { currentStrategies : { } } ;
840+ const form = { incomeLayerMode : "current" , incomeLayerTouched : false , strategy : "some_profile" } ;
841+ syncIncomeLayerForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
842+ assert ( form . incomeLayerMode === "enabled" , "10c: no entry + defaults → 'enabled'" ) ;
843+ }
844+
845+ // 10d: touched → skip
846+ {
847+ const state = { currentStrategies : makeCurrentStrategies ( "ibkr" , {
848+ extra : { income_layer_enabled : false }
849+ } ) } ;
850+ const form = { incomeLayerMode : "enabled" , incomeLayerTouched : true , strategy : "some_profile" } ;
851+ syncIncomeLayerForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
852+ assert ( form . incomeLayerMode === "enabled" , "10d: touched → not overwritten" ) ;
853+ }
854+
855+ // ============================================================
856+ // 11. syncOptionOverlayForAccount (解析为具体值)
857+ // ============================================================
858+
859+ function optionOverlaySupported ( profile ) { return profile !== "no_overlay" ; }
860+ function currentOptionOverlayForAccount ( state , platform , account ) {
861+ return cleanOptionalBoolean ( currentEntryForAccount ( state , platform , account ) ?. option_overlay_enabled ) ;
862+ }
863+ function effectiveOptionOverlayForAccount ( state , platform , account , profile ) {
864+ const configured = currentOptionOverlayForAccount ( state , platform , account ) ;
865+ if ( configured !== null ) return configured ;
866+ if ( ! optionOverlaySupported ( profile ) ) return null ;
867+ return true ;
868+ }
869+ function normalizeOptionOverlayMode ( value ) {
870+ return [ "current" , "enabled" , "disabled" ] . includes ( value ) ? value : "current" ;
871+ }
872+
873+ function syncOptionOverlayForAccount ( state , form , platform , account ) {
874+ if ( ! form || form . optionOverlayTouched ) return ;
875+ const configured = normalizeOptionOverlayMode ( account ?. option_overlay_mode ) ;
876+ if ( configured !== "current" ) {
877+ form . optionOverlayMode = configured ;
878+ return ;
879+ }
880+ if ( ! optionOverlaySupported ( form . strategy ) ) {
881+ form . optionOverlayMode = "disabled" ;
882+ return ;
883+ }
884+ const effective = effectiveOptionOverlayForAccount ( state , platform , account , form . strategy ) ;
885+ if ( effective !== null && effective !== undefined ) {
886+ form . optionOverlayMode = effective ? "enabled" : "disabled" ;
887+ } else {
888+ form . optionOverlayMode = "enabled" ;
889+ }
890+ }
891+
892+ console . log ( "\n=== 11. syncOptionOverlayForAccount (解析为具体值) ===\n" ) ;
893+
894+ // 11a: option_overlay_enabled: true → enabled
895+ {
896+ const state = { currentStrategies : makeCurrentStrategies ( "ibkr" , { extra : { option_overlay_enabled : true } } ) } ;
897+ const form = { optionOverlayMode : "current" , optionOverlayTouched : false , strategy : "some_profile" } ;
898+ syncOptionOverlayForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
899+ assert ( form . optionOverlayMode === "enabled" , "11a: option overlay enabled → 'enabled'" ) ;
900+ }
901+
902+ // 11b: option_overlay_enabled: false → disabled
903+ {
904+ const state = { currentStrategies : makeCurrentStrategies ( "ibkr" , { extra : { option_overlay_enabled : false } } ) } ;
905+ const form = { optionOverlayMode : "current" , optionOverlayTouched : false , strategy : "some_profile" } ;
906+ syncOptionOverlayForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
907+ assert ( form . optionOverlayMode === "disabled" , "11b: option overlay disabled → 'disabled'" ) ;
908+ }
909+
910+ // 11c: no entry + supported → enabled (default)
911+ {
912+ const state = { currentStrategies : { } } ;
913+ const form = { optionOverlayMode : "current" , optionOverlayTouched : false , strategy : "some_profile" } ;
914+ syncOptionOverlayForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
915+ assert ( form . optionOverlayMode === "enabled" , "11c: no entry + supported → 'enabled'" ) ;
916+ }
917+
918+ // 11d: not supported → disabled
919+ {
920+ const state = { currentStrategies : makeCurrentStrategies ( "ibkr" , { extra : { option_overlay_enabled : true } } ) } ;
921+ const form = { optionOverlayMode : "current" , optionOverlayTouched : false , strategy : "no_overlay" } ;
922+ syncOptionOverlayForAccount ( state , form , "ibkr" , makeAccount ( "preview" ) ) ;
923+ assert ( form . optionOverlayMode === "disabled" , "11d: not supported → 'disabled'" ) ;
924+ }
925+
926+ // 11e: account has explicit mode → use it
927+ {
928+ const form = { optionOverlayMode : "current" , optionOverlayTouched : false , strategy : "some_profile" } ;
929+ syncOptionOverlayForAccount ( { } , form , "ibkr" , { key : "preview" , option_overlay_mode : "disabled" } ) ;
930+ assert ( form . optionOverlayMode === "disabled" , "11e: account explicit 'disabled' → 'disabled'" ) ;
931+ }
932+
933+ // ============================================================
934+ // 12. 互斥 UI 不再禁用选项 (回归测试)
935+ // ============================================================
936+
937+ console . log ( "\n=== 12. 互斥 UI 不再禁用选项 ===\n" ) ;
938+
939+ // 12a: 预留现金覆盖活跃时,select cash-only=disabled 不应因 option.disabled 被阻挡
940+ // (此测试验证 reconcileExecutionCashPolicy 可处理用户选择,无需前置禁用)
941+ {
942+ const form = {
943+ cashOnlyExecutionMode : "enabled" , // 当前:不允许融资
944+ reservePolicyMode : "ratio" ,
945+ minReservedCashUsd : "" ,
946+ reservedCashRatio : "0.05" ,
947+ reservedCashTouched : true ,
948+ cashOnlyExecutionTouched : false ,
949+ } ;
950+ // 用户选择"允许融资: 是"
951+ form . cashOnlyExecutionMode = "disabled" ;
952+ form . cashOnlyExecutionTouched = true ;
953+ reconcileExecutionCashPolicy ( form , "margin" ) ;
954+ // 预留现金应被清除
955+ assert ( form . reservePolicyMode === "none" , "12a: margin=yes → reserve cleared" ) ;
956+ assert ( form . cashOnlyExecutionMode === "disabled" , "12a: margin stays 'disabled'" ) ;
957+ // 不再冲突
958+ assert ( executionCashPolicyConflict ( form ) === false , "12a: no conflict after reconciliation" ) ;
959+ }
960+
961+ // 12b: margin 启用时,reserve 下拉框不应 disabled,用户可选 ratio
962+ {
963+ const form = {
964+ cashOnlyExecutionMode : "disabled" , // margin enabled
965+ reservePolicyMode : "none" ,
966+ minReservedCashUsd : "" ,
967+ reservedCashRatio : "" ,
968+ reservedCashTouched : false ,
969+ cashOnlyExecutionTouched : true ,
970+ } ;
971+ // 用户选择预留现金策略: ratio
972+ form . reservePolicyMode = "ratio" ;
973+ form . reservedCashRatio = "0.03" ;
974+ form . reservedCashTouched = true ;
975+ reconcileExecutionCashPolicy ( form , "reserve" ) ;
976+ // 融资应被禁用
977+ assert ( form . cashOnlyExecutionMode === "enabled" , "12b: reserve=ratio → margin disabled" ) ;
978+ assert ( form . reservePolicyMode === "ratio" , "12b: reserve stays 'ratio'" ) ;
979+ assert ( executionCashPolicyConflict ( form ) === false , "12b: no conflict after reconciliation" ) ;
980+ }
981+
708982// ============================================================
709983summary ( ) ;
0 commit comments