使用Q学习和Scala Play框架进行期权交易
1. 利用训练好的模型进行预测
在期权交易场景中,当能够递归地选择具有最高奖励的状态以执行最优策略时(可参考以下代码中的
nextState
方法),就可以采用在线训练的方式运用Q学习算法。一旦使用给定数据完成Q学习模型的训练,就能够通过重写数据转换方法(即
PipeOperator
,也就是
|
),将当前状态转换为预测的目标状态,进而预测下一状态。代码如下:
override def |> : PartialFunction[QLState[T], Try[QLState[T]]] = {
case st: QLState[T]
if isModel =>
Try(
if (st.isGoal) st
else nextState(QLIndexedState[T](st, 0)).state)
}
虽然对模型进行评估是很有必要的,但在真实数据集上进行评估会更有意义。因为在虚假数据上运行和评估模型的性能,就如同买了一辆新车却从不驾驶它一样。接下来,将着手开发基于Q学习的期权交易应用。
2. 期权交易问题描述
此项目旨在根据当前观察到的特征,如到期时间、证券价格和波动率,预测未来N天某证券期权的价格。在期权定价模型方面,有多种选择,其中Black - Scholes随机偏微分方程(PDE)是最为知名的模型之一。
在金融数学中,Black - Scholes方程是一个PDE,用于描述在Black - Scholes模型下欧式看涨期权或欧式看跌期权的价格演变。对于不支付股息的标的股票的欧式看涨或看跌期权,方程如下:
其中,V是期权价格,它是股票价格S和时间t的函数;r是无风险利率;σ是股票的波动率。该方程背后的一个关键金融见解是,任何人都可以通过以正确的方式买卖标的资产来完美对冲期权,且无需承担任何风险。这意味着期权只有一个正确的价格,即由Black - Scholes公式给出的价格。
以IBM的期权为例,考虑一份1月到期、执行价格为95美元的看涨期权,同时卖出一份1月到期、执行价格为85美元的看跌期权。聚焦于IBM的看涨期权,下面的图表展示了2014年5月IBM股票及其执行价格为190美元的衍生看涨期权的每日价格:
现在的问题是,如果在期权到期日IBM股票售价为87美元,该头寸的盈亏情况如何?若售价为100美元又会怎样?在期权交易中,期权价格取决于几个参数,如时间衰减、价格和波动率:
- 期权到期时间(时间衰减)
- 标的证券价格
- 标的资产收益率的波动率
通常的定价模型往往不考虑标的证券交易量的变化,因此一些研究人员将其纳入了期权交易模型。由于基于强化学习(RL)的算法需要明确的状态,所以使用以下四个归一化特征来定义期权的状态:
- 时间衰减(timeToExp):归一化为(0, 1)范围内的到期剩余时间。
- 相对波动率(volatility):在一个交易时段内,标的证券价格的相对变化。它与Black - Scholes模型中定义的更复杂的收益率波动率有所不同。
- 相对于交易量的波动率(vltyByVol):调整了交易量后的标的证券价格的相对波动率。
- 当前价格与执行价格的相对差异(priceToStrike):价格与执行价格之差与执行价格的比率。
以下图表展示了可用于IBM期权策略的四个归一化特征:
有两个文件
IBM.csv
和
IBM_O.csv
分别包含了IBM的股票价格和期权价格数据。股票价格数据集包含日期、开盘价、最高价、最低价、收盘价、交易量和调整后的收盘价。
IBM_O.csv
文件包含了2014年10月18日到期、执行价格为190美元的IBM看涨期权的127个期权价格,部分数值如1.41、2.24、2.42等。那么,能否使用Q学习算法开发一个预测模型,以帮助确定IBM如何利用所有可用特征实现最大利润呢?由于已经了解Q学习的实现方法和期权交易的原理,并且所使用的技术如Scala、Akka、Scala Play Framework和RESTful服务之前已有讨论,因此有实现的可能性。接下来将尝试开发一个Scala Web项目,以实现利润最大化的目标。
3. 实现期权交易Web应用
3.1 期权交易策略实现步骤
使用Q学习实现期权交易策略包括以下步骤:
1. 描述期权的属性
2. 定义函数逼近
3. 指定状态转移的约束条件
3.2 创建期权属性
考虑到市场波动性,长期预测的可靠性较低,因为它可能超出离散马尔可夫模型的约束范围。假设要预测未来两天(N = 2)的期权价格,那么这两天后的期权价格就是奖励的盈亏值。因此,封装以下四个参数:
- timeToExp:到期剩余时间,以期权总持续时间的百分比表示
- Volatility normalized:给定交易时段内标的证券的相对波动率
- vltyByVol:给定交易时段内标的证券的波动率相对于该时段交易量的比率
- priceToStrike:给定交易时段内标的证券价格相对于执行价格的比率
OptionProperty
类用于定义证券上交易期权的属性,其构造函数如下:
class OptionProperty(timeToExp: Double,volatility: Double,vltyByVol:
Double,priceToStrike: Double) {
val toArray = Array[Double](timeToExp, volatility, vltyByVol,
priceToStrike)
require(timeToExp > 0.01, s"OptionProperty time to expiration found
$timeToExp required 0.01")
}
3.3 创建期权模型
需要创建一个
OptionModel
类,它作为期权属性的容器和工厂。该类接受以下参数,并通过访问前面描述的四个特征的数据源,创建一个期权属性列表
propsList
:
- 证券的符号
- 期权的执行价格
strikePrice
- 数据来源
src
- 最小时间衰减或到期时间
minTDecay
- 用于逼近每个特征值的步数(或桶数)
nSteps
OptionModel
类的构造函数如下:
class OptionModel(
symbol: String,
strikePrice: Double,
src: DataSource,
minExpT: Int,
nSteps: Int
)
在类的实现中,首先使用
check
方法进行验证,检查以下条件:
-
strikePrice
:必须为正价格
-
minExpT
:必须在2到16之间
-
nSteps
:至少需要两个步骤
check
方法的代码如下:
def check(strikePrice: Double, minExpT: Int, nSteps: Int): Unit = {
require(strikePrice > 0.0, s"OptionModel.check price found $strikePrice
required > 0")
require(minExpT > 2 && minExpT < 16,s"OptionModel.check Minimum
expiration time found $minExpT required ]2, 16[")
require(nSteps > 1,s"OptionModel.check, number of steps found $nSteps
required > 1")
}
当上述约束条件满足后,创建期权属性列表
propsList
的代码如下:
val propsList = (for {
price <- src.get(adjClose)
volatility <- src.get(volatility)
nVolatility <- normalize[Double](volatility)
vltyByVol <- src.get(volatilityByVol)
nVltyByVol <- normalize[Double](vltyByVol)
priceToStrike <- normalize[Double](price.map(p => 1.0 - strikePrice /
p))
}
yield {
nVolatility.zipWithIndex./:(List[OptionProperty]()) {
case (xs, (v, n)) =>
val normDecay = (n + minExpT).toDouble / (price.size + minExpT)
new OptionProperty(normDecay, v, nVltyByVol(n),
priceToStrike(n)) :: xs
}
.drop(2).reverse
}).get
在上述代码中,工厂使用Scala的
zipWithIndex
方法来表示交易时段的索引。所有特征值都在(0, 1)区间内进行归一化,包括
normDecay
期权的时间衰减(或到期时间)。
OptionModel
类的
quantize
方法将每个期权属性特征的归一化值转换为桶索引数组,并返回一个以桶索引数组为键的每个桶的盈亏映射:
def quantize(o: Array[Double]): Map[Array[Int], Double] = {
val mapper = new mutable.HashMap[Int, Array[Int]]
val acc: NumericAccumulator[Int] = propsList.view.map(_.toArray)
map(toArrayInt(_)).map(ar => {
val enc = encode(ar)
mapper.put(enc, ar)
enc
})
.zip(o)./:(
new NumericAccumulator[Int]) {
case (_acc, (t, y)) => _acc += (t, y); _acc
}
acc.map {
case (k, (v, w)) => (k, v / w) }
.map {
case (k, v) => (mapper(k), v) }.toMap
}
该方法还创建了一个
mapper
实例来索引桶数组。
NumericAccumulator
类型的累加器
acc
扩展了
Map[Int, (Int, Double)]
,用于计算每个桶上特征的出现次数以及期权价格的增减总和。
encode
和
toArrayInt
方法的代码如下:
private def encode(arr: Array[Int]): Int = arr./:((1, 0)) {
case ((s, t), n) => (s * nSteps, t + s * n) }._2
private def toArrayInt(feature: Array[Double]): Array[Int] =
feature.map(x => (nSteps *
x).floor.toInt)
final class NumericAccumulator[T]
extends mutable.HashMap[T, (Int, Double)] {
def +=(key: T, x: Double): Option[(Int, Double)] = {
val newValue =
if (contains(key)) (get(key).get._1 + 1, get(key).get._2 + x)
else (1, x)
super.put(key, newValue)
}
}
如果上述约束条件得到满足,并且
OptionModel
类的构造函数成功实例化,将生成一个
OptionProperty
元素列表;否则,生成一个空列表。
4. 整合实现
由于已经实现了Q学习算法,现在可以开发基于Q学习的期权交易应用。首先,使用
DataSource
类加载数据,然后使用
OptionModel
为给定股票创建期权模型,同时设置默认的执行价格和最小到期时间参数。接着,创建一个用于计算期权盈亏的模型,并对盈亏进行调整以产生正值。实例化一个实现Q学习算法的
QLearning
类,该类在实例化时会初始化并训练Q学习模型,以便在运行时进行预测。
创建一个Scala对象
QLearningMain
,并在其中定义和初始化以下参数:
val name: String = "Q-learning"
val STOCK_PRICES = "/static/IBM.csv"
val OPTION_PRICES = "/static/IBM_O.csv"
val STRIKE_PRICE = 190.0
val MIN_TIME_EXPIRATION = 6
val QUANTIZATION_STEP = 32
val ALPHA = 0.2
val DISCOUNT = 0.6
val MAX_EPISODE_LEN = 128
val NUM_EPISODES = 20
val NUM_NEIGHBHBOR_STATES = 3
run
方法接受奖励类型、量化步数、学习率和折扣率作为输入,显示模型中值的分布,并在散点图上显示最优策略的估计Q值。其工作流程如下:
1. 从
IBM.csv
文件中提取股票价格
2. 使用股票价格和量化步数创建期权模型
3. 从
IBM_O.csv
文件中提取期权价格
4. 使用期权模型和期权价格创建另一个模型进行评估
5. 使用
display
方法在散点图上显示估计的Q值
run
方法的代码如下:
private def run(rewardType: String,quantizeR: Int,alpha: Double,gamma:
Double): Int = {
val sPath = getClass.getResource(STOCK_PRICES).getPath
val src = DataSource(sPath, false, false, 1).get
val option = createOptionModel(src, quantizeR)
val oPricesSrc = DataSource(OPTION_PRICES, false, false, 1).get
val oPrices = oPricesSrc.extract.get
val model = createModel(option, oPrices, alpha, gamma)model.map(m =>
{if (rewardType != "Random")
display(m.bestPolicy.EQ,m.toString,s"$rewardType with quantization
order
$quantizeR")1}).getOrElse(-1)
}
createOptionModel
方法用于创建期权模型:
private def createOptionModel(src: DataSource, quantizeR: Int): OptionModel
=
new OptionModel("IBM", STRIKE_PRICE, src, MIN_TIME_EXPIRATION,
quantizeR)
createModel
方法用于创建期权盈亏模型:
def createModel(ibmOption: OptionModel,oPrice: Seq[Double],alpha:
Double,gamma: Double): Try[QLModel] = {
val qPriceMap = ibmOption.quantize(oPrice.toArray)
val numStates = qPriceMap.size
val neighbors = (n: Int) => {
def getProximity(idx: Int, radius: Int): List[Int] = {
val idx_max =
if (idx + radius >= numStates) numStates - 1
else idx + radius
val idx_min =
if (idx < radius) 0
else idx - radiusRange(idx_min, idx_max + 1).filter(_ !=
idx)./:(List[Int]())((xs, n) => n :: xs)}getProximity(n,
NUM_NEIGHBHBOR_STATES)
}
val qPrice: DblVec = qPriceMap.values.toVector
val profit: DblVec = normalize(zipWithShift(qPrice, 1).map {
case (x, y) => y - x}).get
val maxProfitIndex = profit.zipWithIndex.maxBy(_._1)._2
val reward = (x: Double, y: Double) => Math.exp(30.0 * (y - x))
val probabilities = (x: Double, y: Double) =>
if (y < 0.3 * x) 0.0
else 1.0println(s"$name Goal state index: $maxProfitIndex")
if (!QLearning.validateConstraints(profit.size, neighbors))
thrownew IllegalStateException("QLearningEval Incorrect states
transition constraint")
val instances = qPriceMap.keySet.toSeq.drop(1)
val config = QLConfig(alpha, gamma, MAX_EPISODE_LEN, NUM_EPISODES, 0.1)
val qLearning =
QLearning[Array[Int]](config,Array[Int](maxProfitIndex),profit,reward,proba
bilities,instances,Some(neighbors)) val modelO = qLearning.getModel
if (modelO.isDefined) {
val numTransitions = numStates * (numStates - 1)println(s"$name
Coverage ${modelO.get.coverage} for $numStates states and $numTransitions
transitions")
val profile = qLearning.dumpprintln(s"$name Execution
profilen$profile")display(qLearning)Success(modelO.get)}
else Failure(new IllegalStateException(s"$name model undefined"))
}
如果无法创建期权模型,代码会显示模型创建失败的消息。需要注意的是,考虑到使用的数据集较小(算法会很快收敛),
minCoverage
参数非常重要。
最后,通过以下代码运行程序:
def main(args: Array[String]): Unit = {
run("Maximum reward",QUANTIZATION_STEP, ALPHA, DISCOUNT)
}
运行结果显示了状态之间的转换,以及模型的覆盖情况、执行配置文件和最优策略的奖励等信息。
5. 模型评估
从输出结果可以看出,对于0.1的覆盖率,Q学习模型在126个状态下进行了15750次转换,以达到目标状态37并获得最优奖励。由于训练集较小,只有少数桶有实际值,这表明训练集的大小会影响状态数量。对于小训练集,Q学习会收敛得过快;而对于大训练集,Q学习需要更多时间来收敛,但会为每个逼近的桶提供至少一个值。
通过散点图可以更直观地看到每个状态的Q值,还可以显示不同训练周期或时期Q值对数的分布情况。测试使用学习率α = 0.1和折扣率γ = 0.9。
最终评估包括评估学习率和折扣率对训练覆盖率的影响。结果表明,随着学习率的增加,覆盖率会降低,这证实了学习率应小于0.2的一般规则。而评估折扣率对覆盖率影响的测试结果并不明确。鉴于有大量不同的配置参数和组合,将整个应用封装为一个Scala Web应用是一个不错的选择。
6. 封装为Scala Web应用
6.1 整体思路
目标是获取训练好的模型,并为最大奖励情况构建最佳策略的JSON输出。PlayML是一个Web应用,它使用期权交易Q学习算法提供一个计算API端点,该端点接受输入数据集和一些选项,计算q值并以JSON格式返回,以便在前端进行建模。
6.2 目录结构
Scala Web ML应用的目录结构如下:
- app
- ml
- controller
- API.scala
- Filters.scala
- conf
- application.conf
- lib
- public
- data
- IBM.csv
- IBM_O.csv
- assets
- js
- main.js
- index.html
- target
6.3 后端实现
在后端,封装了前面的Q学习实现,并创建了一个Scala控制器
API.scala
,用于从前端控制模型的行为。代码如下:
import java.nio.file.Paths
import org.codehaus.janino.Java
import ml.stats.TSeries.{normalize, zipWithShift}
import ml.workflow.data.DataSource
import ml.trading.OptionModel
import ml.Predef.{DblPair, DblVec}
import ml.reinforcement.qlearning.{QLConfig, QLModel, QLearning}
import scala.util.{Failure, Success, Try}
import play.api._
import play.api.data.Form
import play.api.libs.json._
import play.api.mvc._
import scala.util.{Failure, Success, Try}
class API extends Controller {
protected val name: String = "Q-learning"
private var sPath =
Paths.get((s"${"public/data/IBM.csv"}")).toAbsolutePath.toString
private var oPath =
Paths.get((s"${"public/data/IBM_O.csv"}")).toAbsolutePath.toString
// Run configuration parameters
private var STRIKE_PRICE = 190.0 // Option strike price
private var MIN_TIME_EXPIRATION = 6 // Minimum expiration time for the
option recorded
private var QUANTIZATION_STEP = 32 // Quantization step (Double => Int)
private var ALPHA = 0.2 // Learning rate
private var DISCOUNT = 0.6 // Discount rate used in the Q-Value update
equation
private var MAX_EPISODE_LEN = 128 // Maximum number of iteration for an
episode
private var NUM_EPISODES = 20 // Number of episodes used for training.
private var MIN_COVERAGE = 0.1
private var NUM_NEIGHBOR_STATES = 3 // Number of states accessible from
any other state
private var REWARD_TYPE = "Maximum reward"
private var ret = JsObject(Seq())
private var retry = 0
private def run(REWARD_TYPE: String,quantizeR: Int,alpha: Double,gamma:
Double) = {
val maybeModel = createModel(createOptionModel(DataSource(sPath,
false, false, 1).get, quantizeR), DataSource(oPath, false,
false, 1).get.extract.get, alpha, gamma)
if (maybeModel != None) {
val model = maybeModel.get
if (REWARD_TYPE != "Random") {
var value = JsArray(Seq())
var x = model.bestPolicy.EQ.distinct.map(x => {value =
value.append(JsObject(Seq("x" -> JsNumber(x._1), "y" ->
JsNumber(x._2))))})ret = ret.+("OPTIMAL", value)
}
}
}
private def createOptionModel(src: DataSource, quantizeR: Int):
OptionModel =
new OptionModel("IBM", STRIKE_PRICE, src, MIN_TIME_EXPIRATION,
quantizeR)
private def createModel(ibmOption: OptionModel,oPrice:
Seq[Double],alpha: Double,gamma: Double): Option[QLModel] = {
val qPriceMap = ibmOption.quantize(oPrice.toArray)
val numStates = qPriceMap.size
val neighbors = (n: Int) => {
def getProximity(idx: Int, radius: Int): List[Int] = {
val idx_max = if (idx + radius >= numStates) numStates - 1
else idx + radius
val idx_min = if (idx < radius) 0
else idx -
radiusscala.collection.immutable.Range(idx_min, idx_max + 1)
.filter(_ != idx)./:(List[Int]())((xs, n) => n
:: xs)
}
getProximity(n, NUM_NEIGHBOR_STATES)
}
val qPrice: DblVec = qPriceMap.values.toVector
val profit: DblVec = normalize(zipWithShift(qPrice, 1).map {
case (x, y) => y - x }).get
val maxProfitIndex = profit.zipWithIndex.maxBy(_._1)._2
val reward = (x: Double, y: Double) => Math.exp(30.0 * (y - x))
val probabilities = (x: Double, y: Double) =>
if (y < 0.3 * x) 0.0 else 1.0ret = ret.+("GOAL_STATE_INDEX",
JsNumber(maxProfitIndex))
if (!QLearning.validateConstraints(profit.size, neighbors))
{ret = ret.+("error", JsString("QLearningEval
Incorrect states transition constraint"))
thrownew IllegalStateException("QLearningEval Incorrect states
transition constraint")}
val instances = qPriceMap.keySet.toSeq.drop(1)
val config = QLConfig(alpha, gamma, MAX_EPISODE_LEN,
NUM_EPISODES, MIN_COVERAGE)
val qLearning = QLearning[Array[Int]](config,Array[Int]
(maxProfitIndex),profit,reward,probabilities,instances,Some(neighbors))
val modelO = qLearning.getModel
if (modelO.isDefined) {
val numTransitions = numStates * (numStates - 1)ret =
ret.+("COVERAGE",
JsNumber(modelO.get.coverage))ret =
ret.+("COVERAGE_STATES", JsNumber(numStates))
ret = ret.+("COVERAGE_TRANSITIONS",
JsNumber(numTransitions))
var value = JsArray()
var x = qLearning._counters.last._2.distinct.map(x =>
{value = value.append(JsNumber(x))
})
ret = ret.+("Q_VALUE", value)modelO
}
else {
if (retry > 5) {ret = ret.+("error", JsString(s"$name model
undefined"))
return None
}
retry += 1Thread.sleep(500)
return createModel(ibmOption,oPrice,alpha,gamma)
}
}
def compute = Action(parse.anyContent) { request =>
try {
if (request.body.asMultipartFormData != None) {
val formData = request.body.asMultipartFormData.get
if (formData.file("STOCK_PRICES").nonEmpty &&
formData.file("STOCK_PRICES").get.filename.nonEmpty)sPath =
formData.file("STOCK_PRICES").get.ref.file.toString
if (formData.file("OPTION_PRICES").nonEmpty &&
formData.file("OPTION_PRICES").get.filename.nonEmpty)oPath =
formData.file("OPTION_PRICES").get.ref.file.toString
val parts = formData.dataParts
if (parts.get("STRIKE_PRICE") != None)STRIKE_PRICE =
parts.get("STRIKE_PRICE").get.mkString("").toDouble
if (parts.get("MIN_TIME_EXPIRATION") !=
None)MIN_TIME_EXPIRATION =
parts.get("MIN_TIME_EXPIRATION").get.mkString("").toInt
if (parts.get("QUANTIZATION_STEP") != None)QUANTIZATION_STEP =
parts.get("QUANTIZATION_STEP").get.mkString("").toInt
if (parts.get("ALPHA") != None)ALPHA =
parts.get("ALPHA").get.mkString("").toDouble
if (parts.get("DISCOUNT") != None)DISCOUNT =
parts.get("DISCOUNT").get.mkString("").toDouble
if (parts.get("MAX_EPISODE_LEN") != None)MAX_EPISODE_LEN =
parts.get("MAX_EPISODE_LEN").get.mkString("").toInt
if (parts.get("NUM_EPISODES") != None)NUM_EPISODES =
parts.get("NUM_EPISODES").get.mkString("").toInt
if (parts.get("MIN_COVERAGE") != None)MIN_COVERAGE =
parts.get("MIN_COVERAGE").get.mkString("").toDouble
if (parts.get("NUM_NEIGHBOR_STATES") !=
None)NUM_NEIGHBOR_STATES =
parts.get("NUM_NEIGHBOR_STATES").get.mkString("").toInt
if (parts.get("REWARD_TYPE") != None)REWARD_TYPE =
parts.get("REWARD_TYPE").get.mkString("")
}
ret = JsObject(Seq("STRIKE_PRICE" ->
JsNumber(STRIKE_PRICE),"MIN_TIME_EXPIRATION" ->
JsNumber(MIN_TIME_EXPIRATION),
"QUANTIZATION_STEP" ->
JsNumber(QUANTIZATION_STEP),
"ALPHA" -> JsNumber(ALPHA),
"DISCOUNT" -> JsNumber(DISCOUNT),
"MAX_EPISODE_LEN" ->
JsNumber(MAX_EPISODE_LEN),
"NUM_EPISODES" -> JsNumber(NUM_EPISODES),
"MIN_COVERAGE" -> JsNumber(MIN_COVERAGE),
"NUM_NEIGHBOR_STATES" ->
JsNumber(NUM_NEIGHBOR_STATES),
"REWARD_TYPE" -> JsString(REWARD_TYPE)))
run(REWARD_TYPE, QUANTIZATION_STEP, ALPHA, DISCOUNT)
}
catch {
case e: Exception => {
ret = ret.+("exception", JsString(e.toString))
}
}
Ok(ret)
}
}
该代码与
QLearningMain.scala
文件的结构相似,主要完成以下两个重要操作:
- 作为一个
Action
,从UI获取输入并计算值
- 使用
JsObject()
方法将结果作为JSON对象返回,以便在UI上显示
6.4 前端实现
前端应用由两部分组成:使用Play框架构建的API端点和使用Angular.js构建的单页应用。前端应用将数据发送到API进行计算,然后使用
chart.js
显示结果。具体步骤如下:
1. 初始化表单
2. 与API进行通信
3. 使用覆盖数据和图表填充视图
算法的JSON输出应包含以下内容:
- 所有配置参数
-
GOAL_STATE_INDEX
,最大利润索引
-
COVERAGE
,达到预定义目标状态的训练试验或时期的比率
-
COVERAGE_STATES
,量化期权值的大小
-
COVERAGE_TRANSITIONS
,状态数量的平方
-
Q_VALUE
,所有状态的q值
-
OPTIMAL
,如果奖励类型不是随机的,则返回奖励最多的状态
前端代码在
PlayML/public/assets/js/main.js
文件中,用于初始化Angular.js应用和
chart.js
模块:
angular.module("App", ['chart.js']).controller("Ctrl", ['$scope', '$http',
function ($scope, $http) {
$scope.form = {REWARD_TYPE: "Maximum reward",NUM_NEIGHBOR_STATES:
3,STRIKE_PRICE: 190.0,MIN_TIME_EXPIRATION: 6,QUANTIZATION_STEP: 32,ALPHA:
0.2,DISCOUNT: 0.6,MAX_EPISODE_LEN: 128,NUM_EPISODES: 20,MIN_COVERAGE: 0.1
};
$scope.run = function () {
var formData = new FormData(document.getElementById('form'));
$http.post('/api/compute', formData, {
headers: {'Content-Type': undefined}}).then(function
successCallback(response) {
$scope.result = response.data;
$('#canvasContainer').html('');
if (response.data.OPTIMAL) {
$('#canvasContainer').append('<canvas
id="optimalCanvas"></canvas>')
Chart.Scatter(document.getElementById("optimalCanvas").getContext("2d"),
{data: { datasets: [{data: response.data.OPTIMAL}] }, options:
{...}});}if (response.data.Q_VALUE) {
$('#canvasContainer').append('<canvas id="valuesCanvas"></canvas>')
Chart.Line(document.getElementById("valuesCanvas").getContext("2d"), {
data: { labels: new Array(response.data.Q_VALUE.length), datasets:
[{
data: response.data.Q_VALUE }] }, options: {...}});}});}}]
);
将上述前端代码嵌入到HTML文件(
PlayML/public/index.html
)中,即可在Web上访问该应用,地址为
http://localhost:9000/
。
6.5 运行和部署说明
运行和部署该应用需要Java 1.8+和SBT作为依赖。具体步骤如下:
1. 下载应用代码,命名为
PlayML.zip
2. 解压文件,得到
ScalaML
文件夹
3. 进入
PlayML
项目文件夹
4. 执行
$ sudo sbt run
命令,下载所有依赖并运行应用
5. 应用可在
http://localhost:9000/
访问,可上传IBM股票和期权价格数据,并提供其他配置参数
部署应用为独立服务器的步骤如下:
1. 执行
$ sudo sbt dist
命令,构建应用二进制文件,输出文件位于
PlayML/target/universal/APP - NAME - SNAPSHOT.zip
,在本例中为
playml - 1.0.zip
2. 解压文件,然后运行
bin
目录下的脚本:
$ unzip APP-NAME-SNAPSHOT.zip
$ APP-NAME-SNAPSHOT /bin/ APP-NAME -Dhttp.port=9000
需要注意的是,绑定到端口9000可能需要root权限。
通过以上步骤,就可以完成基于Q学习和Scala Play框架的期权交易应用的开发、运行和部署。
7. 期权交易应用的技术架构与流程总结
7.1 技术架构
整个期权交易应用的技术架构主要由数据层、算法层、控制层和展示层构成,各层的功能和作用如下表所示:
| 层次 | 功能 | 涉及组件 |
| ---- | ---- | ---- |
| 数据层 | 负责数据的存储、读取和预处理,为后续的算法计算提供数据支持 |
DataSource
类、
IBM.csv
和
IBM_O.csv
文件 |
| 算法层 | 实现Q学习算法,对期权数据进行建模和训练,以预测最优策略 |
QLearning
类、
OptionModel
类、
OptionProperty
类 |
| 控制层 | 作为前端和后端的桥梁,接收前端的请求,调用算法层进行计算,并将结果返回给前端 |
API.scala
控制器 |
| 展示层 | 提供用户界面,让用户可以输入数据和查看计算结果,使用图表进行可视化展示 | Angular.js前端应用、
chart.js
库 |
7.2 整体流程
下面是该期权交易应用的整体工作流程,使用mermaid流程图表示:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B(加载数据):::process
B --> C(创建期权模型):::process
C --> D(训练Q学习模型):::process
D --> E{模型训练成功?}:::decision
E -->|是| F(获取最优策略):::process
E -->|否| D(训练Q学习模型):::process
F --> G(构建JSON输出):::process
G --> H(前端请求计算):::process
H --> I(控制层接收请求):::process
I --> J(调用算法计算):::process
J --> K(返回JSON结果):::process
K --> L(前端展示结果):::process
L --> M([结束]):::startend
从流程图可以看出,整个流程从数据加载开始,经过模型创建和训练,再到前端请求和结果展示,形成了一个完整的闭环。
8. 关键代码和技术点分析
8.1 Q学习算法的核心实现
Q学习算法是整个应用的核心,其核心代码位于
QLearning
类中。在
createModel
方法中,通过以下步骤完成Q学习模型的训练:
1.
数据预处理
:对期权价格数据进行量化处理,得到每个状态的Q值映射
qPriceMap
。
2.
状态转移约束
:定义状态转移的邻域函数
neighbors
,限制每个状态的可达状态范围。
3.
奖励和概率函数
:定义奖励函数
reward
和概率函数
probabilities
,用于评估状态转移的价值。
4.
模型训练
:使用
QLConfig
配置参数,实例化
QLearning
类,调用
getModel
方法进行模型训练。
以下是
createModel
方法的关键代码片段:
def createModel(ibmOption: OptionModel,oPrice: Seq[Double],alpha:
Double,gamma: Double): Try[QLModel] = {
val qPriceMap = ibmOption.quantize(oPrice.toArray)
val numStates = qPriceMap.size
val neighbors = (n: Int) => {
def getProximity(idx: Int, radius: Int): List[Int] = {
val idx_max = if (idx + radius >= numStates) numStates - 1
else idx + radius
val idx_min = if (idx < radius) 0
else idx - radius
scala.collection.immutable.Range(idx_min, idx_max + 1)
.filter(_ != idx)./:(List[Int]())((xs, n) => n :: xs)
}
getProximity(n, NUM_NEIGHBOR_STATES)
}
val qPrice: DblVec = qPriceMap.values.toVector
val profit: DblVec = normalize(zipWithShift(qPrice, 1).map {
case (x, y) => y - x }).get
val maxProfitIndex = profit.zipWithIndex.maxBy(_._1)._2
val reward = (x: Double, y: Double) => Math.exp(30.0 * (y - x))
val probabilities = (x: Double, y: Double) =>
if (y < 0.3 * x) 0.0
else 1.0
println(s"$name Goal state index: $maxProfitIndex")
if (!QLearning.validateConstraints(profit.size, neighbors))
throw new IllegalStateException("QLearningEval Incorrect states transition constraint")
val instances = qPriceMap.keySet.toSeq.drop(1)
val config = QLConfig(alpha, gamma, MAX_EPISODE_LEN, NUM_EPISODES, 0.1)
val qLearning =
QLearning[Array[Int]](config,Array[Int](maxProfitIndex),profit,reward,probabilities,instances,Some(neighbors))
val modelO = qLearning.getModel
if (modelO.isDefined) {
val numTransitions = numStates * (numStates - 1)
println(s"$name Coverage ${modelO.get.coverage} for $numStates states and $numTransitions transitions")
val profile = qLearning.dump
println(s"$name Execution profile\n$profile")
display(qLearning)
Success(modelO.get)
}
else {
Failure(new IllegalStateException(s"$name model undefined"))
}
}
8.2 前端与后端的交互
前端使用Angular.js构建单页应用,通过
$http
服务与后端的
API.scala
控制器进行交互。具体步骤如下:
1.
表单初始化
:在
main.js
中初始化表单数据,设置默认的配置参数。
2.
发送请求
:当用户点击运行按钮时,将表单数据封装为
FormData
对象,使用
$http.post
方法发送到后端的
/api/compute
接口。
3.
接收和处理结果
:后端控制器接收到请求后,调用
run
方法进行计算,将结果以JSON格式返回给前端。前端接收到结果后,使用
chart.js
库将数据绘制成图表展示给用户。
以下是前端代码的关键部分:
angular.module("App", ['chart.js']).controller("Ctrl", ['$scope', '$http',
function ($scope, $http) {
$scope.form = {REWARD_TYPE: "Maximum reward",NUM_NEIGHBOR_STATES:
3,STRIKE_PRICE: 190.0,MIN_TIME_EXPIRATION: 6,QUANTIZATION_STEP: 32,ALPHA:
0.2,DISCOUNT: 0.6,MAX_EPISODE_LEN: 128,NUM_EPISODES: 20,MIN_COVERAGE: 0.1
};
$scope.run = function () {
var formData = new FormData(document.getElementById('form'));
$http.post('/api/compute', formData, {
headers: {'Content-Type': undefined}}).then(function
successCallback(response) {
$scope.result = response.data;
$('#canvasContainer').html('');
if (response.data.OPTIMAL) {
$('#canvasContainer').append('<canvas
id="optimalCanvas"></canvas>')
Chart.Scatter(document.getElementById("optimalCanvas").getContext("2d"),
{data: { datasets: [{data: response.data.OPTIMAL}] }, options:
{...}});}if (response.data.Q_VALUE) {
$('#canvasContainer').append('<canvas id="valuesCanvas"></canvas>')
Chart.Line(document.getElementById("valuesCanvas").getContext("2d"), {
data: { labels: new Array(response.data.Q_VALUE.length), datasets:
[{
data: response.data.Q_VALUE }] }, options: {...}});}});}}]
);
9. 应用的优缺点与改进方向
9.1 优点
- 模型灵活性 :Q学习算法可以根据不同的数据集和配置参数进行调整,适应不同的期权交易场景。
-
可视化展示
:使用
chart.js库将计算结果以图表的形式展示给用户,直观地呈现了期权交易的最优策略和Q值分布。 - 前后端分离 :采用前后端分离的架构,前端使用Angular.js构建单页应用,后端使用Scala Play框架提供API服务,提高了开发效率和代码的可维护性。
9.2 缺点
- 训练时间 :对于大规模的数据集,Q学习算法的训练时间可能会较长,影响应用的实时性。
- 参数敏感性 :Q学习算法的性能对学习率、折扣率等参数比较敏感,需要进行大量的实验来确定最优参数。
- 数据依赖性 :模型的准确性高度依赖于训练数据的质量和数量,如果数据存在偏差或噪声,可能会导致模型的预测结果不准确。
9.3 改进方向
- 优化算法 :可以尝试使用更高效的Q学习算法变种,如深度Q网络(DQN),提高训练速度和模型的性能。
- 参数调优 :使用自动化的参数调优方法,如网格搜索、随机搜索或贝叶斯优化,来确定最优的参数组合。
- 数据增强 :通过数据增强技术,如数据平滑、噪声添加等,提高数据的质量和多样性,增强模型的泛化能力。
10. 总结与展望
通过以上的介绍,我们详细了解了基于Q学习和Scala Play框架的期权交易应用的开发过程。该应用结合了强化学习算法和Web开发技术,为期权交易提供了一种智能化的决策支持工具。通过对模型的评估和分析,我们也发现了一些存在的问题和改进的方向。
在未来的研究和开发中,可以进一步探索以下几个方面:
-
多资产期权交易
:将应用扩展到多资产期权交易场景,考虑不同资产之间的相关性和风险,提供更全面的投资建议。
-
实时数据处理
:引入实时数据接口,实现对期权价格的实时监测和更新,提高应用的实时性和实用性。
-
与其他算法融合
:将Q学习算法与其他机器学习算法,如神经网络、支持向量机等相结合,进一步提高模型的准确性和稳定性。
总之,基于Q学习和Scala Play框架的期权交易应用具有很大的发展潜力,通过不断的优化和改进,有望在金融领域发挥更大的作用。
超级会员免费看

645

被折叠的 条评论
为什么被折叠?



