Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }}
LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }}
LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }}
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}
NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}
EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }}
LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }}
Expand Down Expand Up @@ -289,6 +291,18 @@ jobs:
remove_env_vars+=("LONGBRIDGE_DRY_RUN_ONLY")
fi

if [ -n "${INCOME_THRESHOLD_USD:-}" ]; then
env_pairs+=("INCOME_THRESHOLD_USD=${INCOME_THRESHOLD_USD}")
else
remove_env_vars+=("INCOME_THRESHOLD_USD")
fi

if [ -n "${QQQI_INCOME_RATIO:-}" ]; then
env_pairs+=("QQQI_INCOME_RATIO=${QQQI_INCOME_RATIO}")
else
remove_env_vars+=("QQQI_INCOME_RATIO")
fi

gcloud_args=(
run services update "${CLOUD_RUN_SERVICE}"
--region "${CLOUD_RUN_REGION}"
Expand Down Expand Up @@ -328,6 +342,8 @@ jobs:
LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }}
LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }}
LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }}
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}
NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}
EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }}
LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }}
Expand Down Expand Up @@ -585,6 +601,18 @@ jobs:
remove_env_vars+=("LONGBRIDGE_DRY_RUN_ONLY")
fi

if [ -n "${INCOME_THRESHOLD_USD:-}" ]; then
env_pairs+=("INCOME_THRESHOLD_USD=${INCOME_THRESHOLD_USD}")
else
remove_env_vars+=("INCOME_THRESHOLD_USD")
fi

if [ -n "${QQQI_INCOME_RATIO:-}" ]; then
env_pairs+=("QQQI_INCOME_RATIO=${QQQI_INCOME_RATIO}")
else
remove_env_vars+=("QQQI_INCOME_RATIO")
fi

