From 710e7cb26ac988477eb4828153fcd3effa99cb64 Mon Sep 17 00:00:00 2001 From: liveforpresent Date: Sat, 15 Aug 2026 16:44:22 +0900 Subject: [PATCH] feat(strategy): implement DefineStrategyVersion slice Persist immutable StrategyVersion definitions through the Strategy Store and expose the define endpoint. Related: ADR-001, ADR-002, ADR-013, ADR-018; Use Case DefineStrategyVersion. --- .../refinvest/RefinvestApplicationTests.kt | 92 +++++++++++++ gradle/libs.versions.toml | 1 + .../out/persistence/ConditionJpaEntity.kt | 43 +++++++ .../persistence/JpaStrategyReaderAdapter.kt | 48 ++++++- .../persistence/JpaStrategyStoreAdapter.kt | 121 +++++++++++++++++- .../out/persistence/StrategyJpaEntity.kt | 12 +- .../persistence/StrategyVersionJpaEntity.kt | 36 ++++++ .../id/SnowflakeStrategyVersionIdGenerator.kt | 13 ++ .../web/strategy/StrategyController.kt | 19 +++ .../define/DefineStrategyVersionRequest.kt | 89 +++++++++++++ .../define/DefineStrategyVersionResponse.kt | 65 ++++++++++ .../web/strategy/get/GetStrategyResponse.kt | 30 ++++- strategy/application/build.gradle.kts | 1 + .../define/DefineStrategyVersionService.kt | 46 +++++++ .../strategy/get/GetStrategyService.kt | 12 ++ .../create/CreateStrategyServiceTest.kt | 8 +- .../DefineStrategyVersionServiceTest.kt | 90 +++++++++++++ .../strategy/get/GetStrategyServiceTest.kt | 2 +- .../define/DefineStrategyVersionCommand.kt | 16 +++ .../define/DefineStrategyVersionResult.kt | 20 +++ .../define/DefineStrategyVersionUseCase.kt | 5 + .../inbound/strategy/get/GetStrategyResult.kt | 15 +++ .../port/outbound/StrategyIdGenerator.kt | 5 + .../strategy/port/outbound/StrategyReader.kt | 15 +++ .../strategy/port/outbound/StrategyStore.kt | 5 +- 25 files changed, 796 insertions(+), 13 deletions(-) create mode 100644 strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/ConditionJpaEntity.kt create mode 100644 strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/StrategyVersionJpaEntity.kt create mode 100644 strategy/adapter/snowflake/src/main/kotlin/com/refinvest/core/strategy/adapter/out/id/SnowflakeStrategyVersionIdGenerator.kt create mode 100644 strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/define/DefineStrategyVersionRequest.kt create mode 100644 strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/define/DefineStrategyVersionResponse.kt create mode 100644 strategy/application/src/main/kotlin/com/refinvest/core/strategy/application/strategy/define/DefineStrategyVersionService.kt create mode 100644 strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/define/DefineStrategyVersionServiceTest.kt create mode 100644 strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionCommand.kt create mode 100644 strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionResult.kt create mode 100644 strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionUseCase.kt diff --git a/app/src/test/kotlin/com/refinvest/RefinvestApplicationTests.kt b/app/src/test/kotlin/com/refinvest/RefinvestApplicationTests.kt index 8d36d70..83d3da4 100644 --- a/app/src/test/kotlin/com/refinvest/RefinvestApplicationTests.kt +++ b/app/src/test/kotlin/com/refinvest/RefinvestApplicationTests.kt @@ -105,6 +105,98 @@ class RefinvestApplicationTests( assertTrue(response.body().contains("\"versions\":[]"), response.body()) } + @Test + fun `defines a strategy version through HTTP and returns it from strategy retrieval`() { + val created = HttpClient.newHttpClient().send( + HttpRequest.newBuilder(URI("http://localhost:$port/strategies")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"volatility hypothesis\"}")) + .build(), + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8), + ) + val strategyId = "\"id\":\"(\\d+)\"".toRegex().find(created.body())?.groupValues?.get(1) + assertTrue(created.statusCode() == 201 && strategyId != null, created.body()) + + val defined = HttpClient.newHttpClient().send( + HttpRequest.newBuilder(URI("http://localhost:$port/strategies/$strategyId/versions")) + .header("Content-Type", "application/json") + .POST( + HttpRequest.BodyPublishers.ofString( + """{ + | "primarySignalAsset":"QQQ", + | "conditions":[{ + | "operator":"LT", + | "operandA":{"asset":"QQQ","metric":"RETURN","window":5}, + | "operandB":-0.07 + | }], + | "executionAsset":"TQQQ", + | "lag":3, + | "exit":{"holdingSignalSessions":5} + |}""".trimMargin(), + ), + ) + .build(), + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8), + ) + + assertTrue(defined.statusCode() == 201, defined.body()) + assertTrue(defined.body().contains("\"strategyId\":\"$strategyId\""), defined.body()) + assertTrue(jdbcTemplate.queryForObject("select count(*) from strategy_versions", Long::class.java) == 1L) + + val retrieved = HttpClient.newHttpClient().send( + HttpRequest.newBuilder(URI("http://localhost:$port/strategies/$strategyId")).GET().build(), + HttpResponse.BodyHandlers.ofString(), + ) + + assertTrue(retrieved.statusCode() == 200, retrieved.body()) + assertTrue(retrieved.body().contains("\"primarySignalAsset\":\"QQQ\""), retrieved.body()) + assertTrue(retrieved.body().contains("\"latestVersionId\":"), retrieved.body()) + } + + @Test + fun `rejects VIX as an execution asset when defining a strategy version`() { + val created = HttpClient.newHttpClient().send( + HttpRequest.newBuilder(URI("http://localhost:$port/strategies")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"volatility hypothesis\"}")) + .build(), + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8), + ) + val strategyId = "\"id\":\"(\\d+)\"".toRegex().find(created.body())?.groupValues?.get(1) + assertTrue(created.statusCode() == 201 && strategyId != null, created.body()) + + val response = HttpClient.newHttpClient().send( + HttpRequest.newBuilder(URI("http://localhost:$port/strategies/$strategyId/versions")) + .header("Content-Type", "application/json") + .POST( + HttpRequest.BodyPublishers.ofString( + """{ + | "primarySignalAsset":"QQQ", + | "conditions":[{ + | "operator":"GT", + | "operandA":{"asset":"QQQ","metric":"SIMPLE"}, + | "operandB":1 + | }], + | "executionAsset":"VIX", + | "lag":0, + | "exit":{"holdingSignalSessions":1} + |}""".trimMargin(), + ), + ) + .build(), + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8), + ) + + assertTrue(response.statusCode() == 400, response.body()) + assertTrue( + jdbcTemplate.queryForObject( + "select count(*) from strategy_versions where strategy_id = ?", + Long::class.java, + strategyId.toLong(), + ) == 0L, + ) + } + @Test fun `returns not found for an unknown strategy`() { val response = HttpClient.newHttpClient().send( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1145fe7..8d25dcc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,3 +23,4 @@ postgresql = { module = "org.postgresql:postgresql" } h2 = { module = "com.h2database:h2" } jackson-module-kotlin = { module = "tools.jackson.module:jackson-module-kotlin" } spring-context = { module = "org.springframework:spring-context", version.ref = "spring-framework" } +spring-tx = { module = "org.springframework:spring-tx", version.ref = "spring-framework" } diff --git a/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/ConditionJpaEntity.kt b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/ConditionJpaEntity.kt new file mode 100644 index 0000000..7dbdca9 --- /dev/null +++ b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/ConditionJpaEntity.kt @@ -0,0 +1,43 @@ +package com.refinvest.core.strategy.adapter.out.persistence + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table + +@Entity +@Table(name = "strategy_version_conditions") +class ConditionJpaEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + @ManyToOne + @JoinColumn(name = "strategy_version_id", nullable = false) + var strategyVersion: StrategyVersionJpaEntity, + @Column(name = "condition_order", nullable = false) + var conditionOrder: Int, + @Column(nullable = false) + var operator: String, + @Column(name = "logical_combinator") + var logicalCombinator: String?, + @Column(name = "operand_a_asset", nullable = false) + var operandAAsset: String, + @Column(name = "operand_a_metric", nullable = false) + var operandAMetric: String, + @Column(name = "operand_a_window") + var operandAWindow: Int?, + @Column(name = "operand_b_kind", nullable = false) + var operandBKind: String, + @Column(name = "operand_b_literal") + var operandBLiteral: Double?, + @Column(name = "operand_b_asset") + var operandBAsset: String?, + @Column(name = "operand_b_metric") + var operandBMetric: String?, + @Column(name = "operand_b_window") + var operandBWindow: Int?, +) diff --git a/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/JpaStrategyReaderAdapter.kt b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/JpaStrategyReaderAdapter.kt index c197b95..5ce4de4 100644 --- a/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/JpaStrategyReaderAdapter.kt +++ b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/JpaStrategyReaderAdapter.kt @@ -1,8 +1,20 @@ package com.refinvest.core.strategy.adapter.out.persistence import com.refinvest.core.strategy.domain.StrategyId +import com.refinvest.core.strategy.domain.StrategyVersionId +import com.refinvest.core.strategy.domain.AssetSymbol +import com.refinvest.core.strategy.domain.ComparisonOperator +import com.refinvest.core.strategy.domain.Condition +import com.refinvest.core.strategy.domain.LiteralValue +import com.refinvest.core.strategy.domain.LogicalCombinator +import com.refinvest.core.strategy.domain.MetricOperand +import com.refinvest.core.strategy.domain.MetricReference +import com.refinvest.core.strategy.domain.MetricType +import com.refinvest.core.strategy.domain.SignalSessions +import com.refinvest.core.strategy.domain.TimeBasedExit import com.refinvest.core.strategy.port.outbound.StrategyReadModel import com.refinvest.core.strategy.port.outbound.StrategyReader +import com.refinvest.core.strategy.port.outbound.StrategyVersionReadModel import org.springframework.stereotype.Repository @Repository @@ -11,11 +23,45 @@ class JpaStrategyReaderAdapter( ) : StrategyReader { override fun findById(id: StrategyId): StrategyReadModel? = strategyJpaReader.findById(id.value)?.let { strategy -> + val versions = strategy.versions.map { it.toReadModel() } StrategyReadModel( id = StrategyId(strategy.id), name = strategy.name, createdAt = strategy.createdAt, - latestVersionId = null, + latestVersionId = versions.lastOrNull()?.id, + versions = versions, ) } + + private fun StrategyVersionJpaEntity.toReadModel(): StrategyVersionReadModel = StrategyVersionReadModel( + id = StrategyVersionId(id), + createdAt = createdAt, + primarySignalAsset = AssetSymbol.valueOf(primarySignalAsset), + conditions = conditions.map { it.toDomain() }, + executionAsset = AssetSymbol.valueOf(executionAsset), + lag = SignalSessions(lag), + exit = TimeBasedExit(holdingSignalSessions), + ) + + private fun ConditionJpaEntity.toDomain(): Condition = Condition( + operator = ComparisonOperator.valueOf(operator), + logicalCombinator = logicalCombinator?.let(LogicalCombinator::valueOf), + operandA = MetricReference(AssetSymbol.valueOf(operandAAsset), MetricType.valueOf(operandAMetric), operandAWindow), + operandB = when (operandBKind) { + LITERAL -> LiteralValue(requireNotNull(operandBLiteral)) + METRIC -> MetricOperand( + MetricReference( + AssetSymbol.valueOf(requireNotNull(operandBAsset)), + MetricType.valueOf(requireNotNull(operandBMetric)), + operandBWindow, + ), + ) + else -> error("Unsupported operand B kind: $operandBKind") + }, + ) + + private companion object { + const val LITERAL = "LITERAL" + const val METRIC = "METRIC" + } } diff --git a/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/JpaStrategyStoreAdapter.kt b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/JpaStrategyStoreAdapter.kt index e1a6290..2840139 100644 --- a/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/JpaStrategyStoreAdapter.kt +++ b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/JpaStrategyStoreAdapter.kt @@ -1,6 +1,21 @@ package com.refinvest.core.strategy.adapter.out.persistence import com.refinvest.core.strategy.domain.Strategy +import com.refinvest.core.strategy.domain.StrategyId +import com.refinvest.core.strategy.domain.StrategyVersion +import com.refinvest.core.strategy.domain.AssetSymbol +import com.refinvest.core.strategy.domain.ComparisonOperator +import com.refinvest.core.strategy.domain.Condition +import com.refinvest.core.strategy.domain.ConditionOperand +import com.refinvest.core.strategy.domain.LiteralValue +import com.refinvest.core.strategy.domain.LogicalCombinator +import com.refinvest.core.strategy.domain.MemberId +import com.refinvest.core.strategy.domain.MetricOperand +import com.refinvest.core.strategy.domain.MetricReference +import com.refinvest.core.strategy.domain.MetricType +import com.refinvest.core.strategy.domain.SignalSessions +import com.refinvest.core.strategy.domain.StrategyVersionId +import com.refinvest.core.strategy.domain.TimeBasedExit import com.refinvest.core.strategy.port.outbound.StrategyStore import org.springframework.stereotype.Repository @@ -8,14 +23,106 @@ import org.springframework.stereotype.Repository class JpaStrategyStoreAdapter( private val strategyJpaStore: StrategyJpaStore, ) : StrategyStore { + override fun findById(id: StrategyId): Strategy? = + strategyJpaStore.findById(id.value).orElse(null)?.toDomain() + override fun save(strategy: Strategy) { - strategyJpaStore.save( - StrategyJpaEntity( - id = strategy.id.value, - memberId = strategy.memberId.value, - name = strategy.name, - createdAt = strategy.createdAt, - ), + val entity = StrategyJpaEntity( + id = strategy.id.value, + memberId = strategy.memberId.value, + name = strategy.name, + createdAt = strategy.createdAt, + ) + entity.replaceVersions(strategy.versions.map { it.toEntity(entity) }) + strategyJpaStore.save(entity) + } + + private fun StrategyJpaEntity.toDomain(): Strategy = Strategy.create( + id = StrategyId(id), + memberId = MemberId(memberId), + name = name, + createdAt = createdAt, + ).also { strategy -> versions.forEach { strategy.addVersion(it.toDomain()) } } + + private fun StrategyVersionJpaEntity.toDomain(): StrategyVersion = StrategyVersion.create( + id = StrategyVersionId(id), + strategyId = StrategyId(strategy.id), + createdAt = createdAt, + primarySignalAsset = AssetSymbol.valueOf(primarySignalAsset), + conditions = conditions.map { it.toDomain() }, + executionAsset = AssetSymbol.valueOf(executionAsset), + lag = SignalSessions(lag), + exit = TimeBasedExit(holdingSignalSessions), + ) + + private fun StrategyVersion.toEntity(strategy: StrategyJpaEntity): StrategyVersionJpaEntity { + val entity = StrategyVersionJpaEntity( + id = id.value, + strategy = strategy, + createdAt = createdAt, + primarySignalAsset = primarySignalAsset.name, + executionAsset = executionAsset.name, + lag = lag.value, + holdingSignalSessions = exit.holdingSignalSessions, ) + entity.conditions += conditions.mapIndexed { index, condition -> condition.toEntity(entity, index) } + return entity + } + + private fun ConditionJpaEntity.toDomain(): Condition = Condition( + operator = ComparisonOperator.valueOf(operator), + logicalCombinator = logicalCombinator?.let(LogicalCombinator::valueOf), + operandA = MetricReference(AssetSymbol.valueOf(operandAAsset), MetricType.valueOf(operandAMetric), operandAWindow), + operandB = when (operandBKind) { + LITERAL -> LiteralValue(requireNotNull(operandBLiteral)) + METRIC -> MetricOperand( + MetricReference( + AssetSymbol.valueOf(requireNotNull(operandBAsset)), + MetricType.valueOf(requireNotNull(operandBMetric)), + operandBWindow, + ), + ) + else -> error("Unsupported operand B kind: $operandBKind") + }, + ) + + private fun Condition.toEntity( + strategyVersion: StrategyVersionJpaEntity, + conditionOrder: Int, + ): ConditionJpaEntity = + when (val operandB = operandB) { + is LiteralValue -> ConditionJpaEntity( + strategyVersion = strategyVersion, + conditionOrder = conditionOrder, + operator = operator.name, + logicalCombinator = logicalCombinator?.name, + operandAAsset = operandA.asset.name, + operandAMetric = operandA.metric.name, + operandAWindow = operandA.window, + operandBKind = LITERAL, + operandBLiteral = operandB.value, + operandBAsset = null, + operandBMetric = null, + operandBWindow = null, + ) + is MetricOperand -> ConditionJpaEntity( + strategyVersion = strategyVersion, + conditionOrder = conditionOrder, + operator = operator.name, + logicalCombinator = logicalCombinator?.name, + operandAAsset = operandA.asset.name, + operandAMetric = operandA.metric.name, + operandAWindow = operandA.window, + operandBKind = METRIC, + operandBLiteral = null, + operandBAsset = operandB.reference.asset.name, + operandBMetric = operandB.reference.metric.name, + operandBWindow = operandB.reference.window, + ) + } + + private companion object { + const val LITERAL = "LITERAL" + const val METRIC = "METRIC" } } diff --git a/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/StrategyJpaEntity.kt b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/StrategyJpaEntity.kt index 138ab21..54b16ce 100644 --- a/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/StrategyJpaEntity.kt +++ b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/StrategyJpaEntity.kt @@ -2,8 +2,11 @@ package com.refinvest.core.strategy.adapter.out.persistence import jakarta.persistence.Column import jakarta.persistence.Entity +import jakarta.persistence.FetchType import jakarta.persistence.Id +import jakarta.persistence.OneToMany import jakarta.persistence.Table +import jakarta.persistence.CascadeType import java.time.Instant @Entity @@ -17,4 +20,11 @@ class StrategyJpaEntity( var name: String, @Column(name = "created_at", nullable = false) var createdAt: Instant, -) + @OneToMany(mappedBy = "strategy", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.EAGER) + var versions: MutableList = mutableListOf(), +) { + fun replaceVersions(newVersions: List) { + versions.clear() + versions += newVersions + } +} diff --git a/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/StrategyVersionJpaEntity.kt b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/StrategyVersionJpaEntity.kt new file mode 100644 index 0000000..4cfdd6d --- /dev/null +++ b/strategy/adapter/persistence/src/main/kotlin/com/refinvest/core/strategy/adapter/out/persistence/StrategyVersionJpaEntity.kt @@ -0,0 +1,36 @@ +package com.refinvest.core.strategy.adapter.out.persistence + +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.OrderBy +import jakarta.persistence.Table +import java.time.Instant + +@Entity +@Table(name = "strategy_versions") +class StrategyVersionJpaEntity( + @Id + var id: Long, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "strategy_id", nullable = false) + var strategy: StrategyJpaEntity, + @Column(name = "created_at", nullable = false) + var createdAt: Instant, + @Column(name = "primary_signal_asset", nullable = false) + var primarySignalAsset: String, + @Column(name = "execution_asset", nullable = false) + var executionAsset: String, + @Column(nullable = false) + var lag: Int, + @Column(name = "holding_signal_sessions", nullable = false) + var holdingSignalSessions: Int, + @OneToMany(mappedBy = "strategyVersion", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.EAGER) + @OrderBy("conditionOrder") + var conditions: MutableList = mutableListOf(), +) diff --git a/strategy/adapter/snowflake/src/main/kotlin/com/refinvest/core/strategy/adapter/out/id/SnowflakeStrategyVersionIdGenerator.kt b/strategy/adapter/snowflake/src/main/kotlin/com/refinvest/core/strategy/adapter/out/id/SnowflakeStrategyVersionIdGenerator.kt new file mode 100644 index 0000000..0f3e994 --- /dev/null +++ b/strategy/adapter/snowflake/src/main/kotlin/com/refinvest/core/strategy/adapter/out/id/SnowflakeStrategyVersionIdGenerator.kt @@ -0,0 +1,13 @@ +package com.refinvest.core.strategy.adapter.out.id + +import com.refinvest.core.shared.infrastructure.id.SnowflakeIdGenerator +import com.refinvest.core.strategy.domain.StrategyVersionId +import com.refinvest.core.strategy.port.outbound.StrategyVersionIdGenerator +import org.springframework.stereotype.Component + +@Component +class SnowflakeStrategyVersionIdGenerator( + private val snowflakeIdGenerator: SnowflakeIdGenerator, +) : StrategyVersionIdGenerator { + override fun next(): StrategyVersionId = StrategyVersionId(snowflakeIdGenerator.next()) +} diff --git a/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/StrategyController.kt b/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/StrategyController.kt index 660e245..4280a3a 100644 --- a/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/StrategyController.kt +++ b/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/StrategyController.kt @@ -2,12 +2,16 @@ package com.refinvest.core.strategy.adapter.web.strategy import com.refinvest.core.strategy.adapter.web.strategy.create.CreateStrategyRequest import com.refinvest.core.strategy.adapter.web.strategy.create.CreateStrategyResponse +import com.refinvest.core.strategy.adapter.web.strategy.define.DefineStrategyVersionRequest +import com.refinvest.core.strategy.adapter.web.strategy.define.DefineStrategyVersionResponse import com.refinvest.core.strategy.adapter.web.strategy.get.GetStrategyResponse +import com.refinvest.core.strategy.adapter.web.strategy.get.GetStrategyVersionResponse import com.refinvest.core.strategy.domain.StrategyId import com.refinvest.core.strategy.port.inbound.strategy.create.CreateStrategyCommand import com.refinvest.core.strategy.port.inbound.strategy.create.CreateStrategyUseCase import com.refinvest.core.strategy.port.inbound.strategy.get.GetStrategyQuery import com.refinvest.core.strategy.port.inbound.strategy.get.GetStrategyUseCase +import com.refinvest.core.strategy.port.inbound.strategy.define.DefineStrategyVersionUseCase import jakarta.validation.Valid import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.GetMapping @@ -24,6 +28,7 @@ import org.springframework.web.server.ResponseStatusException class StrategyController( private val createStrategyUseCase: CreateStrategyUseCase, private val getStrategyUseCase: GetStrategyUseCase, + private val defineStrategyVersionUseCase: DefineStrategyVersionUseCase, ) { @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -46,6 +51,20 @@ class StrategyController( name = result.name, createdAt = result.createdAt, latestVersionId = result.latestVersionId?.value?.toString(), + versions = result.versions.map { GetStrategyVersionResponse.from(result.id.value.toString(), it) }, ) } + + @PostMapping("/{strategyId}/versions") + @ResponseStatus(HttpStatus.CREATED) + fun defineVersion( + @PathVariable strategyId: Long, + @Valid @RequestBody request: DefineStrategyVersionRequest, + ): DefineStrategyVersionResponse = try { + defineStrategyVersionUseCase.execute(request.toCommand(StrategyId(strategyId))) + ?.let(DefineStrategyVersionResponse::from) + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Strategy not found") + } catch (exception: IllegalArgumentException) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, exception.message, exception) + } } diff --git a/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/define/DefineStrategyVersionRequest.kt b/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/define/DefineStrategyVersionRequest.kt new file mode 100644 index 0000000..1ab5ef7 --- /dev/null +++ b/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/define/DefineStrategyVersionRequest.kt @@ -0,0 +1,89 @@ +package com.refinvest.core.strategy.adapter.web.strategy.define + +import tools.jackson.databind.JsonNode +import com.refinvest.core.strategy.domain.AssetSymbol +import com.refinvest.core.strategy.domain.ComparisonOperator +import com.refinvest.core.strategy.domain.Condition +import com.refinvest.core.strategy.domain.ConditionOperand +import com.refinvest.core.strategy.domain.LiteralValue +import com.refinvest.core.strategy.domain.LogicalCombinator +import com.refinvest.core.strategy.domain.MetricOperand +import com.refinvest.core.strategy.domain.MetricReference +import com.refinvest.core.strategy.domain.MetricType +import com.refinvest.core.strategy.domain.SignalSessions +import com.refinvest.core.strategy.domain.StrategyId +import com.refinvest.core.strategy.domain.TimeBasedExit +import com.refinvest.core.strategy.port.inbound.strategy.define.DefineStrategyVersionCommand +import jakarta.validation.Valid +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull + +data class DefineStrategyVersionRequest( + val primarySignalAsset: String, + @field:NotEmpty + val conditions: List<@Valid ConditionRequest>, + val executionAsset: String, + @field:Min(0) + val lag: Int, + @field:NotNull + @field:Valid + val exit: TimeBasedExitRequest, +) { + fun toCommand(strategyId: StrategyId): DefineStrategyVersionCommand = DefineStrategyVersionCommand( + strategyId = strategyId, + primarySignalAsset = AssetSymbol.from(primarySignalAsset), + conditions = conditions.map(ConditionRequest::toDomain), + executionAsset = AssetSymbol.from(executionAsset), + lag = SignalSessions(lag), + exit = TimeBasedExit(exit.holdingSignalSessions), + ) +} + +data class ConditionRequest( + val operator: String, + val logicalCombinator: String? = null, + @field:Valid + val operandA: MetricReferenceRequest, + val operandB: JsonNode, +) { + fun toDomain(): Condition = Condition( + operator = ComparisonOperator.valueOf(operator), + logicalCombinator = logicalCombinator?.let(LogicalCombinator::valueOf), + operandA = operandA.toDomain(), + operandB = operandB.toDomainOperand(), + ) + + private fun JsonNode.toDomainOperand(): ConditionOperand = when { + isNumber -> LiteralValue(asDouble()) + isObject -> MetricOperand( + MetricReference( + asset = AssetSymbol.from(requiredText("asset")), + metric = MetricType.valueOf(requiredText("metric")), + window = get("window")?.takeUnless(JsonNode::isNull)?.asInt(), + ), + ) + else -> throw IllegalArgumentException("operandB must be a number or MetricReference") + } + + private fun JsonNode.requiredText(fieldName: String): String = + get(fieldName)?.takeUnless(JsonNode::isNull)?.asText() + ?: throw IllegalArgumentException("operandB.$fieldName is required") +} + +data class MetricReferenceRequest( + val asset: String, + val metric: String, + val window: Int? = null, +) { + fun toDomain(): MetricReference = MetricReference( + asset = AssetSymbol.from(asset), + metric = MetricType.valueOf(metric), + window = window, + ) +} + +data class TimeBasedExitRequest( + @field:Min(1) + val holdingSignalSessions: Int, +) diff --git a/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/define/DefineStrategyVersionResponse.kt b/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/define/DefineStrategyVersionResponse.kt new file mode 100644 index 0000000..6c5fac5 --- /dev/null +++ b/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/define/DefineStrategyVersionResponse.kt @@ -0,0 +1,65 @@ +package com.refinvest.core.strategy.adapter.web.strategy.define + +import com.refinvest.core.strategy.domain.Condition +import com.refinvest.core.strategy.domain.LiteralValue +import com.refinvest.core.strategy.domain.MetricOperand +import com.refinvest.core.strategy.port.inbound.strategy.define.DefineStrategyVersionResult +import java.time.Instant + +data class DefineStrategyVersionResponse( + val id: String, + val strategyId: String, + val createdAt: Instant, + val primarySignalAsset: String, + val conditions: List, + val executionAsset: String, + val lag: Int, + val exit: TimeBasedExitResponse, +) { + companion object { + fun from(result: DefineStrategyVersionResult): DefineStrategyVersionResponse = DefineStrategyVersionResponse( + id = result.id.value.toString(), + strategyId = result.strategyId.value.toString(), + createdAt = result.createdAt, + primarySignalAsset = result.primarySignalAsset.name, + conditions = result.conditions.map(ConditionResponse::from), + executionAsset = result.executionAsset.name, + lag = result.lag.value, + exit = TimeBasedExitResponse(result.exit.holdingSignalSessions), + ) + } +} + +data class ConditionResponse( + val operator: String, + val logicalCombinator: String?, + val operandA: MetricReferenceResponse, + val operandB: Any, +) { + companion object { + fun from(condition: Condition): ConditionResponse = ConditionResponse( + operator = condition.operator.name, + logicalCombinator = condition.logicalCombinator?.name, + operandA = MetricReferenceResponse.from(condition.operandA), + operandB = when (val operandB = condition.operandB) { + is LiteralValue -> operandB.value + is MetricOperand -> MetricReferenceResponse.from(operandB.reference) + }, + ) + } +} + +data class MetricReferenceResponse( + val asset: String, + val metric: String, + val window: Int?, +) { + companion object { + fun from(reference: com.refinvest.core.strategy.domain.MetricReference): MetricReferenceResponse = + MetricReferenceResponse(reference.asset.name, reference.metric.name, reference.window) + } +} + +data class TimeBasedExitResponse( + val holdingSignalSessions: Int, +) diff --git a/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/get/GetStrategyResponse.kt b/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/get/GetStrategyResponse.kt index 2913129..c4f3548 100644 --- a/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/get/GetStrategyResponse.kt +++ b/strategy/adapter/web/src/main/kotlin/com/refinvest/core/strategy/adapter/web/strategy/get/GetStrategyResponse.kt @@ -1,11 +1,39 @@ package com.refinvest.core.strategy.adapter.web.strategy.get import java.time.Instant +import com.refinvest.core.strategy.adapter.web.strategy.define.ConditionResponse +import com.refinvest.core.strategy.adapter.web.strategy.define.TimeBasedExitResponse +import com.refinvest.core.strategy.port.inbound.strategy.get.GetStrategyVersionResult data class GetStrategyResponse( val id: String, val name: String, val createdAt: Instant, val latestVersionId: String?, - val versions: List = emptyList(), + val versions: List, ) + +data class GetStrategyVersionResponse( + val id: String, + val strategyId: String, + val createdAt: Instant, + val primarySignalAsset: String, + val conditions: List, + val executionAsset: String, + val lag: Int, + val exit: TimeBasedExitResponse, +) { + companion object { + fun from(strategyId: String, result: GetStrategyVersionResult): GetStrategyVersionResponse = + GetStrategyVersionResponse( + id = result.id.value.toString(), + strategyId = strategyId, + createdAt = result.createdAt, + primarySignalAsset = result.primarySignalAsset.name, + conditions = result.conditions.map(ConditionResponse::from), + executionAsset = result.executionAsset.name, + lag = result.lag.value, + exit = TimeBasedExitResponse(result.exit.holdingSignalSessions), + ) + } +} diff --git a/strategy/application/build.gradle.kts b/strategy/application/build.gradle.kts index 9b64912..99986e5 100644 --- a/strategy/application/build.gradle.kts +++ b/strategy/application/build.gradle.kts @@ -2,4 +2,5 @@ plugins { id("kotlin-common-conventions") } dependencies { implementation(project(":strategy:port")) implementation(libs.spring.context) + implementation(libs.spring.tx) } diff --git a/strategy/application/src/main/kotlin/com/refinvest/core/strategy/application/strategy/define/DefineStrategyVersionService.kt b/strategy/application/src/main/kotlin/com/refinvest/core/strategy/application/strategy/define/DefineStrategyVersionService.kt new file mode 100644 index 0000000..b3c3042 --- /dev/null +++ b/strategy/application/src/main/kotlin/com/refinvest/core/strategy/application/strategy/define/DefineStrategyVersionService.kt @@ -0,0 +1,46 @@ +package com.refinvest.core.strategy.application.strategy.define + +import com.refinvest.core.strategy.domain.StrategyVersion +import com.refinvest.core.strategy.port.inbound.strategy.define.DefineStrategyVersionCommand +import com.refinvest.core.strategy.port.inbound.strategy.define.DefineStrategyVersionResult +import com.refinvest.core.strategy.port.inbound.strategy.define.DefineStrategyVersionUseCase +import com.refinvest.core.strategy.port.outbound.StrategyStore +import com.refinvest.core.strategy.port.outbound.StrategyVersionIdGenerator +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.Clock + +@Service +open class DefineStrategyVersionService( + private val strategyStore: StrategyStore, + private val strategyVersionIdGenerator: StrategyVersionIdGenerator, + private val clock: Clock, +) : DefineStrategyVersionUseCase { + @Transactional + override fun execute(command: DefineStrategyVersionCommand): DefineStrategyVersionResult? { + val strategy = strategyStore.findById(command.strategyId) ?: return null + val version = StrategyVersion.create( + id = strategyVersionIdGenerator.next(), + strategyId = strategy.id, + createdAt = clock.instant(), + primarySignalAsset = command.primarySignalAsset, + conditions = command.conditions, + executionAsset = command.executionAsset, + lag = command.lag, + exit = command.exit, + ) + strategy.addVersion(version) + strategyStore.save(strategy) + + return DefineStrategyVersionResult( + id = version.id, + strategyId = version.strategyId, + createdAt = version.createdAt, + primarySignalAsset = version.primarySignalAsset, + conditions = version.conditions, + executionAsset = version.executionAsset, + lag = version.lag, + exit = version.exit, + ) + } +} diff --git a/strategy/application/src/main/kotlin/com/refinvest/core/strategy/application/strategy/get/GetStrategyService.kt b/strategy/application/src/main/kotlin/com/refinvest/core/strategy/application/strategy/get/GetStrategyService.kt index ea3ea0c..2f65b70 100644 --- a/strategy/application/src/main/kotlin/com/refinvest/core/strategy/application/strategy/get/GetStrategyService.kt +++ b/strategy/application/src/main/kotlin/com/refinvest/core/strategy/application/strategy/get/GetStrategyService.kt @@ -2,6 +2,7 @@ package com.refinvest.core.strategy.application.strategy.get import com.refinvest.core.strategy.port.inbound.strategy.get.GetStrategyQuery import com.refinvest.core.strategy.port.inbound.strategy.get.GetStrategyResult +import com.refinvest.core.strategy.port.inbound.strategy.get.GetStrategyVersionResult import com.refinvest.core.strategy.port.inbound.strategy.get.GetStrategyUseCase import com.refinvest.core.strategy.port.outbound.StrategyReader import org.springframework.stereotype.Service @@ -17,6 +18,17 @@ class GetStrategyService( name = strategy.name, createdAt = strategy.createdAt, latestVersionId = strategy.latestVersionId, + versions = strategy.versions.map { version -> + GetStrategyVersionResult( + id = version.id, + createdAt = version.createdAt, + primarySignalAsset = version.primarySignalAsset, + conditions = version.conditions, + executionAsset = version.executionAsset, + lag = version.lag, + exit = version.exit, + ) + }, ) } } diff --git a/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/create/CreateStrategyServiceTest.kt b/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/create/CreateStrategyServiceTest.kt index 1c2f746..6ab8912 100644 --- a/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/create/CreateStrategyServiceTest.kt +++ b/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/create/CreateStrategyServiceTest.kt @@ -21,7 +21,13 @@ class CreateStrategyServiceTest { val memberId = MemberId(2L) var saved: Strategy? = null val service = CreateStrategyService( - strategyStore = StrategyStore { saved = it }, + strategyStore = object : StrategyStore { + override fun findById(id: StrategyId): Strategy? = null + + override fun save(strategy: Strategy) { + saved = strategy + } + }, strategyIdGenerator = StrategyIdGenerator { id }, memberIdProvider = MemberIdProvider { memberId }, clock = Clock.fixed(Instant.parse("2026-08-11T00:00:00Z"), ZoneOffset.UTC), diff --git a/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/define/DefineStrategyVersionServiceTest.kt b/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/define/DefineStrategyVersionServiceTest.kt new file mode 100644 index 0000000..9980bec --- /dev/null +++ b/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/define/DefineStrategyVersionServiceTest.kt @@ -0,0 +1,90 @@ +package com.refinvest.core.strategy.application.strategy.define + +import com.refinvest.core.strategy.domain.AssetSymbol +import com.refinvest.core.strategy.domain.ComparisonOperator +import com.refinvest.core.strategy.domain.Condition +import com.refinvest.core.strategy.domain.LiteralValue +import com.refinvest.core.strategy.domain.MemberId +import com.refinvest.core.strategy.domain.MetricReference +import com.refinvest.core.strategy.domain.MetricType +import com.refinvest.core.strategy.domain.SignalSessions +import com.refinvest.core.strategy.domain.Strategy +import com.refinvest.core.strategy.domain.StrategyId +import com.refinvest.core.strategy.domain.StrategyVersionId +import com.refinvest.core.strategy.domain.TimeBasedExit +import com.refinvest.core.strategy.port.inbound.strategy.define.DefineStrategyVersionCommand +import com.refinvest.core.strategy.port.outbound.StrategyStore +import com.refinvest.core.strategy.port.outbound.StrategyVersionIdGenerator +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class DefineStrategyVersionServiceTest { + private val strategyId = StrategyId(1L) + private val clock = Clock.fixed(Instant.parse("2026-08-15T00:00:00Z"), ZoneOffset.UTC) + + @Test + fun `adds an immutable version to the strategy and saves it`() { + val strategy = strategy() + val store = InMemoryStrategyStore(strategy) + val service = DefineStrategyVersionService( + strategyStore = store, + strategyVersionIdGenerator = StrategyVersionIdGenerator { StrategyVersionId(2L) }, + clock = clock, + ) + + val result = service.execute(command()) + + assertEquals(StrategyVersionId(2L), result?.id) + assertEquals(1, store.saved?.versions?.size) + assertEquals(AssetSymbol.QQQ, store.saved?.versions?.single()?.primarySignalAsset) + } + + @Test + fun `returns null when the strategy does not exist`() { + val service = DefineStrategyVersionService( + strategyStore = InMemoryStrategyStore(null), + strategyVersionIdGenerator = StrategyVersionIdGenerator { StrategyVersionId(2L) }, + clock = clock, + ) + + assertNull(service.execute(command())) + } + + private fun strategy(): Strategy = Strategy.create( + id = strategyId, + memberId = MemberId(1L), + name = "volatility hypothesis", + createdAt = clock.instant(), + ) + + private fun command(): DefineStrategyVersionCommand = DefineStrategyVersionCommand( + strategyId = strategyId, + primarySignalAsset = AssetSymbol.QQQ, + conditions = listOf( + Condition( + operator = ComparisonOperator.LT, + operandA = MetricReference(AssetSymbol.QQQ, MetricType.RETURN, 5), + operandB = LiteralValue(-0.07), + ), + ), + executionAsset = AssetSymbol.TQQQ, + lag = SignalSessions(3), + exit = TimeBasedExit(5), + ) + + private class InMemoryStrategyStore( + private val stored: Strategy?, + ) : StrategyStore { + var saved: Strategy? = null + + override fun findById(id: StrategyId): Strategy? = stored?.takeIf { it.id == id } + + override fun save(strategy: Strategy) { + saved = strategy + } + } +} diff --git a/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/get/GetStrategyServiceTest.kt b/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/get/GetStrategyServiceTest.kt index 01a32b1..99ec1a8 100644 --- a/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/get/GetStrategyServiceTest.kt +++ b/strategy/application/src/test/kotlin/com/refinvest/core/strategy/application/strategy/get/GetStrategyServiceTest.kt @@ -15,7 +15,7 @@ class GetStrategyServiceTest { val id = StrategyId(1L) val service = GetStrategyService( StrategyReader { - StrategyReadModel(id, "volatility hypothesis", Instant.parse("2026-08-11T00:00:00Z"), null) + StrategyReadModel(id, "volatility hypothesis", Instant.parse("2026-08-11T00:00:00Z"), null, emptyList()) }, ) diff --git a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionCommand.kt b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionCommand.kt new file mode 100644 index 0000000..c3d4151 --- /dev/null +++ b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionCommand.kt @@ -0,0 +1,16 @@ +package com.refinvest.core.strategy.port.inbound.strategy.define + +import com.refinvest.core.strategy.domain.AssetSymbol +import com.refinvest.core.strategy.domain.Condition +import com.refinvest.core.strategy.domain.SignalSessions +import com.refinvest.core.strategy.domain.StrategyId +import com.refinvest.core.strategy.domain.TimeBasedExit + +data class DefineStrategyVersionCommand( + val strategyId: StrategyId, + val primarySignalAsset: AssetSymbol, + val conditions: List, + val executionAsset: AssetSymbol, + val lag: SignalSessions, + val exit: TimeBasedExit, +) diff --git a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionResult.kt b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionResult.kt new file mode 100644 index 0000000..5e2ad16 --- /dev/null +++ b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionResult.kt @@ -0,0 +1,20 @@ +package com.refinvest.core.strategy.port.inbound.strategy.define + +import com.refinvest.core.strategy.domain.AssetSymbol +import com.refinvest.core.strategy.domain.Condition +import com.refinvest.core.strategy.domain.SignalSessions +import com.refinvest.core.strategy.domain.StrategyId +import com.refinvest.core.strategy.domain.StrategyVersionId +import com.refinvest.core.strategy.domain.TimeBasedExit +import java.time.Instant + +data class DefineStrategyVersionResult( + val id: StrategyVersionId, + val strategyId: StrategyId, + val createdAt: Instant, + val primarySignalAsset: AssetSymbol, + val conditions: List, + val executionAsset: AssetSymbol, + val lag: SignalSessions, + val exit: TimeBasedExit, +) diff --git a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionUseCase.kt b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionUseCase.kt new file mode 100644 index 0000000..7d493e9 --- /dev/null +++ b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/define/DefineStrategyVersionUseCase.kt @@ -0,0 +1,5 @@ +package com.refinvest.core.strategy.port.inbound.strategy.define + +fun interface DefineStrategyVersionUseCase { + fun execute(command: DefineStrategyVersionCommand): DefineStrategyVersionResult? +} diff --git a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/get/GetStrategyResult.kt b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/get/GetStrategyResult.kt index f418a4c..3bf613b 100644 --- a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/get/GetStrategyResult.kt +++ b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/inbound/strategy/get/GetStrategyResult.kt @@ -2,6 +2,10 @@ package com.refinvest.core.strategy.port.inbound.strategy.get import com.refinvest.core.strategy.domain.StrategyId import com.refinvest.core.strategy.domain.StrategyVersionId +import com.refinvest.core.strategy.domain.AssetSymbol +import com.refinvest.core.strategy.domain.Condition +import com.refinvest.core.strategy.domain.SignalSessions +import com.refinvest.core.strategy.domain.TimeBasedExit import java.time.Instant data class GetStrategyResult( @@ -9,4 +13,15 @@ data class GetStrategyResult( val name: String, val createdAt: Instant, val latestVersionId: StrategyVersionId?, + val versions: List, +) + +data class GetStrategyVersionResult( + val id: StrategyVersionId, + val createdAt: Instant, + val primarySignalAsset: AssetSymbol, + val conditions: List, + val executionAsset: AssetSymbol, + val lag: SignalSessions, + val exit: TimeBasedExit, ) diff --git a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyIdGenerator.kt b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyIdGenerator.kt index bf27bff..9f5bbe2 100644 --- a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyIdGenerator.kt +++ b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyIdGenerator.kt @@ -1,7 +1,12 @@ package com.refinvest.core.strategy.port.outbound import com.refinvest.core.strategy.domain.StrategyId +import com.refinvest.core.strategy.domain.StrategyVersionId fun interface StrategyIdGenerator { fun next(): StrategyId } + +fun interface StrategyVersionIdGenerator { + fun next(): StrategyVersionId +} diff --git a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyReader.kt b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyReader.kt index 25e70fa..0fa9bf1 100644 --- a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyReader.kt +++ b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyReader.kt @@ -2,6 +2,10 @@ package com.refinvest.core.strategy.port.outbound import com.refinvest.core.strategy.domain.StrategyId import com.refinvest.core.strategy.domain.StrategyVersionId +import com.refinvest.core.strategy.domain.AssetSymbol +import com.refinvest.core.strategy.domain.Condition +import com.refinvest.core.strategy.domain.SignalSessions +import com.refinvest.core.strategy.domain.TimeBasedExit import java.time.Instant fun interface StrategyReader { @@ -13,4 +17,15 @@ data class StrategyReadModel( val name: String, val createdAt: Instant, val latestVersionId: StrategyVersionId?, + val versions: List, +) + +data class StrategyVersionReadModel( + val id: StrategyVersionId, + val createdAt: Instant, + val primarySignalAsset: AssetSymbol, + val conditions: List, + val executionAsset: AssetSymbol, + val lag: SignalSessions, + val exit: TimeBasedExit, ) diff --git a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyStore.kt b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyStore.kt index 018a9f8..d7b2481 100644 --- a/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyStore.kt +++ b/strategy/port/src/main/kotlin/com/refinvest/core/strategy/port/outbound/StrategyStore.kt @@ -1,7 +1,10 @@ package com.refinvest.core.strategy.port.outbound import com.refinvest.core.strategy.domain.Strategy +import com.refinvest.core.strategy.domain.StrategyId + +interface StrategyStore { + fun findById(id: StrategyId): Strategy? -fun interface StrategyStore { fun save(strategy: Strategy) }