gcloud_args=(
run services update "${CLOUD_RUN_SERVICE}"
--region "${CLOUD_RUN_REGION}"
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ Telegram notifications include structured execution and heartbeat messages, with
| `STRATEGY_PROFILE` | No | Strategy profile selector (default: `soxl_soxx_trend_income`; enabled values include `dynamic_mega_leveraged_pullback`, `global_etf_rotation`, `mega_cap_leader_rotation_dynamic_top20`, `russell_1000_multi_factor_defensive`, `soxl_soxx_trend_income`, `tech_communication_pullback_enhancement`, and `tqqq_growth_income`) |
| `ACCOUNT_REGION` | No | Account region marker for platform-style deployment (e.g. `HK`, `SG`; defaults to `ACCOUNT_PREFIX` / `DEFAULT`) |
| `LONGBRIDGE_DRY_RUN_ONLY` | No | Set to `true` to keep the selected deployment in dry-run mode. |
| `INCOME_THRESHOLD_USD` | No | Optional override for the `tqqq_growth_income` income-layer threshold. Leave unset to use the strategy package default. |
| `QQQI_INCOME_RATIO` | No | Optional override for QQQI's share of the `tqqq_growth_income` income layer, 0–1. |
| `NOTIFY_LANG` | No | Notification language: `en` (English, default) or `zh` (Chinese) |
| `GOOGLE_CLOUD_PROJECT` | No | GCP project ID (defaults to ADC project when unset) |

Expand Down Expand Up @@ -108,12 +110,12 @@ Recommended setup:
- Optional fallback only: `TELEGRAM_TOKEN`
- **GitHub Environment: `longbridge-hk`**
- Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME`
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO`
- Current live example: `STRATEGY_PROFILE=tech_communication_pullback_enhancement`
- Recommended secret-name values: `longport-app-key-hk`, `longport-app-secret-hk`
- **GitHub Environment: `longbridge-sg`**
- Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME`
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`
- Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO`
- Current live example: `STRATEGY_PROFILE=tqqq_growth_income`
- Recommended secret-name values: `longport-app-key-sg`, `longport-app-secret-sg`

Expand Down Expand Up @@ -209,6 +211,8 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换
| `STRATEGY_PROFILE` | 否 | 策略档位选择(默认: `soxl_soxx_trend_income`;已启用值还包括 `dynamic_mega_leveraged_pullback`、`global_etf_rotation`、`mega_cap_leader_rotation_dynamic_top20`、`russell_1000_multi_factor_defensive`、`soxl_soxx_trend_income`、`tech_communication_pullback_enhancement` 和 `tqqq_growth_income`) |
| `ACCOUNT_REGION` | 否 | 平台化部署时的账户区域标记(如 `HK`、`SG`;默认按 `ACCOUNT_PREFIX` / `DEFAULT` 推断) |
| `LONGBRIDGE_DRY_RUN_ONLY` | 否 | 设为 `true` 时,该部署保持 dry-run。 |
| `INCOME_THRESHOLD_USD` | 否 | 可选的 `tqqq_growth_income` 收入层启动阈值覆盖。不填时使用策略包默认值。 |
| `QQQI_INCOME_RATIO` | 否 | 可选的 QQQI 收入层占比覆盖,0–1。 |
| `NOTIFY_LANG` | 否 | 通知语言: `en`(英文,默认)或 `zh`(中文) |
| `GOOGLE_CLOUD_PROJECT` | 否 | GCP 项目 ID(未设置时使用 ADC 默认项目) |

Expand Down Expand Up @@ -251,12 +255,12 @@ Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `lo
- 仅保留为 fallback:`TELEGRAM_TOKEN`
- **GitHub Environment: `longbridge-hk`**
- Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME`
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`
- 当前线上示例:`STRATEGY_PROFILE=tech_communication_pullback_enhancement`
- 建议的 secret-name 值:`longport-app-key-hk`、`longport-app-secret-hk`
- **GitHub Environment: `longbridge-sg`**
- Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME`
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`
- 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`
- 当前线上示例:`STRATEGY_PROFILE=tqqq_growth_income`
- 建议的 secret-name 值:`longport-app-key-sg`、`longport-app-secret-sg`

Expand Down
18 changes: 18 additions & 0 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class PlatformRuntimeSettings:
tg_token: str | None
tg_chat_id: str | None
dry_run_only: bool
income_threshold_usd: float | None = None
qqqi_income_ratio: float | None = None
feature_snapshot_path: str | None = None
feature_snapshot_manifest_path: str | None = None
strategy_config_path: str | None = None
Expand Down Expand Up @@ -109,6 +111,8 @@ def load_platform_runtime_settings(
tg_token=os.getenv("TELEGRAM_TOKEN"),
tg_chat_id=os.getenv("GLOBAL_TELEGRAM_CHAT_ID"),
dry_run_only=_resolve_bool_env("LONGBRIDGE_DRY_RUN_ONLY"),
income_threshold_usd=_optional_float_env("INCOME_THRESHOLD_USD"),
qqqi_income_ratio=_qqqi_income_ratio_env(),
feature_snapshot_path=_first_non_empty(
os.getenv("LONGBRIDGE_FEATURE_SNAPSHOT_PATH"),
os.getenv("FEATURE_SNAPSHOT_PATH"),
Expand Down Expand Up @@ -159,6 +163,20 @@ def _resolve_bool_env(name: str) -> bool:
return str(raw_value).strip().lower() in {"1", "true", "yes", "on"}


def _optional_float_env(name: str) -> float | None:
raw_value = os.getenv(name)
if raw_value is None or raw_value.strip() == "":
return None
return float(raw_value)


def _qqqi_income_ratio_env() -> float | None:
value = _optional_float_env("QQQI_INCOME_RATIO")
if value is not None and not (0.0 <= value <= 1.0):
raise ValueError(f"QQQI_INCOME_RATIO must be in [0,1], got {value}")
return value


def _first_non_empty(*values: str | None) -> str | None:
for value in values:
text = str(value or "").strip()
Expand Down
16 changes: 16 additions & 0 deletions strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,21 @@ def _default_runtime_settings(profile: str, display_name: str) -> PlatformRuntim
)


def _build_runtime_overrides(profile: str, runtime_settings: PlatformRuntimeSettings) -> dict[str, Any]:
overrides: dict[str, Any] = {}
if profile == "tqqq_growth_income":
if runtime_settings.income_threshold_usd is not None:
overrides["income_threshold_usd"] = runtime_settings.income_threshold_usd
if runtime_settings.qqqi_income_ratio is not None:
overrides["qqqi_income_ratio"] = runtime_settings.qqqi_income_ratio
return overrides


def load_strategy_runtime(
raw_profile: str | None,
*,
runtime_settings: PlatformRuntimeSettings | None = None,
runtime_overrides: Mapping[str, Any] | None = None,
logger: Callable[[str], None] = print,
) -> LoadedStrategyRuntime:
entrypoint = load_strategy_entrypoint_for_profile(raw_profile)
Expand All @@ -290,19 +301,24 @@ def load_strategy_runtime(
entrypoint.manifest.profile,
entrypoint.manifest.display_name,
)
overrides = _build_runtime_overrides(entrypoint.manifest.profile, resolved_runtime_settings)
overrides.update(runtime_overrides or {})
runtime = LoadedStrategyRuntime(
entrypoint=entrypoint,
runtime_adapter=runtime_adapter,
runtime_settings=resolved_runtime_settings,
runtime_overrides=overrides,
logger=logger,
)
runtime_config = runtime.load_runtime_parameters()
merged_runtime_config = dict(entrypoint.manifest.default_config)
merged_runtime_config.update(runtime_config)
merged_runtime_config.update(overrides)
return LoadedStrategyRuntime(
entrypoint=entrypoint,
runtime_adapter=runtime_adapter,
runtime_settings=resolved_runtime_settings,
runtime_overrides=overrides,
runtime_config=runtime_config,
merged_runtime_config=merged_runtime_config,
logger=logger,
Expand Down
27 changes: 27 additions & 0 deletions tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def test_load_platform_runtime_settings_uses_defaults(self):
self.assertIsNone(settings.tg_token)
self.assertIsNone(settings.tg_chat_id)
self.assertFalse(settings.dry_run_only)
self.assertIsNone(settings.income_threshold_usd)
self.assertIsNone(settings.qqqi_income_ratio)
self.assertIsNone(settings.feature_snapshot_path)
self.assertIsNone(settings.strategy_config_path)

Expand Down Expand Up @@ -91,6 +93,31 @@ def test_dry_run_only_is_loaded_from_env(self):

self.assertTrue(settings.dry_run_only)

def test_income_layer_overrides_are_loaded_from_env(self):
with patch.dict(
os.environ,
{
"STRATEGY_PROFILE": "tqqq_growth_income",
"INCOME_THRESHOLD_USD": "100000",
"QQQI_INCOME_RATIO": "0.5",
},
clear=True,
):
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")

self.assertEqual(settings.strategy_profile, "tqqq_growth_income")
self.assertEqual(settings.income_threshold_usd, 100000.0)
self.assertEqual(settings.qqqi_income_ratio, 0.5)

def test_rejects_invalid_qqqi_income_ratio(self):
with patch.dict(
os.environ,
{"STRATEGY_PROFILE": "tqqq_growth_income", "QQQI_INCOME_RATIO": "1.5"},
clear=True,
):
with self.assertRaisesRegex(ValueError, "QQQI_INCOME_RATIO"):
load_platform_runtime_settings(project_id_resolver=lambda: "project-1")

def test_rejects_human_readable_alias(self):
with patch.dict(os.environ, {"STRATEGY_PROFILE": "semiconductor_trend_income"}, clear=True):
with self.assertRaises(ValueError):
Expand Down
53 changes: 52 additions & 1 deletion tests/test_strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@
from runtime_config_support import PlatformRuntimeSettings


class _TqqqEntrypoint:
manifest = StrategyManifest(
profile="tqqq_growth_income",
domain="us_equity",
display_name="TQQQ Growth Income",
description="test entrypoint",
required_inputs=frozenset({"benchmark_history", "portfolio_snapshot"}),
default_config={
"benchmark_symbol": "QQQ",
"managed_symbols": ("TQQQ", "QQQ", "BOXX", "SPYI", "QQQI"),
"income_threshold_usd": 1_000_000_000.0,
"qqqi_income_ratio": 0.5,
},
)

def evaluate(self, ctx):
self.ctx = ctx
return StrategyDecision(diagnostics={"signal_description": "tqqq"})


class _SemiconductorEntrypoint:
def __init__(self):
self.manifest = StrategyManifest(
Expand Down Expand Up @@ -73,7 +93,13 @@ def evaluate(self, ctx):
return StrategyDecision(diagnostics={"signal_description": "leveraged pullback"})


def _build_runtime_settings(profile: str, *, feature_snapshot_path: str | None = None) -> PlatformRuntimeSettings:
def _build_runtime_settings(
profile: str,
*,
feature_snapshot_path: str | None = None,
income_threshold_usd: float | None = None,
qqqi_income_ratio: float | None = None,
) -> PlatformRuntimeSettings:
return PlatformRuntimeSettings(
project_id=None,
secret_name="longport_token_hk",
Expand All @@ -88,6 +114,8 @@ def _build_runtime_settings(profile: str, *, feature_snapshot_path: str | None =
tg_token=None,
tg_chat_id=None,
dry_run_only=False,
income_threshold_usd=income_threshold_usd,
qqqi_income_ratio=qqqi_income_ratio,
feature_snapshot_path=feature_snapshot_path,
feature_snapshot_manifest_path=None,
strategy_config_path=None,
Expand Down Expand Up @@ -185,6 +213,29 @@ def test_load_strategy_runtime_uses_entrypoint_default_config(self):
self.assertIs(runtime.entrypoint, entrypoint)
self.assertEqual(runtime.managed_symbols, ("SOXL", "SOXX", "BOXX", "QQQI", "SPYI"))

def test_load_strategy_runtime_applies_tqqq_income_overrides_from_settings(self):
entrypoint = _TqqqEntrypoint()

with patch.object(strategy_runtime_module, "load_strategy_entrypoint_for_profile", return_value=entrypoint):
with patch.object(
strategy_runtime_module,
"load_strategy_runtime_adapter_for_profile",
return_value=StrategyRuntimeAdapter(portfolio_input_name="portfolio_snapshot"),
):
runtime = strategy_runtime_module.load_strategy_runtime(
"tqqq_growth_income",
runtime_settings=_build_runtime_settings(
"tqqq_growth_income",
income_threshold_usd=100000.0,
qqqi_income_ratio=0.5,
),
)

self.assertEqual(runtime.runtime_overrides["income_threshold_usd"], 100000.0)
self.assertEqual(runtime.runtime_overrides["qqqi_income_ratio"], 0.5)
self.assertEqual(runtime.merged_runtime_config["income_threshold_usd"], 100000.0)
self.assertEqual(runtime.merged_runtime_config["qqqi_income_ratio"], 0.5)

def test_feature_snapshot_runtime_loads_snapshot_into_context(self):
entrypoint = _TechEntrypoint()
runtime = strategy_runtime_module.LoadedStrategyRuntime(
Expand Down
4 changes: 4 additions & 0 deletions tests/test_sync_cloud_run_env_workflow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ grep -Fq 'LONGPORT_SECRET_NAME: ${{ vars.LONGPORT_SECRET_NAME }}' "$workflow_fil
grep -Fq 'LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }}' "$workflow_file"
grep -Fq 'LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }}' "$workflow_file"
grep -Fq 'LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }}' "$workflow_file"
grep -Fq 'INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}' "$workflow_file"
grep -Fq 'QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}' "$workflow_file"
grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }}' "$workflow_file"
grep -Fq "STRATEGY_PROFILE: \${{ vars.STRATEGY_PROFILE || 'soxl_soxx_trend_income' }}" "$workflow_file"
grep -Fq "ACCOUNT_REGION: \${{ vars.ACCOUNT_REGION || 'HK' }}" "$workflow_file"
Expand Down Expand Up @@ -73,6 +75,8 @@ grep -Fq 'LONGBRIDGE_FEATURE_SNAPSHOT_PATH=${LONGBRIDGE_FEATURE_SNAPSHOT_PATH}'
grep -Fq 'LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH=${LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH}' "$workflow_file"
grep -Fq 'LONGBRIDGE_STRATEGY_CONFIG_PATH=${LONGBRIDGE_STRATEGY_CONFIG_PATH}' "$workflow_file"
grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}' "$workflow_file"
grep -Fq 'INCOME_THRESHOLD_USD=${INCOME_THRESHOLD_USD}' "$workflow_file"
grep -Fq 'QQQI_INCOME_RATIO=${QQQI_INCOME_RATIO}' "$workflow_file"
grep -Fq 'STRATEGY_PROFILE=${STRATEGY_PROFILE}' "$workflow_file"
grep -Fq 'ACCOUNT_REGION=${ACCOUNT_REGION}' "$workflow_file"
grep -Fq '"SERVICE_NAME"' "$workflow_file"
Expand Down